From 39dd742cbb8047d97c1e662b06267ecf3cfa33c7 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Sat, 10 Oct 2026 21:35:23 -0500 Subject: [PATCH 1/2] fix(omvs): by-name wrappers accept a body-only call (ISSUE-135) 14 *_by_name wrappers required their first argument (`name` or `filter_string`) even though they accept `body`, so a body-only call raised TypeError (reported by Resource Explorer for get_actor_profiles_by_name). The first argument now defaults to None in both the async and sync versions, in actor_manager (2), asset_maker (2), connection_maker (3), data_designer (2), data_discovery, product_manager, schema_maker (2) and solution_architect. None already meant "match everything", like "*", so name-only and name+body calls are unchanged. _async_get_data_value_specifications_by_name also gets the classification_names=None default its sync version already had. Signed-off-by: Dan Wolfson --- pyegeria/omvs/actor_manager.py | 8 +- pyegeria/omvs/asset_maker.py | 8 +- pyegeria/omvs/connection_maker.py | 12 +-- pyegeria/omvs/data_designer.py | 9 ++- pyegeria/omvs/data_discovery.py | 4 +- pyegeria/omvs/product_manager.py | 4 +- pyegeria/omvs/schema_maker.py | 8 +- pyegeria/omvs/solution_architect.py | 4 +- .../test_issue_135_by_name_body_only.py | 80 +++++++++++++++++++ 9 files changed, 109 insertions(+), 28 deletions(-) create mode 100644 tests/micro-tests/test_issue_135_by_name_body_only.py diff --git a/pyegeria/omvs/actor_manager.py b/pyegeria/omvs/actor_manager.py index 0e060bf1..68f13fd3 100644 --- a/pyegeria/omvs/actor_manager.py +++ b/pyegeria/omvs/actor_manager.py @@ -906,7 +906,7 @@ def find_actor_profiles( @dynamic_catch async def _async_get_actor_profiles_by_name( self, - name: str, + name: str | None = None, metadata_element_type_name: str | None = "ActorProfile", metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, @@ -976,7 +976,7 @@ async def _async_get_actor_profiles_by_name( def get_actor_profiles_by_name( self, - name: str, + name: str | None = None, metadata_element_type_name: str | None = "ActorProfile", metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, @@ -2467,7 +2467,7 @@ def find_actor_roles( @dynamic_catch async def _async_get_actor_roles_by_name( self, - name: str, + name: str | None = None, metadata_element_type_name: str | None = "ActorRole", metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, @@ -2537,7 +2537,7 @@ async def _async_get_actor_roles_by_name( def get_actor_roles_by_name( self, - name: str, + name: str | None = None, metadata_element_type_name: str | None = "ActorRole", metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, diff --git a/pyegeria/omvs/asset_maker.py b/pyegeria/omvs/asset_maker.py index 6da398f9..769ae0b3 100644 --- a/pyegeria/omvs/asset_maker.py +++ b/pyegeria/omvs/asset_maker.py @@ -559,7 +559,7 @@ def delete_asset( @dynamic_catch async def _async_get_assets_by_name( self, - filter_string: str, + filter_string: str | None = None, metadata_element_type_name: str | None = "Asset", metadata_element_subtypes: list[str] | None = None, include_only_classified_elements: list[str] | None = None, @@ -654,7 +654,7 @@ async def _async_get_assets_by_name( @dynamic_catch def get_assets_by_name( self, - filter_string: str, + filter_string: str | None = None, metadata_element_type_name: str | None = "Asset", metadata_element_subtypes: list[str] | None = None, include_only_classified_elements: list[str] | None = None, @@ -5920,7 +5920,7 @@ def find_software_capabilities( @dynamic_catch async def _async_get_software_capabilities_by_name( self, - name: str, + name: str | None = None, metadata_element_type_name: str | None = "SoftwareCapability", graph_query_depth: int = 3, start_from: int = 0, @@ -5980,7 +5980,7 @@ async def _async_get_software_capabilities_by_name( @dynamic_catch def get_software_capabilities_by_name( self, - name: str, + name: str | None = None, metadata_element_type_name: str | None = "SoftwareCapability", graph_query_depth: int = 3, start_from: int = 0, diff --git a/pyegeria/omvs/connection_maker.py b/pyegeria/omvs/connection_maker.py index ff7d6de1..794cf604 100644 --- a/pyegeria/omvs/connection_maker.py +++ b/pyegeria/omvs/connection_maker.py @@ -125,7 +125,7 @@ def get_connection_by_guid( async def _async_get_connections_by_name( self, - name: str, + name: str | None = None, metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, skip_relationships: list[str] | None = None, @@ -157,7 +157,7 @@ async def _async_get_connections_by_name( def get_connections_by_name( self, - name: str, + name: str | None = None, metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, skip_relationships: list[str] | None = None, @@ -363,7 +363,7 @@ def get_connector_type_by_guid( async def _async_get_connector_types_by_name( self, - name: str, + name: str | None = None, metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, skip_relationships: list[str] | None = None, @@ -395,7 +395,7 @@ async def _async_get_connector_types_by_name( def get_connector_types_by_name( self, - name: str, + name: str | None = None, metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, skip_relationships: list[str] | None = None, @@ -664,7 +664,7 @@ def get_endpoint_by_guid( async def _async_get_endpoints_by_name( self, - name: str, + name: str | None = None, metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, skip_relationships: list[str] | None = None, @@ -696,7 +696,7 @@ async def _async_get_endpoints_by_name( def get_endpoints_by_name( self, - name: str, + name: str | None = None, metadata_element_subtypes: list[str] | None = None, include_only_relationships: list[str] | None = None, skip_relationships: list[str] | None = None, diff --git a/pyegeria/omvs/data_designer.py b/pyegeria/omvs/data_designer.py index c6a76274..397aaf07 100644 --- a/pyegeria/omvs/data_designer.py +++ b/pyegeria/omvs/data_designer.py @@ -2892,7 +2892,7 @@ def find_data_fields(self, search_string: str, return response @dynamic_catch - async def _async_get_data_fields_by_name(self, filter_string: str, classification_names: Optional[list[str]] = None, + async def _async_get_data_fields_by_name(self, filter_string: str | None = None, classification_names: Optional[list[str]] = None, body: Optional[dict | FilterRequestBody] = None, start_from: int = 0, page_size: int = 0, graph_query_depth: int = 3, output_format: str = 'JSON', @@ -2956,7 +2956,7 @@ async def _async_get_data_fields_by_name(self, filter_string: str, classificatio return response @dynamic_catch - def get_data_fields_by_name(self, filter_string: str, classification_names: Optional[list[str]] = None, body: dict = None, + def get_data_fields_by_name(self, filter_string: str | None = None, classification_names: Optional[list[str]] = None, body: dict = None, start_from: int = 0, page_size: int = max_paging_size, graph_query_depth: int = 3, output_format: str = 'JSON', report_spec: str | dict = None, **kwargs) -> list | str: @@ -3025,7 +3025,8 @@ def get_data_field_by_guid(self, guid: str, element_type: Optional[str] = None, self._async_get_data_field_by_guid(guid, element_type, body, graph_query_depth, output_format, report_spec, **kwargs)) @dynamic_catch - async def _async_get_data_value_specifications_by_name(self, filter_string: str, classification_names: list[str], + async def _async_get_data_value_specifications_by_name(self, filter_string: str | None = None, + classification_names: Optional[list[str]] = None, body: Optional[dict | FilterRequestBody] = None, start_from: int = 0, page_size: int = 0, graph_query_depth: int = 3, output_format: str = 'JSON', @@ -3088,7 +3089,7 @@ async def _async_get_data_value_specifications_by_name(self, filter_string: str, return response @dynamic_catch - def get_data_value_specifications_by_name(self, filter_string: str, classification_names: Optional[list[str]] = None, body: dict = None, + def get_data_value_specifications_by_name(self, filter_string: str | None = None, classification_names: Optional[list[str]] = None, body: dict = None, start_from: int = 0, page_size: int = max_paging_size, graph_query_depth: int = 3, output_format: str = 'JSON', report_spec: str | dict = None, **kwargs) -> list | str: diff --git a/pyegeria/omvs/data_discovery.py b/pyegeria/omvs/data_discovery.py index b3bf4338..bbebc625 100644 --- a/pyegeria/omvs/data_discovery.py +++ b/pyegeria/omvs/data_discovery.py @@ -1722,12 +1722,12 @@ def delete_analysis_report(self, report_guid: str, body: Optional[dict | DeleteE loop.run_until_complete(self._async_delete_analysis_report(report_guid, body)) @dynamic_catch - async def _async_get_analysis_reports_by_name(self, name: str, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: + async def _async_get_analysis_reports_by_name(self, name: str | None = None, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: """Get analysis reports by name. Async version.""" url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports/by-name" return await self._async_get_name_request(url, _type="AnalysisReport", _gen_output=self._generate_annotation_output, filter_string=name, start_from=start_from, page_size=page_size, **kwargs) - def get_analysis_reports_by_name(self, name: str, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: + def get_analysis_reports_by_name(self, name: str | None = None, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: """Get analysis reports by name.""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_get_analysis_reports_by_name(name, start_from, page_size, **kwargs)) diff --git a/pyegeria/omvs/product_manager.py b/pyegeria/omvs/product_manager.py index 41b52235..e66809a7 100644 --- a/pyegeria/omvs/product_manager.py +++ b/pyegeria/omvs/product_manager.py @@ -2070,7 +2070,7 @@ def generate_data_product(self, digital_product_guid: str) -> dict | str: @dynamic_catch async def _async_get_governance_action_processes_by_name( self, - name: str, + name: str | None = None, body: Optional[dict] = None, start_from: int = 0, page_size: int = 0, @@ -2115,7 +2115,7 @@ async def _async_get_governance_action_processes_by_name( @dynamic_catch def get_governance_action_processes_by_name( self, - name: str, + name: str | None = None, body: Optional[dict] = None, start_from: int = 0, page_size: int = 0, diff --git a/pyegeria/omvs/schema_maker.py b/pyegeria/omvs/schema_maker.py index 49b9ffa3..9277440d 100644 --- a/pyegeria/omvs/schema_maker.py +++ b/pyegeria/omvs/schema_maker.py @@ -483,23 +483,23 @@ def create_schema_attribute_from_template(self, body: dict | TemplateRequestBody return loop.run_until_complete(self._async_create_schema_attribute_from_template(body)) @dynamic_catch - async def _async_get_schema_types_by_name(self, name: str, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: + async def _async_get_schema_types_by_name(self, name: str | None = None, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: """Get schema types by name. Async version.""" url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/schema-maker/schema-types/by-name" return await self._async_get_name_request(url, _type="SchemaType", _gen_output=self._generate_schema_output, filter_string=name, start_from=start_from, page_size=page_size, **kwargs) - def get_schema_types_by_name(self, name: str, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: + def get_schema_types_by_name(self, name: str | None = None, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: """Get schema types by name.""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_get_schema_types_by_name(name, start_from, page_size, **kwargs)) @dynamic_catch - async def _async_get_schema_attributes_by_name(self, name: str, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: + async def _async_get_schema_attributes_by_name(self, name: str | None = None, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: """Get schema attributes by name. Async version.""" url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/schema-maker/schema-attributes/by-name" return await self._async_get_name_request(url, _type="SchemaAttribute", _gen_output=self._generate_schema_output, filter_string=name, start_from=start_from, page_size=page_size, **kwargs) - def get_schema_attributes_by_name(self, name: str, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: + def get_schema_attributes_by_name(self, name: str | None = None, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: """Get schema attributes by name.""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_get_schema_attributes_by_name(name, start_from, page_size, **kwargs)) diff --git a/pyegeria/omvs/solution_architect.py b/pyegeria/omvs/solution_architect.py index a6aef044..64fa8119 100644 --- a/pyegeria/omvs/solution_architect.py +++ b/pyegeria/omvs/solution_architect.py @@ -6686,7 +6686,7 @@ def find_concept_model_elements(self, search_string: str = "*", output_format, report_spec, **kwargs)) @dynamic_catch - async def _async_get_concept_model_elements_by_name(self, name: str, + async def _async_get_concept_model_elements_by_name(self, name: str | None = None, body: Optional[dict | SearchStringRequestBody] = None, start_from: int = 0, page_size: int = max_paging_size, output_format: str = "JSON", report_spec: str | dict | None = None, @@ -6699,7 +6699,7 @@ async def _async_get_concept_model_elements_by_name(self, name: str, output_format=output_format, report_spec=report_spec, **kwargs) @dynamic_catch - def get_concept_model_elements_by_name(self, name: str, + def get_concept_model_elements_by_name(self, name: str | None = None, body: Optional[dict | SearchStringRequestBody] = None, start_from: int = 0, page_size: int = max_paging_size, output_format: str = "JSON", report_spec: str | dict | None = None, diff --git a/tests/micro-tests/test_issue_135_by_name_body_only.py b/tests/micro-tests/test_issue_135_by_name_body_only.py new file mode 100644 index 00000000..4d080fe5 --- /dev/null +++ b/tests/micro-tests/test_issue_135_by_name_body_only.py @@ -0,0 +1,80 @@ +""" +ISSUE-135: 14 *_by_name wrappers required their first argument (`name` or `filter_string`) even though +they accept `body`, so a body-only call raised TypeError. The first argument now defaults to None in +both the async and sync versions; name-only and name+body calls are unchanged. +""" +import inspect +import json + +import pytest + +from pyegeria.omvs.actor_manager import ActorManager +from pyegeria.omvs.asset_maker import AssetMaker +from pyegeria.omvs.connection_maker import ConnectionMaker +from pyegeria.omvs.data_designer import DataDesigner +from pyegeria.omvs.data_discovery import DataDiscovery +from pyegeria.omvs.product_manager import ProductManager +from pyegeria.omvs.schema_maker import SchemaMaker +from pyegeria.omvs.solution_architect import SolutionArchitect + +WRAPPERS = [ + (ActorManager, "get_actor_profiles_by_name"), + (ActorManager, "get_actor_roles_by_name"), + (AssetMaker, "get_assets_by_name"), + (AssetMaker, "get_software_capabilities_by_name"), + (ConnectionMaker, "get_connections_by_name"), + (ConnectionMaker, "get_connector_types_by_name"), + (ConnectionMaker, "get_endpoints_by_name"), + (DataDesigner, "get_data_fields_by_name"), + (DataDesigner, "get_data_value_specifications_by_name"), + (DataDiscovery, "get_analysis_reports_by_name"), + (ProductManager, "get_governance_action_processes_by_name"), + (SchemaMaker, "get_schema_types_by_name"), + (SchemaMaker, "get_schema_attributes_by_name"), + (SolutionArchitect, "get_concept_model_elements_by_name"), +] + + +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(json.loads(payload) if isinstance(payload, str) else payload) + return _Response() + + client._async_make_request = fake_make_request + return client, calls + + +@pytest.mark.parametrize("cls, method", WRAPPERS) +def test_first_argument_is_optional_in_both_versions(cls, method): + for name in (method, f"_async_{method}"): + params = list(inspect.signature(getattr(cls, name)).parameters.values())[1:] + assert params[0].default is None, f"{cls.__name__}.{name}({params[0].name}) still required" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("cls, method", WRAPPERS) +async def test_body_only_call_sends_the_body(cls, method, monkeypatch): + client, calls = _capture(cls, monkeypatch) + + await getattr(client, f"_async_{method}")(body={"class": "FilterRequestBody", "filter": "Probe"}) + + assert calls[0]["filter"] == "Probe" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("cls, method", WRAPPERS) +async def test_name_only_call_is_unchanged(cls, method, monkeypatch): + client, calls = _capture(cls, monkeypatch) + + await getattr(client, f"_async_{method}")("Erin") + + assert calls[0]["filter"] == "Erin" From e381211ae5ff439f1fd0b33398b7731bb89eb25f Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Sat, 10 Oct 2026 21:44:50 -0500 Subject: [PATCH 2/2] fix(omvs): remaining by-name body-only gaps (ISSUE-135) - SolutionArchitect get_info_supply_chain/solution_blueprints/ solution_components/solution_roles_by_name did body["filter"] = name, which overwrote the caller's filter with None on a body-only call and mutated the caller's dict. They now copy the body and set filter only when a name is given (a given name still wins, as before). - The same four sent paging as URL query parameters, which Egeria's SolutionArchitectResource/ActorManagerResource ignore (only addImplementation is a query parameter, on information-supply-chains). Paging now goes in the body, without overriding values the body sets. - AutomatedCuration.get_engine_actions_by_name required a name even with a body, contrary to its docstring ("the body supersedes the name"). It now requires one only when no body is given. It also passed the type filter as 'metadata_element_type'; now the canonical key (the 6.2.1 helper fix already accepted the old one). Signed-off-by: Dan Wolfson --- pyegeria/omvs/automated_curation.py | 6 +- pyegeria/omvs/solution_architect.py | 51 ++++++++--- .../test_issue_135_by_name_body_only.py | 87 +++++++++++++++++++ 3 files changed, 128 insertions(+), 16 deletions(-) diff --git a/pyegeria/omvs/automated_curation.py b/pyegeria/omvs/automated_curation.py index 0d3f80d4..2514633a 100644 --- a/pyegeria/omvs/automated_curation.py +++ b/pyegeria/omvs/automated_curation.py @@ -2897,7 +2897,9 @@ async def _async_get_engine_actions_by_name( if name is None and "filter_string" in kwargs: name = kwargs.pop("filter_string") - validate_name(name) + # A supplied body supersedes the name (as documented), so only require a name without one (ISSUE-135). + if body is None: + validate_name(name) url = f"{self.ref_curation_command_base}/assets/by-name" @@ -2905,7 +2907,7 @@ async def _async_get_engine_actions_by_name( params = { 'filter_string': name, - 'metadata_element_type': metadata_element_type, + 'metadata_element_type_name': metadata_element_type, 'metadata_element_subtypes': metadata_element_subtypes, 'include_only_relationships': include_only_relationships, 'skip_relationships': skip_relationships, diff --git a/pyegeria/omvs/solution_architect.py b/pyegeria/omvs/solution_architect.py index 64fa8119..ade48f87 100644 --- a/pyegeria/omvs/solution_architect.py +++ b/pyegeria/omvs/solution_architect.py @@ -2026,8 +2026,7 @@ async def _async_get_info_supply_chain_by_name(self, name: Optional[str] = None, name = kwargs.pop("filter_string") add_impl = str(add_implementation).lower() - possible_query_params = query_string( - [("addImplementation", add_impl), ("startFrom", start_from), ("pageSize", page_size)]) + possible_query_params = query_string([("addImplementation", add_impl)]) if body is None: body = { @@ -2035,7 +2034,14 @@ async def _async_get_info_supply_chain_by_name(self, name: Optional[str] = None, "filter": name, } else: - body["filter"] = name + # Copy, so the caller's dict is not changed, and keep the body's own filter on a body-only call + # (ISSUE-135). + body = dict(body) + if name is not None: + body["filter"] = name + # The server reads paging from the body only; it ignores startFrom/pageSize query parameters. + body.setdefault("startFrom", start_from) + body.setdefault("pageSize", page_size) url = (f"{self.solution_architect_command_root}/information-supply-chains/by-name" f"{possible_query_params}") @@ -3472,7 +3478,14 @@ async def _async_get_solution_blueprints_by_name(self, name: Optional[str] = Non "filter": name, } else: - body["filter"] = name + # Copy, so the caller's dict is not changed, and keep the body's own filter on a body-only call + # (ISSUE-135). + body = dict(body) + if name is not None: + body["filter"] = name + # The server reads paging from the body only; it ignores startFrom/pageSize query parameters. + body.setdefault("startFrom", start_from) + body.setdefault("pageSize", page_size) url = f"{self.solution_architect_command_root}/solution-blueprints/by-name" @@ -4925,7 +4938,6 @@ async def _async_get_solution_components_by_name(self, name: Optional[str] = Non if name is None and "filter_string" in kwargs: name = kwargs.pop("filter_string") - possible_query_params = query_string([("startFrom", start_from), ("pageSize", page_size)]) if body is None: body = { @@ -4933,10 +4945,16 @@ async def _async_get_solution_components_by_name(self, name: Optional[str] = Non "filter": name, } else: - body["filter"] = name - - url = (f"{self.solution_architect_command_root}/solution-components/by-name" - f"{possible_query_params}") + # Copy, so the caller's dict is not changed, and keep the body's own filter on a body-only call + # (ISSUE-135). + body = dict(body) + if name is not None: + body["filter"] = name + # The server reads paging from the body only; it ignores startFrom/pageSize query parameters. + body.setdefault("startFrom", start_from) + body.setdefault("pageSize", page_size) + + url = f"{self.solution_architect_command_root}/solution-components/by-name" response: Response = await self._async_make_request("POST", url, body_slimmer(body), **kwargs) element = response.json().get("elements", NO_ELEMENTS_FOUND) if element == NO_ELEMENTS_FOUND: @@ -6373,7 +6391,6 @@ async def _async_get_solution_roles_by_name(self, name: Optional[str] = None, bo if name is None and "filter_string" in kwargs: name = kwargs.pop("filter_string") - possible_query_params = query_string([("startFrom", start_from), ("pageSize", page_size)]) if body is None: body = { @@ -6381,10 +6398,16 @@ async def _async_get_solution_roles_by_name(self, name: Optional[str] = None, bo "filter": name, } else: - body["filter"] = name - - url = (f"{self.solution_architect_command_root}/solution-roles/by-name" - f"{possible_query_params}") + # Copy, so the caller's dict is not changed, and keep the body's own filter on a body-only call + # (ISSUE-135). + body = dict(body) + if name is not None: + body["filter"] = name + # The server reads paging from the body only; it ignores startFrom/pageSize query parameters. + body.setdefault("startFrom", start_from) + body.setdefault("pageSize", page_size) + + url = f"{self.solution_architect_command_root}/solution-roles/by-name" response: Response = await self._async_make_request("POST", url, body_slimmer(body), **kwargs) element = response.json().get("elements", NO_ELEMENTS_FOUND) if element == NO_ELEMENTS_FOUND: diff --git a/tests/micro-tests/test_issue_135_by_name_body_only.py b/tests/micro-tests/test_issue_135_by_name_body_only.py index 4d080fe5..5789d15c 100644 --- a/tests/micro-tests/test_issue_135_by_name_body_only.py +++ b/tests/micro-tests/test_issue_135_by_name_body_only.py @@ -78,3 +78,90 @@ async def test_name_only_call_is_unchanged(cls, method, monkeypatch): await getattr(client, f"_async_{method}")("Erin") assert calls[0]["filter"] == "Erin" + + +# --- Wrappers that already defaulted `name` but mishandled a body-only call ----------------- + +from pyegeria.omvs.automated_curation import AutomatedCuration + +SA_WRAPPERS = [ + "_async_get_info_supply_chain_by_name", + "_async_get_solution_blueprints_by_name", + "_async_get_solution_components_by_name", + "_async_get_solution_roles_by_name", +] + + +def _capture_with_url(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 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method", SA_WRAPPERS) +async def test_solution_architect_body_only_keeps_the_filter_and_the_callers_dict(method, monkeypatch): + client, calls = _capture_with_url(SolutionArchitect, monkeypatch) + body = {"class": "FilterRequestBody", "filter": "Probe"} + + await getattr(client, method)(body=body) + + assert calls[0]["body"]["filter"] == "Probe" + assert body == {"class": "FilterRequestBody", "filter": "Probe"} # not mutated + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method", SA_WRAPPERS) +async def test_solution_architect_name_still_sets_the_filter(method, monkeypatch): + client, calls = _capture_with_url(SolutionArchitect, monkeypatch) + + await getattr(client, method)("Erin", body={"class": "FilterRequestBody", "filter": "Probe"}) + + assert calls[0]["body"]["filter"] == "Erin" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method", SA_WRAPPERS) +async def test_solution_architect_paging_goes_in_the_body(method, monkeypatch): + """Egeria's SolutionArchitectResource/ActorManagerResource read paging from the body only.""" + client, calls = _capture_with_url(SolutionArchitect, monkeypatch) + + await getattr(client, method)("Erin", start_from=5, page_size=7) + + assert calls[0]["body"]["startFrom"] == 5 and calls[0]["body"]["pageSize"] == 7 + assert "startFrom" not in calls[0]["url"] and "pageSize" not in calls[0]["url"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method", SA_WRAPPERS) +async def test_solution_architect_body_paging_is_not_overridden(method, monkeypatch): + client, calls = _capture_with_url(SolutionArchitect, monkeypatch) + + await getattr(client, method)(body={"class": "FilterRequestBody", "filter": "P", "pageSize": 3}) + + assert calls[0]["body"]["pageSize"] == 3 + + +@pytest.mark.asyncio +async def test_engine_actions_by_name_accepts_a_body_only_call(monkeypatch): + client, calls = _capture_with_url(AutomatedCuration, monkeypatch) + + await client._async_get_engine_actions_by_name(body={"class": "FilterRequestBody", "filter": "Probe"}) + + assert calls[0]["body"]["filter"] == "Probe" + + +@pytest.mark.asyncio +async def test_engine_actions_by_name_sends_its_type_filter(monkeypatch): + client, calls = _capture_with_url(AutomatedCuration, monkeypatch) + + await client._async_get_engine_actions_by_name("Erin") + + assert calls[0]["body"]["metadataElementTypeName"] == "EngineAction"