diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 42d6092..8171b6c 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -412,6 +412,12 @@ from ..graphql.generated.list_ledger_blocked_source_graphs import ( ListLedgerBlockedSourceGraphsBlockedSourceGraphs as BlockedSourceGraphsPage, ) +from ..graphql.generated.list_ledger_report_shares import ( + ListLedgerReportShares, +) +from ..graphql.generated.list_ledger_report_shares import ( + ListLedgerReportSharesReportShares as ReportSharesPage, +) from ..graphql.generated.list_ledger_agents import ( ListLedgerAgents, ListLedgerAgentsAgents, @@ -528,6 +534,7 @@ LIST_INFORMATION_BLOCKS_GQL, LIST_LEDGER_ACCOUNTS_GQL, LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL, + LIST_LEDGER_REPORT_SHARES_GQL, LIST_LEDGER_AGENTS_GQL, LIST_LEDGER_DIMENSIONS_GQL, LIST_LEDGER_ELEMENTS_GQL, @@ -3535,6 +3542,34 @@ def revoke_report_share( "Revoke report share", envelope, RevokeReportShareResponse ) + def list_report_shares( + self, + graph_id: str, + report_id: str | None = None, + include_revoked: bool = False, + limit: int = 100, + offset: int = 0, + ) -> ReportSharesPage | None: + """Where this graph's reports have been shared — the sender's record + behind `share_report` and `revoke_report_share`. + + `report_id` narrows to one report; omitted, the list is the graph's whole + distribution log. Revoked shares stay out unless `include_revoked` is set: + a recipient shared to, revoked, and shared to again has one revoked row + and one live. + """ + data = self._query( + graph_id, + LIST_LEDGER_REPORT_SHARES_GQL, + { + "reportId": report_id, + "includeRevoked": include_revoked, + "limit": limit, + "offset": offset, + }, + ) + return ListLedgerReportShares.model_validate(data).report_shares + # ── Blocked source graphs ──────────────────────────────────────────── # # Sharing is authorized capability-style: whoever holds this graph's id diff --git a/robosystems_client/graphql/generated/__init__.py b/robosystems_client/graphql/generated/__init__.py index 1a08492..b993609 100644 --- a/robosystems_client/graphql/generated/__init__.py +++ b/robosystems_client/graphql/generated/__init__.py @@ -352,6 +352,12 @@ ListLedgerReconciliationsReconciliationsReconciliationsDifferencesRollForward, ListLedgerReconciliationsReconciliationsReconciliationsRollForward, ) +from .list_ledger_report_shares import ( + ListLedgerReportShares, + ListLedgerReportSharesReportShares, + ListLedgerReportSharesReportSharesPagination, + ListLedgerReportSharesReportSharesReportShares, +) from .list_ledger_reports import ( ListLedgerReports, ListLedgerReportsReports, @@ -456,6 +462,7 @@ LIST_LEDGER_MAPPINGS_GQL, LIST_LEDGER_PUBLISH_LISTS_GQL, LIST_LEDGER_RECONCILIATIONS_GQL, + LIST_LEDGER_REPORT_SHARES_GQL, LIST_LEDGER_REPORTS_GQL, LIST_LEDGER_STRUCTURES_GQL, LIST_LEDGER_TAXONOMIES_GQL, @@ -716,6 +723,7 @@ "LIST_LEDGER_PUBLISH_LISTS_GQL", "LIST_LEDGER_RECONCILIATIONS_GQL", "LIST_LEDGER_REPORTS_GQL", + "LIST_LEDGER_REPORT_SHARES_GQL", "LIST_LEDGER_STRUCTURES_GQL", "LIST_LEDGER_TAXONOMIES_GQL", "LIST_LEDGER_TRANSACTIONS_GQL", @@ -806,6 +814,10 @@ "ListLedgerReconciliationsReconciliationsReconciliationsDifferencesComponents", "ListLedgerReconciliationsReconciliationsReconciliationsDifferencesRollForward", "ListLedgerReconciliationsReconciliationsReconciliationsRollForward", + "ListLedgerReportShares", + "ListLedgerReportSharesReportShares", + "ListLedgerReportSharesReportSharesPagination", + "ListLedgerReportSharesReportSharesReportShares", "ListLedgerReports", "ListLedgerReportsReports", "ListLedgerReportsReportsReports", diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index a7fac4f..1a77bfe 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -58,6 +58,7 @@ from .list_ledger_mappings import ListLedgerMappings from .list_ledger_publish_lists import ListLedgerPublishLists from .list_ledger_reconciliations import ListLedgerReconciliations +from .list_ledger_report_shares import ListLedgerReportShares from .list_ledger_reports import ListLedgerReports from .list_ledger_structures import ListLedgerStructures from .list_ledger_taxonomies import ListLedgerTaxonomies @@ -125,6 +126,7 @@ LIST_LEDGER_MAPPINGS_GQL, LIST_LEDGER_PUBLISH_LISTS_GQL, LIST_LEDGER_RECONCILIATIONS_GQL, + LIST_LEDGER_REPORT_SHARES_GQL, LIST_LEDGER_REPORTS_GQL, LIST_LEDGER_STRUCTURES_GQL, LIST_LEDGER_TAXONOMIES_GQL, @@ -1016,6 +1018,29 @@ def list_ledger_reconciliations( data = self.get_data(response) return ListLedgerReconciliations.model_validate(data) + def list_ledger_report_shares( + self, + include_revoked: bool, + limit: int, + offset: int, + report_id: Union[Optional[str], UnsetType] = UNSET, + **kwargs: Any, + ) -> ListLedgerReportShares: + variables: dict[str, object] = { + "reportId": report_id, + "includeRevoked": include_revoked, + "limit": limit, + "offset": offset, + } + response = self.execute( + query=LIST_LEDGER_REPORT_SHARES_GQL, + operation_name="ListLedgerReportShares", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return ListLedgerReportShares.model_validate(data) + def list_ledger_reports( self, lifecycle: Union[Optional[ReportLifecycle], UnsetType] = UNSET, diff --git a/robosystems_client/graphql/generated/list_ledger_report_shares.py b/robosystems_client/graphql/generated/list_ledger_report_shares.py new file mode 100644 index 0000000..d7fde85 --- /dev/null +++ b/robosystems_client/graphql/generated/list_ledger_report_shares.py @@ -0,0 +1,40 @@ +from typing import Optional + +from pydantic import Field + +from .base_model import BaseModel + + +class ListLedgerReportShares(BaseModel): + report_shares: Optional["ListLedgerReportSharesReportShares"] = Field( + alias="reportShares" + ) + + +class ListLedgerReportSharesReportShares(BaseModel): + report_shares: list["ListLedgerReportSharesReportSharesReportShares"] = Field( + alias="reportShares" + ) + pagination: "ListLedgerReportSharesReportSharesPagination" + + +class ListLedgerReportSharesReportSharesReportShares(BaseModel): + id: str + report_id: str = Field(alias="reportId") + target_graph_id: str = Field(alias="targetGraphId") + target_graph_name: Optional[str] = Field(alias="targetGraphName") + shared_by: str = Field(alias="sharedBy") + shared_at: str = Field(alias="sharedAt") + revoked_at: Optional[str] = Field(alias="revokedAt") + fact_count: int = Field(alias="factCount") + + +class ListLedgerReportSharesReportSharesPagination(BaseModel): + total: int + limit: int + offset: int + has_more: bool = Field(alias="hasMore") + + +ListLedgerReportShares.model_rebuild() +ListLedgerReportSharesReportShares.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index ab19e70..c6b0d8b 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -55,6 +55,7 @@ "LIST_LEDGER_PUBLISH_LISTS_GQL", "LIST_LEDGER_RECONCILIATIONS_GQL", "LIST_LEDGER_REPORTS_GQL", + "LIST_LEDGER_REPORT_SHARES_GQL", "LIST_LEDGER_STRUCTURES_GQL", "LIST_LEDGER_TAXONOMIES_GQL", "LIST_LEDGER_TRANSACTIONS_GQL", @@ -2199,6 +2200,34 @@ } """ +LIST_LEDGER_REPORT_SHARES_GQL = """ +query ListLedgerReportShares($reportId: String, $includeRevoked: Boolean! = false, $limit: Int! = 100, $offset: Int! = 0) { + reportShares( + reportId: $reportId + includeRevoked: $includeRevoked + limit: $limit + offset: $offset + ) { + reportShares { + id + reportId + targetGraphId + targetGraphName + sharedBy + sharedAt + revokedAt + factCount + } + pagination { + total + limit + offset + hasMore + } + } +} +""" + LIST_LEDGER_REPORTS_GQL = """ query ListLedgerReports($lifecycle: ReportLifecycle, $entityId: String) { reports(lifecycle: $lifecycle, entityId: $entityId) { diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerReportShares.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerReportShares.graphql new file mode 100644 index 0000000..8d2f70c --- /dev/null +++ b/robosystems_client/graphql/operations/ledger/ListLedgerReportShares.graphql @@ -0,0 +1,19 @@ +query ListLedgerReportShares( + $reportId: String + $includeRevoked: Boolean! = false + $limit: Int! = 100 + $offset: Int! = 0 +) { + reportShares( + reportId: $reportId + includeRevoked: $includeRevoked + limit: $limit + offset: $offset + ) { + reportShares { + id reportId targetGraphId targetGraphName + sharedBy sharedAt revokedAt factCount + } + pagination { total limit offset hasMore } + } +} diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index 38b3294..c2a1d0c 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -814,6 +814,34 @@ type Query { listId: String! ): PublishListDetail + """ + Where this graph's reports have been shared: each recipient graph, when, + by whom, how many facts it received, and whether the share was revoked. + + The sender's record behind `share-report` and `revoke-report-share`. + Revoked shares are history: a recipient shared to, revoked, and shared to + again has two rows, one revoked and one live. + """ + reportShares( + """One report's shares. Omit for the graph's whole distribution log.""" + reportId: String = null + + """ + Also list revoked shares. Off by default, so the list is the copies recipients still hold. + """ + includeRevoked: Boolean! = false + + """ + Maximum rows to return, 1-1000. Omit to use this field's own default; out of range raises `INVALID_PAGINATION`. + """ + limit: Int = null + + """ + Rows to skip before returning, 0 or greater. Out of range raises `INVALID_PAGINATION`. + """ + offset: Int = null + ): ReportShareList + """ Source graphs barred from sharing reports into this graph. @@ -4626,6 +4654,44 @@ type PublishListMember { addedAt: DateTime! } +"""Paginated list of a report's shares, newest first.""" +type ReportShareList { + """Shares, newest first.""" + reportShares: [ReportShare!]! + + """Pagination metadata.""" + pagination: PaginationInfo! +} + +"""One delivery of a report to a recipient graph.""" +type ReportShare { + """Share row identifier (ULID).""" + id: String! + + """The shared report's ID in this graph.""" + reportId: String! + + """The recipient's graph ID.""" + targetGraphId: String! + + """Display name of the recipient graph (if known).""" + targetGraphName: String + + """User ID that shared the report.""" + sharedBy: String! + + """When the report was shared, or last re-shared.""" + sharedAt: DateTime! + + """ + When the share was revoked. Null while the recipient holds the copy; a re-share after a revoke is a new row, so a revoked row stays revoked. + """ + revokedAt: DateTime + + """Number of facts delivered to the recipient on this share.""" + factCount: Int! +} + """Paginated list of blocked source graphs.""" type BlockedSourceGraphList { """Blocked source graphs.""" diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index 189a203..88155a4 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -3599,6 +3599,67 @@ def test_list_blocked_source_graphs(self, mock_query, mock_config, graph_id): assert page.blocked_source_graphs[0].source_graph_id == "kg_sender" assert page.pagination.total == 1 + @patch("robosystems_client.clients.ledger_client.LedgerClient._query") + def test_list_report_shares(self, mock_query, mock_config, graph_id): + mock_query.return_value = { + "reportShares": { + "reportShares": [ + { + "id": "share_2", + "reportId": "rpt_1", + "targetGraphId": "kg_recipient", + "targetGraphName": "Harbor Fund", + "sharedBy": "usr_1", + "sharedAt": "2026-10-01T12:00:00Z", + "revokedAt": None, + "factCount": 47, + }, + { + "id": "share_1", + "reportId": "rpt_1", + "targetGraphId": "kg_recipient", + "targetGraphName": "Harbor Fund", + "sharedBy": "usr_1", + "sharedAt": "2026-09-01T12:00:00Z", + "revokedAt": "2026-09-15T12:00:00Z", + "factCount": 40, + }, + ], + "pagination": {"total": 2, "limit": 100, "offset": 0, "hasMore": False}, + } + } + client = LedgerClient(mock_config) + page = client.list_report_shares(graph_id, report_id="rpt_1", include_revoked=True) + assert page is not None + assert [s.id for s in page.report_shares] == ["share_2", "share_1"] + assert page.report_shares[0].revoked_at is None + assert page.report_shares[1].revoked_at is not None + assert page.report_shares[0].fact_count == 47 + assert page.pagination.total == 2 + assert mock_query.call_args.args[2] == { + "reportId": "rpt_1", + "includeRevoked": True, + "limit": 100, + "offset": 0, + } + + @patch("robosystems_client.clients.ledger_client.LedgerClient._query") + def test_list_report_shares_defaults_to_live_shares_of_every_report( + self, mock_query, mock_config, graph_id + ): + mock_query.return_value = { + "reportShares": { + "reportShares": [], + "pagination": {"total": 0, "limit": 100, "offset": 0, "hasMore": False}, + } + } + client = LedgerClient(mock_config) + page = client.list_report_shares(graph_id) + assert page is not None + assert page.report_shares == [] + assert mock_query.call_args.args[2]["reportId"] is None + assert mock_query.call_args.args[2]["includeRevoked"] is False + # ── Chart of accounts ─────────────────────────────────────────────────