From 305d16a379912873e97680f6d9c2f377eec7f83e Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Thu, 15 Oct 2026 22:54:42 -0500 Subject: [PATCH] feat(ledger): list_information_block_summaries, the block list as identities and counts The rows, filters, order and page of list_information_blocks, read by the server in one query instead of an envelope build per block. The call that fills a picker: fact_count > 0 marks a block whose envelope carries facts. The schema snapshot also takes periodRole from robosystems #1832. Waits on the robosystems release carrying #1834. --- robosystems_client/clients/ledger_client.py | 40 ++++++++++ .../graphql/generated/__init__.py | 8 ++ .../graphql/generated/client.py | 29 +++++++ .../list_information_block_summaries.py | 27 +++++++ .../graphql/generated/operations.py | 25 ++++++ .../ListInformationBlockSummaries.graphql | 21 +++++ robosystems_client/graphql/schema.graphql | 80 +++++++++++++++++++ tests/test_facade_wire_shapes.py | 16 ++++ tests/test_ledger_client.py | 28 +++++++ 9 files changed, 274 insertions(+) create mode 100644 robosystems_client/graphql/generated/list_information_block_summaries.py create mode 100644 robosystems_client/graphql/operations/ledger/ListInformationBlockSummaries.graphql diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index a144d4c..2a12b93 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -365,6 +365,10 @@ from ..graphql.generated.get_ledger_trial_balance import ( GetLedgerTrialBalanceTrialBalance as TrialBalance, ) +from ..graphql.generated.list_information_block_summaries import ( + ListInformationBlockSummaries, + ListInformationBlockSummariesInformationBlockSummaries, +) from ..graphql.generated.list_information_blocks import ( ListInformationBlocks, ListInformationBlocksInformationBlocks, @@ -475,6 +479,7 @@ GET_LEDGER_SUMMARY_GQL, GET_LEDGER_TRANSACTION_GQL, GET_LEDGER_TRIAL_BALANCE_GQL, + LIST_INFORMATION_BLOCK_SUMMARIES_GQL, LIST_INFORMATION_BLOCKS_GQL, LIST_LEDGER_ACCOUNTS_GQL, LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL, @@ -1587,6 +1592,41 @@ def list_information_blocks( ) return ListInformationBlocks.model_validate(data).information_blocks + def list_information_block_summaries( + self, + graph_id: str, + *, + block_type: str | None = None, + category: str | None = None, + limit: int | None = None, + offset: int | None = None, + scenario_id: str | None = None, + entity_id: str | None = None, + ) -> list[ListInformationBlockSummariesInformationBlockSummaries]: + """List Information Blocks as identities and counts, one row per block. + + The rows, filters, order and page of ``list_information_blocks``, read + in one query instead of an envelope build per block — the call that + fills a picker. ``fact_count > 0`` marks a block whose envelope carries + facts (a library-seeded structure variant carries none); a block's + atoms come from ``get_information_block``. + """ + data = self._query( + graph_id, + LIST_INFORMATION_BLOCK_SUMMARIES_GQL, + { + "blockType": block_type, + "category": category, + "limit": limit, + "offset": offset, + "scenarioId": scenario_id, + "entityId": entity_id, + }, + ) + return ListInformationBlockSummaries.model_validate( + data + ).information_block_summaries + def create_information_block( self, graph_id: str, diff --git a/robosystems_client/graphql/generated/__init__.py b/robosystems_client/graphql/generated/__init__.py index b39dad3..e17ebff 100644 --- a/robosystems_client/graphql/generated/__init__.py +++ b/robosystems_client/graphql/generated/__init__.py @@ -198,6 +198,10 @@ ) from .get_library_taxonomy import GetLibraryTaxonomy, GetLibraryTaxonomyLibraryTaxonomy from .list_chart_templates import ListChartTemplates, ListChartTemplatesChartTemplates +from .list_information_block_summaries import ( + ListInformationBlockSummaries, + ListInformationBlockSummariesInformationBlockSummaries, +) from .list_information_blocks import ( ListInformationBlocks, ListInformationBlocksInformationBlocks, @@ -380,6 +384,7 @@ GET_LIBRARY_ELEMENT_GQL, GET_LIBRARY_TAXONOMY_GQL, LIST_CHART_TEMPLATES_GQL, + LIST_INFORMATION_BLOCK_SUMMARIES_GQL, LIST_INFORMATION_BLOCKS_GQL, LIST_INVESTOR_PORTFOLIOS_GQL, LIST_INVESTOR_POSITIONS_GQL, @@ -601,6 +606,7 @@ "GraphQLClientInvalidResponseError", "LIST_CHART_TEMPLATES_GQL", "LIST_INFORMATION_BLOCKS_GQL", + "LIST_INFORMATION_BLOCK_SUMMARIES_GQL", "LIST_INVESTOR_PORTFOLIOS_GQL", "LIST_INVESTOR_POSITIONS_GQL", "LIST_INVESTOR_SECURITIES_GQL", @@ -626,6 +632,8 @@ "LIST_LIBRARY_TAXONOMY_ARCS_GQL", "ListChartTemplates", "ListChartTemplatesChartTemplates", + "ListInformationBlockSummaries", + "ListInformationBlockSummariesInformationBlockSummaries", "ListInformationBlocks", "ListInformationBlocksInformationBlocks", "ListInformationBlocksInformationBlocksArtifact", diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index 98de538..8ee0283 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -37,6 +37,7 @@ from .get_library_element_equivalents import GetLibraryElementEquivalents from .get_library_taxonomy import GetLibraryTaxonomy from .list_chart_templates import ListChartTemplates +from .list_information_block_summaries import ListInformationBlockSummaries from .list_information_blocks import ListInformationBlocks from .list_investor_portfolios import ListInvestorPortfolios from .list_investor_positions import ListInvestorPositions @@ -97,6 +98,7 @@ GET_LIBRARY_ELEMENT_GQL, GET_LIBRARY_TAXONOMY_GQL, LIST_CHART_TEMPLATES_GQL, + LIST_INFORMATION_BLOCK_SUMMARIES_GQL, LIST_INFORMATION_BLOCKS_GQL, LIST_INVESTOR_PORTFOLIOS_GQL, LIST_INVESTOR_POSITIONS_GQL, @@ -616,6 +618,33 @@ def list_chart_templates(self, **kwargs: Any) -> ListChartTemplates: data = self.get_data(response) return ListChartTemplates.model_validate(data) + def list_information_block_summaries( + self, + block_type: Union[Optional[str], UnsetType] = UNSET, + category: Union[Optional[str], UnsetType] = UNSET, + limit: Union[Optional[int], UnsetType] = UNSET, + offset: Union[Optional[int], UnsetType] = UNSET, + scenario_id: Union[Optional[str], UnsetType] = UNSET, + entity_id: Union[Optional[str], UnsetType] = UNSET, + **kwargs: Any, + ) -> ListInformationBlockSummaries: + variables: dict[str, object] = { + "blockType": block_type, + "category": category, + "limit": limit, + "offset": offset, + "scenarioId": scenario_id, + "entityId": entity_id, + } + response = self.execute( + query=LIST_INFORMATION_BLOCK_SUMMARIES_GQL, + operation_name="ListInformationBlockSummaries", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return ListInformationBlockSummaries.model_validate(data) + def list_information_blocks( self, block_type: Union[Optional[str], UnsetType] = UNSET, diff --git a/robosystems_client/graphql/generated/list_information_block_summaries.py b/robosystems_client/graphql/generated/list_information_block_summaries.py new file mode 100644 index 0000000..1a46505 --- /dev/null +++ b/robosystems_client/graphql/generated/list_information_block_summaries.py @@ -0,0 +1,27 @@ +from typing import Optional + +from pydantic import Field + +from .base_model import BaseModel + + +class ListInformationBlockSummaries(BaseModel): + information_block_summaries: list[ + "ListInformationBlockSummariesInformationBlockSummaries" + ] = Field(alias="informationBlockSummaries") + + +class ListInformationBlockSummariesInformationBlockSummaries(BaseModel): + id: str + block_type: str = Field(alias="blockType") + name: str + display_name: str = Field(alias="displayName") + category: str + taxonomy_id: Optional[str] = Field(alias="taxonomyId") + taxonomy_name: Optional[str] = Field(alias="taxonomyName") + entity_id: Optional[str] = Field(alias="entityId") + fact_count: int = Field(alias="factCount") + connection_count: int = Field(alias="connectionCount") + + +ListInformationBlockSummaries.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index 20c14e9..0fe48d2 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -34,6 +34,7 @@ "GET_LIBRARY_TAXONOMY_GQL", "LIST_CHART_TEMPLATES_GQL", "LIST_INFORMATION_BLOCKS_GQL", + "LIST_INFORMATION_BLOCK_SUMMARIES_GQL", "LIST_INVESTOR_PORTFOLIOS_GQL", "LIST_INVESTOR_POSITIONS_GQL", "LIST_INVESTOR_SECURITIES_GQL", @@ -1242,6 +1243,30 @@ } """ +LIST_INFORMATION_BLOCK_SUMMARIES_GQL = """ +query ListInformationBlockSummaries($blockType: String, $category: String, $limit: Int, $offset: Int, $scenarioId: String, $entityId: String) { + informationBlockSummaries( + blockType: $blockType + category: $category + limit: $limit + offset: $offset + scenarioId: $scenarioId + entityId: $entityId + ) { + id + blockType + name + displayName + category + taxonomyId + taxonomyName + entityId + factCount + connectionCount + } +} +""" + LIST_INFORMATION_BLOCKS_GQL = """ query ListInformationBlocks($blockType: String, $category: String, $limit: Int, $offset: Int, $scenarioId: String, $entityId: String) { informationBlocks( diff --git a/robosystems_client/graphql/operations/ledger/ListInformationBlockSummaries.graphql b/robosystems_client/graphql/operations/ledger/ListInformationBlockSummaries.graphql new file mode 100644 index 0000000..ff8a916 --- /dev/null +++ b/robosystems_client/graphql/operations/ledger/ListInformationBlockSummaries.graphql @@ -0,0 +1,21 @@ +query ListInformationBlockSummaries( + $blockType: String + $category: String + $limit: Int + $offset: Int + $scenarioId: String + $entityId: String +) { + informationBlockSummaries( + blockType: $blockType + category: $category + limit: $limit + offset: $offset + scenarioId: $scenarioId + entityId: $entityId + ) { + id blockType name displayName category + taxonomyId taxonomyName entityId + factCount connectionCount + } +} diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index a20a118..6524dab 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -791,6 +791,43 @@ type Query { entityId: String = null ): [InformationBlock!]! + """ + List Information Blocks as identities and counts, one row per block. + + The rows, filters, order and page of `informationBlocks`, read in one + query instead of an envelope build per block — the call that fills a + picker or a rail. `factCount` says whether a block carries facts for + the entity read (a library-seeded structure variant carries none); + a block's atoms come from `informationBlock(id)`. + """ + informationBlockSummaries( + """Filter to one registered block type, e.g. `schedule`.""" + blockType: String = null + + """ + Filter on the registry entry's category label, e.g. `Close` or `Reporting`. + """ + category: String = null + + """ + 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 + + """ + A forecast block's structure id; its entity scopes the list when `entityId` is omitted. + """ + scenarioId: String = null + + """The entity whose blocks and facts to list. Omit for the group parent.""" + entityId: String = null + ): [InformationBlockSummary!]! + """Fetch a single Taxonomy Block envelope by id.""" taxonomyBlock( """The taxonomy block's structure id.""" @@ -3809,6 +3846,11 @@ type FactRow { """Indentation depth in the structure hierarchy (0 = root).""" depth: Int! + + """ + On a roll-forward, `start` for the beginning-balance row and `end` for the ending-balance row — one concept, two rows. Null elsewhere. + """ + periodRole: String } """ @@ -3948,6 +3990,44 @@ type BlockedSourceGraph { reason: String } +""" +One row of a block list: the envelope's identity with counts in place +of its atoms. Read in one query, so a graph's whole catalogue lists in the +time one envelope builds. +""" +type InformationBlockSummary { + id: String! + + """Discriminator — 'schedule', …""" + blockType: String! + name: String! + + """Registry-sourced display label (e.g., 'Schedule').""" + displayName: String! + + """Registry-sourced sidebar grouping ('Close', 'Reporting', …).""" + category: String! + + """Source taxonomy the Structure was seeded from.""" + taxonomyId: String + + """Display name of the source taxonomy.""" + taxonomyName: String + + """ + The entity whose books the block reads: its owner for a schedule, reconciliation or forecast; the entity asked for, else the group parent, on a block shared by the group. Null on the library. + """ + entityId: String + + """ + Facts in the block's scope for that entity, every set — what its envelope carries. Zero on a library-seeded structure variant and on a schedule that has run off into history, which a picker leaves out. + """ + factCount: Int! + + """Arcs the block's structure carries.""" + connectionCount: Int! +} + type TaxonomyBlock { id: ID! name: String! diff --git a/tests/test_facade_wire_shapes.py b/tests/test_facade_wire_shapes.py index de5dcfd..b79882a 100644 --- a/tests/test_facade_wire_shapes.py +++ b/tests/test_facade_wire_shapes.py @@ -702,6 +702,22 @@ def test_create_report_sends_the_entity(self, httpx_mock): body = json.loads(httpx_mock.get_requests()[0].content) assert body["entity_id"] == "ent_sub" + def test_list_information_block_summaries_sends_filters(self, httpx_mock): + httpx_mock.add_response( + method="POST", + url=f"{BASE}/extensions/{GRAPH}/graphql", + json={"data": {"informationBlockSummaries": []}}, + ) + + blocks = LedgerClient(CONFIG).list_information_block_summaries( + GRAPH, block_type="forecast", entity_id="ent_sub" + ) + + assert blocks == [] + payload = json.loads(httpx_mock.get_requests()[0].content) + assert payload["variables"] == {"blockType": "forecast", "entityId": "ent_sub"} + assert "informationBlockSummaries" in payload["query"] + def test_list_information_blocks_sends_scenario_id(self, httpx_mock): httpx_mock.add_response( method="POST", diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index 20043ec..f20f038 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -1674,6 +1674,34 @@ def test_list_information_blocks(self, mock_execute, mock_config, graph_id): variables = mock_execute.call_args[0][2] assert variables["blockType"] == "schedule" + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_list_information_block_summaries(self, mock_execute, mock_config, graph_id): + mock_execute.return_value = { + "informationBlockSummaries": [ + { + "id": "str_sched_1", + "blockType": "schedule", + "name": "Depreciation", + "displayName": "Schedule", + "category": "Close", + "taxonomyId": "tax_01", + "taxonomyName": "My CoA", + "entityId": "ent_parent", + "factCount": 12, + "connectionCount": 3, + } + ] + } + client = LedgerClient(mock_config) + result = client.list_information_block_summaries(graph_id, limit=200) + assert len(result) == 1 + assert result[0].id == "str_sched_1" + assert result[0].block_type == "schedule" + assert (result[0].fact_count, result[0].connection_count) == (12, 3) + assert not hasattr(result[0], "facts") + variables = mock_execute.call_args[0][2] + assert variables["limit"] == 200 + @patch("robosystems_client.graphql.client.GraphQLClient.execute") def test_get_information_block(self, mock_execute, mock_config, graph_id): mock_execute.return_value = {