From afdba513296bb55a4cbcdc39cbe336040349c009 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Sat, 10 Oct 2026 19:57:21 -0500 Subject: [PATCH] fix(omvs): by-name type filter reaches Egeria; read nestedSolutionComponents (ISSUE-131, ISSUE-133) ISSUE-131: 13 *_by_name wrappers passed the type filter to _async_get_name_request as `metadata_element_type` (the _async_find_request spelling). The helper reads `metadata_element_type_name`, so the value fell into **kwargs, FilterRequestBody dropped it, and the search ran unfiltered by type. The helper now accepts both spellings (the canonical one wins), and the 13 wrappers use the canonical key. The 14 find_* wrappers were already correct and are unchanged. ISSUE-133: Egeria returns a solution component's children as nestedSolutionComponents; no REST response has subComponents. GovernanceOfficer._extract_solution_components_properties read only subComponents (it currently has no callers). Removed the never-matching subComponents fallbacks in solution_architect.py (two) and md_processing/v2/solution_architect.py. Signed-off-by: Dan Wolfson --- md_processing/v2/solution_architect.py | 4 - pyegeria/core/_server_client.py | 7 ++ pyegeria/omvs/actor_manager.py | 14 +-- pyegeria/omvs/collection_manager.py | 2 +- pyegeria/omvs/digital_business.py | 2 +- pyegeria/omvs/governance_officer.py | 7 +- pyegeria/omvs/location_arena.py | 2 +- pyegeria/omvs/runtime_manager.py | 2 +- pyegeria/omvs/solution_architect.py | 12 --- pyegeria/omvs/subject_area.py | 2 +- pyegeria/omvs/time_keeper.py | 2 +- tests/micro-tests/test_issue_131_133.py | 121 ++++++++++++++++++++++++ 12 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 tests/micro-tests/test_issue_131_133.py diff --git a/md_processing/v2/solution_architect.py b/md_processing/v2/solution_architect.py index 20712dda..d8c7c921 100644 --- a/md_processing/v2/solution_architect.py +++ b/md_processing/v2/solution_architect.py @@ -384,10 +384,6 @@ async def _get_component_related_elements(self, guid: str) -> Dict[str, Any]: for comp in response.get('nestedSolutionComponents', []): res["sub_component_guids"].append(comp['relatedElement']['elementHeader'].get('guid')) - # subComponents - for comp in response.get('subComponents', []): - res["sub_component_guids"].append(comp['elementHeader'].get('guid')) - # usedInSolutionComponents (Parents) for comp in response.get('usedInSolutionComponents', []): res["parent_component_guids"].append(comp['relatedElement']['elementHeader'].get('guid')) diff --git a/pyegeria/core/_server_client.py b/pyegeria/core/_server_client.py index ac6c5530..399397f0 100644 --- a/pyegeria/core/_server_client.py +++ b/pyegeria/core/_server_client.py @@ -6589,6 +6589,13 @@ async def _async_get_name_request(self, url: str, _type: str, _gen_output: Calla body: Optional[dict | FilterRequestBody] = None, max_mermaid_node_count=10, **kwargs) -> Any: + # _async_find_request spells this argument `metadata_element_type`; a wrapper copied from a find_* + # sibling passed that spelling here, where it fell into **kwargs and FilterRequestBody silently + # dropped it, so the by-name search ran unfiltered by type (ISSUE-131). Accept both. + legacy_type_name = kwargs.pop("metadata_element_type", None) + if metadata_element_type_name is None: + metadata_element_type_name = legacy_type_name + if isinstance(body, FilterRequestBody): validated_body = body elif isinstance(body, dict): diff --git a/pyegeria/omvs/actor_manager.py b/pyegeria/omvs/actor_manager.py index 957a9853..0e060bf1 100644 --- a/pyegeria/omvs/actor_manager.py +++ b/pyegeria/omvs/actor_manager.py @@ -953,7 +953,7 @@ async def _async_get_actor_profiles_by_name( url = str(HttpUrl(f"{self.command_root}/actor-profiles/by-name")) params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, @@ -2514,7 +2514,7 @@ async def _async_get_actor_roles_by_name( url = str(HttpUrl(f"{self.command_root}/actor-roles/by-name")) params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, @@ -4193,7 +4193,7 @@ async def _async_get_user_identities_by_name( url = str(HttpUrl(f"{self.command_root}/user-identities/by-name")) params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, @@ -4885,7 +4885,7 @@ async def _async_get_contribution_records_by_name( url = f"{self.command_root}/contribution-records/by-name" params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, @@ -6069,7 +6069,7 @@ async def _async_get_contact_details_by_name( url = f"{self.command_root}/contact-details/by-name" params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, @@ -6875,7 +6875,7 @@ async def _async_get_perspectives_by_name( url = str(HttpUrl(f"{self.command_root}/perspectives/by-name")) params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, @@ -7681,7 +7681,7 @@ async def _async_get_skills_by_name( url = str(HttpUrl(f"{self.command_root}/skills/by-name")) params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, diff --git a/pyegeria/omvs/collection_manager.py b/pyegeria/omvs/collection_manager.py index 420338b5..927e1a42 100644 --- a/pyegeria/omvs/collection_manager.py +++ b/pyegeria/omvs/collection_manager.py @@ -844,7 +844,7 @@ async def _async_get_collections_by_name( url = str(HttpUrl(f"{self.collection_command_root}/by-name")) params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, diff --git a/pyegeria/omvs/digital_business.py b/pyegeria/omvs/digital_business.py index 85c91b70..dbe5a9ab 100644 --- a/pyegeria/omvs/digital_business.py +++ b/pyegeria/omvs/digital_business.py @@ -473,7 +473,7 @@ async def _async_get_business_capabilities_by_name( url = f"{self.digital_business_command_root}/collections/by-name" params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, diff --git a/pyegeria/omvs/governance_officer.py b/pyegeria/omvs/governance_officer.py index 3f450349..877b0111 100644 --- a/pyegeria/omvs/governance_officer.py +++ b/pyegeria/omvs/governance_officer.py @@ -271,12 +271,13 @@ def _extract_solution_components_properties(self, element: Union[Dict, List[Dict # Extract sub components sub_comp_md = "" - sub_components = element.get('subComponents', None) + # Egeria returns children as nestedSolutionComponents (SolutionComposition); there is no + # subComponents field in any REST response (ISSUE-133). + sub_components = element.get('nestedSolutionComponents', None) if sub_components: for sub_component in sub_components: - sub_comp_prop = sub_component['properties'] + sub_comp_prop = sub_component['relatedElement']['properties'] sub_comp_name = sub_comp_prop.get("displayName", None) - sub_comp_desc = sub_comp_prop.get("description", None) sub_comp_md += f" {sub_comp_name}" comp_graph = element.get('mermaidGraph', None) diff --git a/pyegeria/omvs/location_arena.py b/pyegeria/omvs/location_arena.py index f36f2562..59ee274c 100644 --- a/pyegeria/omvs/location_arena.py +++ b/pyegeria/omvs/location_arena.py @@ -1204,7 +1204,7 @@ async def _async_get_locations_by_name( url = f"{self.ref_location_command_base}/locations/by-name" params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, diff --git a/pyegeria/omvs/runtime_manager.py b/pyegeria/omvs/runtime_manager.py index 52df163e..7e68f6be 100644 --- a/pyegeria/omvs/runtime_manager.py +++ b/pyegeria/omvs/runtime_manager.py @@ -3468,7 +3468,7 @@ async def _async_get_metadata_repository_cohorts_by_name( url = f"{self.runtime_command_root}/metadata-repository-cohorts/by-name" params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "graph_query_depth": graph_query_depth, "start_from": start_from, diff --git a/pyegeria/omvs/solution_architect.py b/pyegeria/omvs/solution_architect.py index 39ca5606..a6aef044 100644 --- a/pyegeria/omvs/solution_architect.py +++ b/pyegeria/omvs/solution_architect.py @@ -672,14 +672,6 @@ def _get_component_rel_elements_dict(self, el_struct: dict)-> dict | str: parent_qnames.append(comp['relatedElement']['properties'].get("qualifiedName", "")) parent_names.append(comp['relatedElement']['properties'].get('displayName', "")) - sub_components = el_struct.get("subComponents", {}) - if sub_components: - for sub_component in sub_components: - sub_component_guids.append(sub_component['elementHeader']['guid']) - sub_component_qnames.append(sub_component['properties'].get("qualifiedName", "")) - sub_component_names.append(sub_component['properties'].get('displayName', "")) - - mermaid = el_struct.get("mermaidGraph", {}) return {"in_components_guids": parent_guids, @@ -5184,10 +5176,6 @@ def get_component_related_elements(self, guid: str, **kwargs) -> dict: guid = comp['relatedElement']['elementHeader'].get('guid', None) sub_component_guids.append(guid) - sub_components = response.get("subComponents",{}) - for sub_component in sub_components: - sub_component_guids.append(sub_component["elementHeader"]["guid"]) - parent_components = response.get('usedInSolutionComponents', None) if parent_components is not None: for comp in parent_components: diff --git a/pyegeria/omvs/subject_area.py b/pyegeria/omvs/subject_area.py index 82467e25..10e36faa 100644 --- a/pyegeria/omvs/subject_area.py +++ b/pyegeria/omvs/subject_area.py @@ -638,7 +638,7 @@ async def _async_get_subject_areas_by_name( url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/subject-area/collections/by-name" params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, diff --git a/pyegeria/omvs/time_keeper.py b/pyegeria/omvs/time_keeper.py index 901f5b2e..643fc99b 100644 --- a/pyegeria/omvs/time_keeper.py +++ b/pyegeria/omvs/time_keeper.py @@ -1356,7 +1356,7 @@ async def _async_get_context_events_by_name( url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/time-keeper/context-events/by-name" params = { "filter_string": name, - "metadata_element_type": metadata_element_type_name, + "metadata_element_type_name": metadata_element_type_name, "metadata_element_subtypes": metadata_element_subtypes, "include_only_relationships": include_only_relationships, "skip_relationships": skip_relationships, diff --git a/tests/micro-tests/test_issue_131_133.py b/tests/micro-tests/test_issue_131_133.py new file mode 100644 index 00000000..3c057644 --- /dev/null +++ b/tests/micro-tests/test_issue_131_133.py @@ -0,0 +1,121 @@ +""" +ISSUE-131: 13 *_by_name wrappers passed the type filter to _async_get_name_request as +`metadata_element_type` (the _async_find_request spelling); the helper reads `metadata_element_type_name`, +so the key fell into **kwargs, FilterRequestBody dropped it, and the search ran unfiltered by type. +ISSUE-133: Egeria returns a solution component's children as nestedSolutionComponents; there is no +subComponents field in any REST response. +""" +import json + +import pytest + +from pyegeria.omvs.actor_manager import ActorManager +from pyegeria.omvs.collection_manager import CollectionManager +from pyegeria.omvs.digital_business import DigitalBusiness +from pyegeria.omvs.governance_officer import GovernanceOfficer +from pyegeria.omvs.location_arena import LocationArena +from pyegeria.omvs.runtime_manager import RuntimeManager +from pyegeria.omvs.solution_architect import SolutionArchitect +from pyegeria.omvs.subject_area import SubjectArea +from pyegeria.omvs.time_keeper import TimeKeeper + + +class _Response: + def json(self): + return {"relatedHTTPCode": 200, "elements": []} + + +def _capture(cls, monkeypatch): + monkeypatch.setattr(cls, "check_connection", lambda self: "test") + client = cls("view-server", "https://localhost:1", "user", "pwd") + calls: list = [] + + async def fake_make_request(method, url, payload=None, *args, **kwargs): + calls.append({"url": url, "body": json.loads(payload) if isinstance(payload, str) else payload}) + return _Response() + + client._async_make_request = fake_make_request + return client, calls + + +# --- ISSUE-131 ------------------------------------------------------------------------------ + +BY_NAME_WRAPPERS = [ + (ActorManager, "_async_get_actor_profiles_by_name"), + (ActorManager, "_async_get_actor_roles_by_name"), + (ActorManager, "_async_get_user_identities_by_name"), + (ActorManager, "_async_get_contribution_records_by_name"), + (ActorManager, "_async_get_contact_details_by_name"), + (ActorManager, "_async_get_perspectives_by_name"), + (ActorManager, "_async_get_skills_by_name"), + (CollectionManager, "_async_get_collections_by_name"), + (DigitalBusiness, "_async_get_business_capabilities_by_name"), + (LocationArena, "_async_get_locations_by_name"), + (RuntimeManager, "_async_get_metadata_repository_cohorts_by_name"), + (SubjectArea, "_async_get_subject_areas_by_name"), + (TimeKeeper, "_async_get_context_events_by_name"), +] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("cls, method", BY_NAME_WRAPPERS) +async def test_by_name_wrappers_send_the_type_filter(cls, method, monkeypatch): + client, calls = _capture(cls, monkeypatch) + + await getattr(client, method)("x", metadata_element_type_name="ProbeType") + + assert calls[0]["body"]["metadataElementTypeName"] == "ProbeType" + + +@pytest.mark.asyncio +async def test_name_request_accepts_the_find_request_spelling(monkeypatch): + client, calls = _capture(CollectionManager, monkeypatch) + + await client._async_get_name_request("https://localhost:1/x/by-name", _type="T", _gen_output=None, + filter_string="x", metadata_element_type="ProbeType") + + assert calls[0]["body"]["metadataElementTypeName"] == "ProbeType" + + +@pytest.mark.asyncio +async def test_name_request_prefers_the_canonical_spelling(monkeypatch): + client, calls = _capture(CollectionManager, monkeypatch) + + await client._async_get_name_request("https://localhost:1/x/by-name", _type="T", _gen_output=None, + filter_string="x", metadata_element_type_name="Canonical", + metadata_element_type="Legacy") + + assert calls[0]["body"]["metadataElementTypeName"] == "Canonical" + + +# --- ISSUE-133 ------------------------------------------------------------------------------ + +def _component_with_child(): + return { + "elementHeader": {"guid": "parent-guid"}, + "properties": {"displayName": "Parent", "qualifiedName": "SolutionComponent::Parent"}, + "nestedSolutionComponents": [{ + "relatedElement": { + "elementHeader": {"guid": "child-guid"}, + "properties": {"displayName": "Child", "qualifiedName": "SolutionComponent::Child"}, + } + }], + } + + +def test_governance_officer_reads_nested_solution_components(monkeypatch): + monkeypatch.setattr(GovernanceOfficer, "check_connection", lambda self: "test") + client = GovernanceOfficer("view-server", "https://localhost:1", "user", "pwd") + + props = client._extract_solution_components_properties(_component_with_child()) + + assert props["sub_components"].strip() == "Child" + + +def test_solution_architect_reads_nested_solution_components(monkeypatch): + monkeypatch.setattr(SolutionArchitect, "check_connection", lambda self: "test") + client = SolutionArchitect("view-server", "https://localhost:1", "user", "pwd") + + rel = client._get_component_rel_elements_dict(_component_with_child()) + + assert rel["sub_component_guids"] == ["child-guid"]