From 6ae9228696c6f7b97f33d6844d134accb6bc3687 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 17:42:14 +0100 Subject: [PATCH 1/8] fix(pyegeria): reapply 3 fixes lost to worktree cleanup -- template body, arity, classification helper These three fixes were made and verified in an isolated Agent worktree (egeria-python-omvs-audit-followup) that the harness auto-cleaned once its commits were pushed and PR #277 merged. Because these three fixes were still uncommitted at that point, they were lost with the directory rather than merged. Reapplied here from scratch on top of PR #277's tip (confirmed absent via egeria-python-73, a peer session, before redoing). 1. governance_officer.get_governance_action_process_graph sent GetRequestBody where Egeria-api-governance-officer.http documents ResultsRequestBody. Added an opt-in body_model param to _async_get_guid_request (default unchanged: GetRequestBody) so this one call can send the documented body without touching the other 24 callers of that helper. Kept on _async_get_guid_request rather than switching to _async_get_results_body_request, because only this helper reads the singular "elementGraph" response key the graph endpoint returns. 2. Six *_from_template methods (actor_manager x3, data_discovery, subject_area, time_keeper) called _async_create_element_body_request instead of _async_create_element_from_template -- silently dropping the 8 fields only TemplateRequestBody has (template_guid, placeholder_property_values, replacement_properties, deep_copy, ...) via PyegeriaModel's extra='ignore'. The element was created with no template ever applied, no error raised. Also fixed 4 call sites (actor_manager x3, location_arena) passing an extra "POST" positional argument to _async_create_element_from_template, which only takes (url, body) -- confirmed this raises TypeError before any request is sent. 3. classification_explorer's clear_known_duplicate_classification and clear_consolidated_duplicate_classification annotated DeleteClassificationRequestBody but called _async_delete_relationship_request, whose validator only accepts DeleteRelationshipRequestBody or dict -- passing the annotated type hits the validator's else branch, which returns None, so the guarded call is skipped and the clear silently never happens. Switched both to the dedicated _async_delete_classification_request helper (the other 12 clear_* methods in this file already used it correctly). Verified: full pyegeria/ compiles, all touched modules import, zero _async_create_element_from_template call sites now exceed its 2-param arity, micro-tests pass, functional tests collect. Audit: 612 OK / 47 mismatch (was 605/54 on this base) -- governance-officer and the six from-template methods now report 0 mismatches; classification-explorer down to the 6 unrelated findings that predate this pass. Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 24 +++--------------------- pyegeria/core/_server_client.py | 23 ++++++++++++++++++----- pyegeria/omvs/actor_manager.py | 12 ++++++------ pyegeria/omvs/classification_explorer.py | 4 ++-- pyegeria/omvs/data_discovery.py | 2 +- pyegeria/omvs/governance_officer.py | 8 ++++++-- pyegeria/omvs/location_arena.py | 2 +- pyegeria/omvs/subject_area.py | 2 +- pyegeria/omvs/time_keeper.py | 2 +- 9 files changed, 39 insertions(+), 40 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 5505a8ff..47d2ecbf 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 603 | -| Mismatch (verb/path/body) | 56 | +| OK | 612 | +| Mismatch (verb/path/body) | 47 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -139,12 +139,6 @@ _Review only - cross-service overlap is often intentional._ SDK: /actor-manager/user-identities/{}/security-group-membership/reclassify API: /actor-manager/user-identities/{}/security-group-memberships/reclassify - removeAllSecurityGroupMembership: MISSING (`POST /actor-manager/user-identities/{}/security-group-memberships/declassify`) -- createContactDetailsFromTemplate: MISMATCH `create_contact_details_from_template` - - BODY sends NewElementRequestBody != TemplateRequestBody -- createPerspectiveFromTemplate: MISMATCH `create_perspective_from_template` - - BODY sends NewElementRequestBody != TemplateRequestBody -- createSkillFromTemplate: MISMATCH `create_skill_from_template` - - BODY sends NewElementRequestBody != TemplateRequestBody ### Service: asset-catalog @@ -194,11 +188,7 @@ _Review only - cross-service overlap is often intentional._ - addOwnership: MISSING (`POST /classification-explorer/elements/{}/ownership`) - clearOwnership: MISSING (`POST /classification-explorer/elements/{}/ownership/remove`) - clearDigitalResourceOrigin: MISSING (`POST /classification-explorer/elements/{}/digital-resource-origin/remove`) -- clearKnownDuplicateClassification: MISMATCH `clear_known_duplicate_classification` - - BODY sends DeleteRelationshipRequestBody != DeleteClassificationRequestBody - setupPeerDuplicates: MISSING (`POST /classification-explorer/related-elements/{}/peer-duplicate/{}/attach`) -- clearConsolidatedDuplicateClassification: MISMATCH `clear_consolidated_duplicate_classification` - - BODY sends DeleteRelationshipRequestBody != DeleteClassificationRequestBody - clearSemanticAssignment: MISSING (`POST /classification-explorer/elements/{}/semantic-assignment/terms/{}/detach`) - getSemanticAssignees: MISMATCH `get_semantic_assignees` - PATH @@ -306,8 +296,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: data-discovery -- createAnnotationFromTemplate: MISMATCH `create_annotation_from_template` - - BODY sends NewElementRequestBody != TemplateRequestBody ### Service: data-engineer @@ -397,8 +385,6 @@ _Review only - cross-service overlap is often intentional._ - findGovernanceActionProcesses: MISSING (`POST /governance-officer/governance-action-processes/by-search-string`) - getAllGovernanceActionProcesses: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) - getGovernanceActionProcessesByName: MISSING (`POST /governance-officer/governance-definitions/by-name`) -- getGovernanceActionProcessGraph: MISMATCH `get_governance_action_process_graph` - - BODY sends GetRequestBody != ResultsRequestBody - addGovernanceDefinitionToElement: MISSING (`POST /governance-officer/elements/{}/governed-by/definition/{}/attach`) - removeGovernanceDefinitionFromElement: MISSING (`POST /governance-officer/elements/{}/governed-by/definition/{}/detach`) - linkApprovedPurpose: MISSING (`POST /governance-officer/elements/{}/approved-purposes/{}/attach`) @@ -448,7 +434,7 @@ _Review only - cross-service overlap is often intentional._ API: /metadata-expert/metadata-elements/guid-by-unique-name - BODY sends FilterRequestBody != UniqueNameRequestBody - getMetadataElementHistory: MISMATCH `get_metadata_element_history` - - BODY sends GetRequestBody != HistoryRequestBody + - BODY sends GetRequestBody|ResultsRequestBody != HistoryRequestBody - getClassificationHistory: MISMATCH `get_classification_history` - PATH SDK: /{}/metadata-elements/{}/classifications/{}/history @@ -671,8 +657,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: subject-area -- createSubjectAreaFromTemplate: MISMATCH `create_subject_area_from_template` - - BODY sends NewElementRequestBody != TemplateRequestBody - linkSubjectAreas: MISSING (`POST /subject-area/collections/{}/collection-hierarchies/{}/attach`) - detachSubjectAreas: MISSING (`POST /subject-area/collections/{}/collection-hierarchies/{}/detach`) - findAllSubjectAreas: MISSING (`POST /subject-area/collectionss/by-search-string`) @@ -686,8 +670,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: time-keeper -- createContextEventFromTemplate: MISMATCH `create_context_event_from_template` - - BODY sends NewElementRequestBody != TemplateRequestBody ### Service: valid-metadata diff --git a/pyegeria/core/_server_client.py b/pyegeria/core/_server_client.py index 8501e96f..e54af3e4 100644 --- a/pyegeria/core/_server_client.py +++ b/pyegeria/core/_server_client.py @@ -6522,17 +6522,30 @@ async def _async_get_guid_request(self, url: str, _type: str, _gen_output: Calla skip_relationships: list[str] | None = None, graph_query_depth: int = 3, output_format: str = 'JSON', report_spec: Optional[str | dict] = None, - body: Optional[dict | GetRequestBody] = None, max_mermaid_node_count=10, + body: Optional[dict | GetRequestBody | ResultsRequestBody] = None, + max_mermaid_node_count=10, + body_model: type[GetRequestBody | ResultsRequestBody] = GetRequestBody, **kwargs) -> Any: + """Retrieve an element by GUID. - if isinstance(body, GetRequestBody): + `body_model` selects the request-body class to send. It defaults to + GetRequestBody, which is what nearly every endpoint reached through + this helper documents. A few endpoints document ResultsRequestBody + instead (e.g. governance-officer's .../graph); they pass it explicitly + rather than switching to _async_get_results_body_request, because only + this helper understands the singular "elementGraph" response key. + """ + if isinstance(body, (GetRequestBody, ResultsRequestBody)): validated_body = body elif isinstance(body, dict): - validated_body = self._validate_body(self._get_request_adapter.validate_python, body) + adapter = (self._get_request_adapter.validate_python + if body_model is GetRequestBody + else self._results_request_adapter.validate_python) + validated_body = self._validate_body(adapter, body) else: _type = _type.replace(" ", "") body = { - "class": "GetRequestBody", + "class": body_model.__name__, "metadataElementTypeName": _type, "includeOnlyRelationships": include_only_relationships, "skipRelationships": skip_relationships, @@ -6540,7 +6553,7 @@ async def _async_get_guid_request(self, url: str, _type: str, _gen_output: Calla "maxMermaidNodeCount": max_mermaid_node_count, **kwargs } - validated_body = self._validate_body(GetRequestBody.model_validate, body) + validated_body = self._validate_body(body_model.model_validate, body) json_body = validated_body.model_dump_json(indent=2, exclude_none=True) diff --git a/pyegeria/omvs/actor_manager.py b/pyegeria/omvs/actor_manager.py index 2212ac9b..e0f2ab35 100644 --- a/pyegeria/omvs/actor_manager.py +++ b/pyegeria/omvs/actor_manager.py @@ -251,7 +251,7 @@ async def _async_create_actor_profile_from_template(self, body: Optional[dict | # Handle Optional body parameter body_to_use = body if body is not None else {} - return await self._async_create_element_from_template("POST", url, body_to_use) + return await self._async_create_element_from_template(url, body_to_use) @dynamic_catch def create_actor_profile_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str: @@ -1430,7 +1430,7 @@ async def _async_create_actor_role_from_template(self, body: Optional[dict | Tem # Handle Optional body parameter body_to_use = body if body is not None else {} - return await self._async_create_element_from_template("POST", url, body_to_use) + return await self._async_create_element_from_template(url, body_to_use) @dynamic_catch def create_actor_role_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str: @@ -3002,7 +3002,7 @@ async def _async_create_user_identity_from_template(self, body: Optional[dict | """ url = f"{self.command_root}/user-identities/from-template" - return await self._async_create_element_from_template("POST", url, body) + return await self._async_create_element_from_template(url, body) @dynamic_catch def create_user_identity_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str: @@ -5544,7 +5544,7 @@ async def _async_create_contact_details_from_template(self, body: Optional[dict } """ url = f"{self.command_root}/contact-details/from-template" - return await self._async_create_element_body_request(url, ["ContactDetailsProperties"], body) + return await self._async_create_element_from_template(url, body) @dynamic_catch def create_contact_details_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str: @@ -6582,7 +6582,7 @@ async def _async_create_perspective_from_template(self, body: Optional[dict | Te """ url = f"{self.command_root}/perspectives/from-template" body_to_use = body if body is not None else {} - return await self._async_create_element_body_request(url, ["PerspectiveProperties"], body_to_use) + return await self._async_create_element_from_template(url, body_to_use) @dynamic_catch def create_perspective_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str: @@ -7388,7 +7388,7 @@ async def _async_create_skill_from_template(self, body: Optional[dict | Template """ url = f"{self.command_root}/skills/from-template" body_to_use = body if body is not None else {} - return await self._async_create_element_body_request(url, ["SkillProperties"], body_to_use) + return await self._async_create_element_from_template(url, body_to_use) @dynamic_catch def create_skill_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str: diff --git a/pyegeria/omvs/classification_explorer.py b/pyegeria/omvs/classification_explorer.py index a6015f11..0fc3c815 100644 --- a/pyegeria/omvs/classification_explorer.py +++ b/pyegeria/omvs/classification_explorer.py @@ -11108,7 +11108,7 @@ async def _async_clear_known_duplicate_classification( "effectiveTime": effective_time } - await self._async_delete_relationship_request(url, body) + await self._async_delete_classification_request(url, body) def clear_known_duplicate_classification( self, @@ -11591,7 +11591,7 @@ async def _async_clear_consolidated_duplicate_classification( "effectiveTime": effective_time } - await self._async_delete_relationship_request(url, body) + await self._async_delete_classification_request(url, body) def clear_consolidated_duplicate_classification( self, diff --git a/pyegeria/omvs/data_discovery.py b/pyegeria/omvs/data_discovery.py index 9ec8cf4d..186024f6 100644 --- a/pyegeria/omvs/data_discovery.py +++ b/pyegeria/omvs/data_discovery.py @@ -216,7 +216,7 @@ async def _async_create_annotation_from_template(self, body: dict | TemplateRequ ``` """ url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/from-template" - return await self._async_create_element_body_request(url, "AnnotationProperties", body) + return await self._async_create_element_from_template(url, body) def create_annotation_from_template(self, body: dict | TemplateRequestBody) -> str: """Create an annotation from a template. diff --git a/pyegeria/omvs/governance_officer.py b/pyegeria/omvs/governance_officer.py index fd785d8e..b619b1ee 100644 --- a/pyegeria/omvs/governance_officer.py +++ b/pyegeria/omvs/governance_officer.py @@ -30,7 +30,7 @@ ReferenceableProperties, TemplateRequestBody, UpdateElementRequestBody, NewRelationshipRequestBody, DeleteElementRequestBody, DeleteRelationshipRequestBody, - UpdateRelationshipRequestBody) + UpdateRelationshipRequestBody, ResultsRequestBody) from pyegeria.core.utils import dynamic_catch GOV_DEF_PROPERTIES_LIST = ["GovernanceDefinitionProperties", "GovernanceStrategyProperties", "RegulationProperties", @@ -2739,10 +2739,14 @@ async def _async_get_governance_action_process_graph(self, guid: str, element_ty f"{self.url_marker}/governance-action-processes/{guid}/graph") type = element_type if element_type else "GovernanceDefinition" + # This endpoint documents ResultsRequestBody, not the GetRequestBody + # this helper sends by default (Egeria-api-governance-officer.http). + # Kept on _async_get_guid_request because only it reads the singular + # "elementGraph" response key that the graph endpoint returns. response = await self._async_get_guid_request(url, _type=type, _gen_output=self._generate_governance_definition_output, output_format=output_format, report_spec=report_spec, - body=body, **kwargs) + body=body, body_model=ResultsRequestBody, **kwargs) return response diff --git a/pyegeria/omvs/location_arena.py b/pyegeria/omvs/location_arena.py index 36788423..54f9420f 100644 --- a/pyegeria/omvs/location_arena.py +++ b/pyegeria/omvs/location_arena.py @@ -201,7 +201,7 @@ async def _async_create_location_from_template(self, body: Optional[dict | Templ """ url = f"{self.ref_location_command_base}/locations/from-template" - return await self._async_create_element_from_template("POST", url, body) + return await self._async_create_element_from_template(url, body) @dynamic_catch def create_location_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str: diff --git a/pyegeria/omvs/subject_area.py b/pyegeria/omvs/subject_area.py index 09578e78..d7bf490f 100644 --- a/pyegeria/omvs/subject_area.py +++ b/pyegeria/omvs/subject_area.py @@ -219,7 +219,7 @@ async def _async_create_subject_area_from_template(self, body: dict | TemplateRe ``` """ url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/subject-area/collections/from-template" - return await self._async_create_element_body_request(url, "SubjectAreaProperties", body) + return await self._async_create_element_from_template(url, body) def create_subject_area_from_template(self, body: dict | TemplateRequestBody) -> str: """Create a subject area from a template. diff --git a/pyegeria/omvs/time_keeper.py b/pyegeria/omvs/time_keeper.py index c9a6f8f2..db3e08fb 100644 --- a/pyegeria/omvs/time_keeper.py +++ b/pyegeria/omvs/time_keeper.py @@ -259,7 +259,7 @@ async def _async_create_context_event_from_template(self, body: dict | TemplateR ``` """ url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/time-keeper/context-events/from-template" - return await self._async_create_element_body_request(url, "ContextEventProperties", body) + return await self._async_create_element_from_template(url, body) def create_context_event_from_template(self, body: dict | TemplateRequestBody) -> str: """Create a context event from a template. From d394ac9c94ecf2ab3f6230f4bea184fd8bdc2c05 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 17:58:50 +0100 Subject: [PATCH 2/8] fix(pyegeria): metadata-expert audit -- resolve base_path() helper, fix broken get_metadata_element_history metadata-expert reported 16 mismatches; 14 were a new variant of the tool artifact already fixed for connection-maker/governance-officer. Tooling (scripts/omvs_audit.py): - Resolve module-level helper functions that just return the service root. metadata_expert.py (and governance_officer.py, solution_architect.py) builds URLs as f"{base_path(self, self.view_server)}/..." -- a plain function call, not a self. reference, so neither the by-name nor by-value root detection matched it. Added resolve_module_root_funcs(): any top-level function whose single return statement contains "/open-metadata/" is registered by name, and _flatten's Call handling checks that registry before falling through to the URL-wrapper/opaque-expression cases. The function's own arguments are irrelevant (they just interpolate platform_url/view_server, already collapsed to placeholders), so this resolves by name, not by call shape. Endpoint fixes: - get_metadata_element_history routed through _async_get_guid_request, whose default validates against GetRequestBody -- a Literal['GetRequestBody'] discriminator on the "class" field. The method hand-builds a body with "class": "HistoryRequestBody" (matching Egeria-api-metadata-expert.http; no HistoryRequestBody model exists in pyegeria.models). Every call raised a pydantic ValidationError before any request was sent. Rewritten to send the raw dict via _async_make_request directly, mirroring _async_get_classification_history -- the sibling endpoint in this same file that already does this correctly. - get_metadata_guid_by_unique_name annotated body as dict | FilterRequestBody, but actually sends "class": "UniqueNameRequestBody" (matching ground truth; again no backing pyegeria.models class exists). A caller following the annotation and passing an actual FilterRequestBody object would crash in body_slimmer(), whose body.items() call assumes a dict. Corrected the annotation to Optional[dict] with a comment, rather than importing a class that doesn't exist. Audit now reports 628 OK / 31 mismatch (was 612 / 47). Not fixed here (missing, not mismatch -- different bucket, deferred): getAllRelatedMetadataElements and findRelationshipsBetweenMetadataElements have no SDK method at all. Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 70 +++----------------------------- pyegeria/omvs/metadata_expert.py | 29 ++++++++++--- scripts/omvs_audit.py | 32 ++++++++++++++- 3 files changed, 60 insertions(+), 71 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 47d2ecbf..73489c5c 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 612 | -| Mismatch (verb/path/body) | 47 | +| OK | 628 | +| Mismatch (verb/path/body) | 31 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -62,6 +62,9 @@ _Review only - cross-service overlap is often intentional._ - `action-author.py`: `_async_get_governance_action_process` - `governance-officer.py`: `_async_get_governance_definition_by_guid` - `governance-officer.py`: `_async_get_governance_action_process` +- `POST /metadata-expert/metadata-elements/{}/history` + - `metadata-expert.py`: `_async_get_element_history` + - `metadata-expert.py`: `_async_get_metadata_element_history` - `POST /metadata-expert/related-elements` - `collection-manager.py`: `_async_link_saved_query_to_results_set` - `metadata-expert.py`: `_async_create_related_elements` @@ -419,73 +422,10 @@ _Review only - cross-service overlap is often intentional._ - updateRelatedElementsInStore: MISSING (`POST /metadata-expert/related-elements/{}/update-properties`) - updateRelatedElementsEffectivityInStore: MISSING (`POST /metadata-expert/related-elements/{}/update-effectivity`) - deleteRelatedElementsInStore: MISSING (`POST /metadata-expert/related-elements/{}/delete`) -- getMetadataElementByGUID: MISMATCH `get_metadata_element_by_guid` - - PATH - SDK: /{}/metadata-elements/{} - API: /metadata-expert/metadata-elements/{} - getAnchoredElementsGraph: MISSING (`POST /metadata-expert/metadata-elements/{}/with-anchored-elements`) -- getMetadataElementByUniqueName: MISMATCH `get_metadata_element_by_unique_name` - - PATH - SDK: /{}/metadata-elements/by-unique-name - API: /metadata-expert/metadata-elements/by-unique-name -- getMetadataElementGUIDByUniqueName: MISMATCH `get_metadata_guid_by_unique_name` - - PATH - SDK: /{}/metadata-elements/guid-by-unique-name - API: /metadata-expert/metadata-elements/guid-by-unique-name - - BODY sends FilterRequestBody != UniqueNameRequestBody -- getMetadataElementHistory: MISMATCH `get_metadata_element_history` - - BODY sends GetRequestBody|ResultsRequestBody != HistoryRequestBody -- getClassificationHistory: MISMATCH `get_classification_history` - - PATH - SDK: /{}/metadata-elements/{}/classifications/{}/history - API: /metadata-expert/metadata-elements/{}/classifications/{}/history -- findMetadataElementsWithString: MISMATCH `find_metadata_elements_with_string` - - PATH - SDK: /{}/metadata-elements/by-search-string - API: /metadata-expert/metadata-elements/by-search-string -- findElementsForAnchor: MISMATCH `find_elements_for_anchor` - - PATH - SDK: /{}/metadata-elements/by-search-string/for-anchor/{} - API: /metadata-expert/metadata-elements/by-search-string/for-anchor/{} -- findElementsInAnchorDomain: MISMATCH `find_elements_in_anchor_domain` - - PATH - SDK: /{}/metadata-elements/by-search-string/in-anchor-domain/{} - API: /metadata-expert/metadata-elements/by-search-string/in-anchor-domain/{} -- findElementsInAnchorScope: MISMATCH `find_elements_in_anchor_scope` - - PATH - SDK: /{}/metadata-elements/by-search-string/in-anchor-scope/{} - API: /metadata-expert/metadata-elements/by-search-string/in-anchor-scope/{} - getAllRelatedMetadataElements: MISSING (`POST /metadata-expert/related-elements/{}/any-type`) -- getRelatedMetadataElements: MISMATCH `get_related_metadata_elements` - - PATH - SDK: /{}/related-elements/{}/type/{} - API: /metadata-expert/related-elements/{}/type/{} -- getAllMetadataElementRelationships: MISMATCH `get_all_metadata_element_relationships` - - PATH - SDK: /{}/metadata-elements/{}/linked-by-any-type/to-elements/{} - API: /metadata-expert/metadata-elements/{}/linked-by-any-type/to-elements/{} -- getMetadataElementRelationships: MISMATCH `get_metadata_element_relationships` - - PATH - SDK: /{}/metadata-elements/{}/linked-by-type/{}/to-elements/{} - API: /metadata-expert/metadata-elements/{}/linked-by-type/{}/to-elements/{} -- findMetadataElements: MISMATCH `find_metadata_elements` - - PATH - SDK: /{}/metadata-elements/by-search-conditions - API: /metadata-expert/metadata-elements/by-search-conditions -- countMetadataElements: MISMATCH `count_metadata_elements` - - PATH - SDK: /{}/metadata-elements/by-search-conditions/count - API: /metadata-expert/metadata-elements/by-search-conditions/count - findRelationshipsBetweenMetadataElements: MISSING (`POST /metadata-expert/relationships/by-search-conditions`) - countRelationshipsBetweenMetadataElements: MISSING (`POST /metadata-expert/relationships/by-search-conditions/count`) -- getRelationshipByGUID: MISMATCH `get_relationship_by_guid` - - PATH - SDK: /{}/relationships/by-guid/{} - API: /metadata-expert/relationships/by-guid/{} -- getRelationshipHistory: MISMATCH `get_relationship_history` - - PATH - SDK: /{}/relationships/{}/history - API: /metadata-expert/relationships/{}/history ### Service: my-profile diff --git a/pyegeria/omvs/metadata_expert.py b/pyegeria/omvs/metadata_expert.py index 4a3fe662..53cb5197 100644 --- a/pyegeria/omvs/metadata_expert.py +++ b/pyegeria/omvs/metadata_expert.py @@ -891,7 +891,7 @@ async def _async_get_metadata_guid_by_unique_name( name: Optional[str] = None, property_name: str = "qualifiedName", as_of_time: Optional[str] = None, - body: Optional[dict | FilterRequestBody] = None, + body: Optional[dict] = None, # sends "class": "UniqueNameRequestBody" (no pyegeria.models class backs this yet) **kwargs ) -> str: """ @@ -964,7 +964,7 @@ def get_metadata_guid_by_unique_name( name: Optional[str] = None, property_name: str = "qualifiedName", as_of_time: Optional[str] = None, - body: Optional[dict | FilterRequestBody] = None, + body: Optional[dict] = None, # sends "class": "UniqueNameRequestBody" (no pyegeria.models class backs this yet) **kwargs ) -> str: """ @@ -1555,7 +1555,18 @@ async def _async_get_metadata_element_history( body: Optional[dict] = None, **kwargs, ) -> list | str: - """Retrieve the history of a metadata element. Async version.""" + """Retrieve the history of a metadata element. Async version. + + Notes + ----- + Sends a raw "HistoryRequestBody" dict via _async_make_request rather + than through _async_get_guid_request: that helper validates against + GetRequestBody, whose "class" field is a Literal["GetRequestBody"] -- + passing "HistoryRequestBody" through it raised a pydantic + ValidationError on every call (no HistoryRequestBody model exists in + pyegeria.models yet). Mirrors _async_get_classification_history, the + sibling endpoint in this file that already does this correctly. + """ if body is None: body = { "class": "HistoryRequestBody", @@ -1563,10 +1574,18 @@ async def _async_get_metadata_element_history( "oldestFirst": oldest_first, } url = f"{self.command_root}/metadata-elements/{metadata_element_guid}/history" - return await self._async_get_guid_request( - url, _type="MetadataElement", _gen_output=self._generate_referenceable_output, body=body, **kwargs + + response: Response = await self._async_make_request( + "POST", url, body_slimmer(body), ) + elements = response.json().get("elements", NO_ELEMENTS_FOUND) + if type(elements) is str: + logger.info(NO_ELEMENTS_FOUND) + return NO_ELEMENTS_FOUND + + return elements + @dynamic_catch def get_metadata_element_history( self, diff --git a/scripts/omvs_audit.py b/scripts/omvs_audit.py index 3115a37c..c3825951 100644 --- a/scripts/omvs_audit.py +++ b/scripts/omvs_audit.py @@ -301,10 +301,16 @@ def _flatten(node: ast.AST, roots: dict[str, str]) -> str: if node.attr in roots: return roots[node.attr] return "{%s}" % node.attr - # Unwrap URL-normalising wrappers, e.g. str(HttpUrl(f"...")). if isinstance(node, ast.Call): fname = (node.func.id if isinstance(node.func, ast.Name) else getattr(node.func, "attr", None)) + # A module-level helper that just returns the service root, e.g. + # url = f"{base_path(self, self.view_server)}/metadata-elements/{guid}". + # Its args (self, view_server) are irrelevant here - both already + # collapse to "" / "{}" - so look it up by name, not by call shape. + if fname in roots: + return roots[fname] + # Unwrap URL-normalising wrappers, e.g. str(HttpUrl(f"...")). if fname in URL_WRAPPERS and node.args: return _flatten(node.args[0], roots) # e.g. name.lower() - name the receiver so lint can classify it @@ -350,6 +356,29 @@ def resolve_roots(tree: ast.AST) -> dict[str, str]: return roots +def resolve_module_root_funcs(tree: ast.Module) -> dict[str, str]: + """Resolve module-level helper functions that just return the service root. + + Several modules (metadata_expert, governance_officer, solution_architect) + define ``def base_path(client, view_server): return f"{client.platform_url} + /servers/{view_server}/api/open-metadata/"`` and call it as + ``f"{base_path(self, self.view_server)}/..."``. Its own args are irrelevant + - only the literal path segments in its return value matter - so this + resolves by function name for lookup in ``_flatten``'s Call handling. + """ + funcs: dict[str, str] = {} + for node in tree.body: + if not isinstance(node, ast.FunctionDef): + continue + returns = [n for n in ast.walk(node) if isinstance(n, ast.Return) and n.value is not None] + if len(returns) != 1: + continue + value = _flatten(returns[0].value, {}) + if "/open-metadata/" in value: + funcs[node.name] = value + return funcs + + def parse_py_file(filepath: str, helper_verbs: dict[str, str], helper_bodies: dict[str, str] | None = None) -> dict[str, PyMethod]: content = open(filepath, encoding="utf-8").read() @@ -360,6 +389,7 @@ def parse_py_file(filepath: str, helper_verbs: dict[str, str], return {} roots = resolve_roots(tree) + roots.update(resolve_module_root_funcs(tree)) methods: dict[str, PyMethod] = {} sync_names: list[str] = [] From 6b92d37d9d78be416735bb7a21f3359544886914 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 18:02:44 +0100 Subject: [PATCH 3/8] fix(pyegeria): actor-manager audit -- security-group-membership plural, detach helper direction actor-manager reported 7 mismatches; 4 were real, fixed; 2 are a .http ground-truth typo (see below); 1 is a stray .http parse artifact. Fixes: - add_security_group_membership / update_security_group_membership built their URL as .../security-group-membership/classify|reclassify (singular). Egeria-api-actor-manager.http documents .../security-group-memberships/ (plural) consistently across classify, reclassify, and declassify; remove_all_security_group_memberships in this same file already used the plural form correctly. Also dropped a stray "url = url = (...)" double assignment in add_security_group_membership while touching that line. - detach_asset_from_profile / detach_person_role_from_profile annotated their body as DeleteRelationshipRequestBody (correct, matches ground truth) but called _async_delete_element_request -- the inverse of the bug class fixed earlier in governance_officer/classification_explorer. Its validator treats a DeleteRelationshipRequestBody instance as "not provided" (isinstance check against DeleteElementRequestBody fails, falls to the else branch), silently discarding the caller's relationship-detach properties and sending a default DeleteElementRequestBody body instead. Switched both to _async_delete_relationship_request to match the annotation and the ground truth's /detach path shape. Not changed -- ground-truth data-quality issues, not SDK bugs: - updateActorRole / deleteActorRole: Egeria-api-actor-manager.http's URLs for these two are missing the {{actorRoleGUID}} path segment entirely (.../actor-roles/update, .../actor-roles/delete) while every sibling type in the same file (actor-profiles, user-identities) correctly uses .../{guid}/update and .../{guid}/delete. The SDK's {guid}/update / {guid}/delete shape is the one consistent with the rest of the API and is presumed correct; changing it to match would introduce a real bug to satisfy a typo in the ground truth. - getActorRoleByGUID: the .http line for this endpoint ends in a stray `"})` copy/paste artifact (`.../retrieve"})`), which canon_path's cleanup can't fully strip without risking corruption of legitimately parameter-terminated paths elsewhere. Cosmetic parser noise, not a real finding. Audit now reports 631 OK / 27 mismatch (was 628 / 31). Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 16 ++-------------- pyegeria/omvs/actor_manager.py | 8 ++++---- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 73489c5c..4f7a0029 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 628 | -| Mismatch (verb/path/body) | 31 | +| OK | 632 | +| Mismatch (verb/path/body) | 27 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -111,15 +111,11 @@ _Review only - cross-service overlap is often intentional._ ### Service: actor-manager -- detachAssetFromProfile: MISMATCH `detach_asset_from_profile` - - BODY sends DeleteElementRequestBody != DeleteRelationshipRequestBody - find All ContributionRecords: MISSING (`POST /actor-manager/contribution-records/by-search-string`) - updateActorRole: MISMATCH `update_actor_role` - PATH SDK: /actor-manager/actor-roles/{}/update API: /actor-manager/actor-roles/update -- detachPersonRoleFromProfile: MISMATCH `detach_person_role_from_profile` - - BODY sends DeleteElementRequestBody != DeleteRelationshipRequestBody - Detach a team role from a team profile.: MISSING (`POST /actor-manager/actor-roles/{}/team-role-appointments/{}/detach`) - linkITProfileRoleToProfile: MISSING (`POST /actor-manager/actor-roles/{}/it-profile-role-appointments/{}/attach`) - detachITProfileRoleFromProfile: MISSING (`POST /actor-manager/actor-roles/{}/it-profile-role-appointments/{}/detach`) @@ -133,14 +129,6 @@ _Review only - cross-service overlap is often intentional._ SDK: /actor-manager/actor-roles/{}/retrieve API: /actor-manager/actor-roles/{}/retrieve"} - detachProfileIdentity: MISSING (`POST /actor-manager/user-identities/{}/profile-identity/{}/detach`) -- addSecurityGroupMembership: MISMATCH `add_security_group_membership` - - PATH - SDK: /actor-manager/user-identities/{}/security-group-membership/classify - API: /actor-manager/user-identities/{}/security-group-memberships/classify -- updateSecurityGroupMembership: MISMATCH `update_security_group_membership` - - PATH - SDK: /actor-manager/user-identities/{}/security-group-membership/reclassify - API: /actor-manager/user-identities/{}/security-group-memberships/reclassify - removeAllSecurityGroupMembership: MISSING (`POST /actor-manager/user-identities/{}/security-group-memberships/declassify`) ### Service: asset-catalog diff --git a/pyegeria/omvs/actor_manager.py b/pyegeria/omvs/actor_manager.py index e0f2ab35..f198c53c 100644 --- a/pyegeria/omvs/actor_manager.py +++ b/pyegeria/omvs/actor_manager.py @@ -580,7 +580,7 @@ async def _async_detach_asset_from_profile(self, asset_guid: str, it_profile_gui """ url = (f"{self.command_root}/assets/{asset_guid}/it-profiles/{it_profile_guid}/detach") - await self._async_delete_element_request(url, body) + await self._async_delete_relationship_request(url, body) logger.debug(f"Detached asset {asset_guid} from it profile {it_profile_guid}") def detach_asset_from_profile(self, asset_guid: str, it_profile_guid: str, @@ -1754,7 +1754,7 @@ async def _async_detach_person_role_from_profile(self, person_role_guid: str, pe url = ( f"{self.command_root}/actor-roles/{person_role_guid}/person-role-appointments/{person_profile_guid}/detach") - await self._async_delete_element_request(url, body) + await self._async_delete_relationship_request(url, body) logger.debug(f"Detached Person Rolet {person_role_guid} from Person Profile {person_profile_guid}") def detach_person_role_from_profile(self, person_role_guid: str, person_profile_guid: str, @@ -3613,7 +3613,7 @@ async def _async_add_security_group_membership(self, user_identity_guid: str, se """ - url = url = (f"{self.command_root}/user-identities/{user_identity_guid}/security-group-membership/classify") + url = (f"{self.command_root}/user-identities/{user_identity_guid}/security-group-memberships/classify") await self._async_new_classification_request(url, ["SecurityGroupMembershipProperties"], body) logger.debug(f"Classifying User Identity {user_identity_guid} with Security Groups {security_groups}") @@ -3712,7 +3712,7 @@ async def _async_update_security_group_membership(self, user_identity_guid: str, "forDuplicateProcessing" : false } """ - url = (f"{self.command_root}/user-identities/{user_identity_guid}/security-group-membership/reclassify") + url = (f"{self.command_root}/user-identities/{user_identity_guid}/security-group-memberships/reclassify") await self._async_make_request("POST", url, body) logger.debug(f"Updated security classifications for {user_identity_guid}") From 3b78bcf3ef1978f0ef234ebb2b62a768a56f69b0 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 18:04:45 +0100 Subject: [PATCH 4/8] fix(pyegeria): classification-explorer -- drop spurious /glossaries/ path segment Six methods (get_semantic_assignees, get_source_elements, get_elements_sourced_from, get_licensed_elements, get_licenses, get_certified_elements) built URLs as .../classification-explorer/glossaries/elements/... . Every one of these operates on a generic Referenceable element (source/license/certification relationships), not a glossary term, and Egeria-api-classification-explorer.http confirms none of them include "glossaries" in the path -- only get_meanings (glossaries/terms/...) actually does, and was correctly left untouched. Audit now reports 0 mismatches for classification-explorer (was 6). Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 28 ++---------------------- pyegeria/omvs/classification_explorer.py | 12 +++++----- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 4f7a0029..5312fdcc 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 632 | -| Mismatch (verb/path/body) | 27 | +| OK | 638 | +| Mismatch (verb/path/body) | 21 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -181,10 +181,6 @@ _Review only - cross-service overlap is often intentional._ - clearDigitalResourceOrigin: MISSING (`POST /classification-explorer/elements/{}/digital-resource-origin/remove`) - setupPeerDuplicates: MISSING (`POST /classification-explorer/related-elements/{}/peer-duplicate/{}/attach`) - clearSemanticAssignment: MISSING (`POST /classification-explorer/elements/{}/semantic-assignment/terms/{}/detach`) -- getSemanticAssignees: MISMATCH `get_semantic_assignees` - - PATH - SDK: /classification-explorer/glossaries/elements/by-semantic-assignment/{} - API: /classification-explorer/elements/by-semantic-assignment/{} - addGovernanceDefinitionToElement: MISSING (`POST /classification-explorer/elements/{}/governed-by/definition/{}/attach`) - removeGovernanceDefinitionFromElement: MISSING (`POST /classification-explorer/elements/{}/governed-by/definition/{}/detach`) - addGovernanceExpectations: MISSING (`POST /classification-explorer/elements/{}/governance-expectations`) @@ -194,29 +190,9 @@ _Review only - cross-service overlap is often intentional._ - removeResourceListFromElement: MISSING (`POST /classification-explorer/elements/{}/resource-list/{}/detach`) - addMoreInformationToElement: MISSING (`POST /classification-explorer/elements/{}/more-information/{}/attach`) - removeMoreInformationFromElement: MISSING (`POST /classification-explorer/elements/{}/more-information/{}/detach`) -- getSourceElements: MISMATCH `get_source_elements` - - PATH - SDK: /classification-explorer/glossaries/elements/{}/source - API: /classification-explorer/elements/{}/source -- getElementsSourcedFrom: MISMATCH `get_elements_sourced_from` - - PATH - SDK: /classification-explorer/glossaries/elements/{}/sourced-from - API: /classification-explorer/elements/{}/sourced-from - removeScopeFromElement: MISSING (`POST /classification-explorer/elements/{}/scoped-by/{}/detach`) - licenseElement: ELSEWHERE -> `governance-officer.py` -- getLicensedElements: MISMATCH `get_licensed_elements` - - PATH - SDK: /classification-explorer/glossaries/elements/licenses/{} - API: /classification-explorer/elements/licenses/{} -- getLicenses: MISMATCH `get_licenses` - - PATH - SDK: /classification-explorer/glossaries/elements/{}/licenses - API: /classification-explorer/elements/{}/licenses - certifyElement: ELSEWHERE -> `governance-officer.py` -- getCertifiedElements: MISMATCH `get_certified_elements` - - PATH - SDK: /classification-explorer/glossaries/elements/certifications/{} - API: /classification-explorer/elements/certifications/{} - getSearchKeywordByGUID: MISSING (`POST /classification-explorer/search-keywords/{}/retrieve`) - getSearchKeywordsByKeyword: MISSING (`POST /classification-explorer/search-keywords/by-keyword`) - findSearchKeywords: MISSING (`POST /classification-explorer/search-keywords/by-search-string`) diff --git a/pyegeria/omvs/classification_explorer.py b/pyegeria/omvs/classification_explorer.py index 0fc3c815..ebc37fda 100644 --- a/pyegeria/omvs/classification_explorer.py +++ b/pyegeria/omvs/classification_explorer.py @@ -961,7 +961,7 @@ async def _async_get_semantic_assignees( """ - url = (f"{self.classification_command_root}/glossaries/elements/by-semantic-assignment/{term_guid}") + url = (f"{self.classification_command_root}/elements/by-semantic-assignment/{term_guid}") response = await self._async_make_request("POST", url, body_slimmer(body), timeout=default_timeout, **kwargs) elements = response.json().get("elements", None) @@ -1362,7 +1362,7 @@ async def _async_get_source_elements( """ - url = (f"{self.classification_command_root}/glossaries/elements/{element_guid}/source") + url = (f"{self.classification_command_root}/elements/{element_guid}/source") response = await self._async_get_results_body_request(url, "Referenceable", self._generate_referenceable_output, start_from=start_from, page_size=page_size, @@ -1496,7 +1496,7 @@ async def _async_get_elements_sourced_from( """ - url = (f"{self.classification_command_root}/glossaries/elements/{element_guid}/sourced-from") + url = (f"{self.classification_command_root}/elements/{element_guid}/sourced-from") response = await self._async_get_results_body_request(url, "Referenceable", self._generate_referenceable_output, start_from=start_from, page_size=page_size, @@ -1888,7 +1888,7 @@ async def _async_get_licensed_elements( """ - url = (f"{self.classification_command_root}/glossaries/elements/licenses/{license_type_guid}") + url = (f"{self.classification_command_root}/elements/licenses/{license_type_guid}") response = await self._async_get_results_body_request(url, "Referenceable", self._generate_referenceable_output, start_from=start_from, page_size=page_size, @@ -2017,7 +2017,7 @@ async def _async_get_licenses( """ - url = (f"{self.classification_command_root}/glossaries/elements/{element_guid}/licenses") + url = (f"{self.classification_command_root}/elements/{element_guid}/licenses") response = await self._async_get_results_body_request(url, "Referenceable", self._generate_referenceable_output, start_from=start_from, page_size=page_size, @@ -2146,7 +2146,7 @@ async def _async_get_certified_elements( """ - url = (f"{self.classification_command_root}/glossaries/elements/certifications/{certification_type_guid}") + url = (f"{self.classification_command_root}/elements/certifications/{certification_type_guid}") response = await self._async_get_results_body_request(url, "Referenceable", self._generate_referenceable_output, start_from=start_from, page_size=page_size, From db5f67c72052611469016d374d034bdbe7e1127e Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 18:10:37 +0100 Subject: [PATCH 5/8] fix(pyegeria): solution-architect audit -- drop stray by-name path suffix, resolve query-string helper solution-architect reported 5 mismatches: 1 real duplicate-name bug, 3 tool artifacts from a second variant of the base_path()-style false positive, 1 left flagged rather than force-fixed. Tooling (scripts/omvs_audit.py): - _flatten's ast.Name branch never consulted `roots` at all -- only the Attribute and Call branches did. A bare local variable referenced in an f-string (`f"{url}{possible_query_params}"`) always rendered as an opaque placeholder no matter what it held. Now checks `roots` first. - Added QUERY_STRING_FUNCS + per-function local-variable resolution: methods assign a `query_string(...)`-built value to a local (e.g. `possible_query_params = query_string([("startFrom", ...), ...])`) before interpolating it into the URL. query_string() always returns "" or a "?key=value&..." suffix (solution_architect.py / collection_manager.py), safe to elide entirely since canon_path already splits on "?". Without this, every by-name/paginated endpoint using this pattern reported a false PATH mismatch (get_solution_roles_by_name, get_solution_components_by_name, get_solution_component_implementations all cleared by this alone). Endpoint fix: - get_design_patterns_by_name appended the search name directly onto the URL path (.../design-patterns/by-name/{name}) *and* sent it again in the request body via _async_get_name_request's FilterRequestBody -- the name was effectively sent twice, and the URL didn't match Egeria-api-solution-architect.http's undecorated .../by-name path. Removed the URL segment; the body-carried filter is Egeria's actual query mechanism for this endpoint (confirmed against ground truth). Left flagged, not changed: - detach_solution_linking_wire(component1_guid, component2_guid) builds .../wired-to/{}/detach, a path Egeria-api-solution-architect.http does not document at all -- only .../wires/{relationshipGUID}/detach exists, which detach_solution_linking_wire_by_guid (a sibling method, already correct) already implements. The two-component-GUID method has no relationship GUID to build the documented URL from, so this isn't a mechanical path fix; flagging for a follow-up decision (deprecate in favor of the _by_guid sibling, or confirm live whether the two-GUID form has a real backing endpoint under another path). Audit now reports 642 OK / 17 mismatch (was 638 / 21). Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 20 ++------------------ pyegeria/omvs/solution_architect.py | 2 +- scripts/omvs_audit.py | 27 ++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 5312fdcc..b2be5b35 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 638 | -| Mismatch (verb/path/body) | 21 | +| OK | 642 | +| Mismatch (verb/path/body) | 17 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -510,10 +510,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: solution-architect -- getDesignPatternsByName: MISMATCH `get_design_patterns_by_name` - - PATH - SDK: /solution-architect/design-patterns/by-name/{} - API: /solution-architect/design-patterns/by-name - createInformationSupplyChain: MISSING (`POST /solution-architect/information-supply-chains`) - createInformationSupplyChainFromTemplate: MISSING (`POST /solution-architect/information-supply-chains/from-template`) - updateInformationSupplyChain: MISSING (`POST /solution-architect/information-supply-chains/{}/update`) @@ -534,10 +530,6 @@ _Review only - cross-service overlap is often intentional._ - detachSolutionComponentActor: MISSING (`POST /solution-architect/solution-roles/{}/solution-component-actors/{}/detach`) - findAllSolutionRoles - with full request body: MISSING (`POST /solution-architect/solution-roles/by-search-string`) - findSolutionRoles - with full request body: MISSING (`POST /solution-architect/solution-roles/by-search-string`) -- getSolutionRolesByName: MISMATCH `get_solution_roles_by_name` - - PATH - SDK: /solution-architect/solution-roles/by-name{} - API: /solution-architect/solution-roles/by-name - getSolutionRolesByName - with full request body: MISSING (`POST /solution-architect/solution-roles/by-name`) - getSolutionRoleByGUID - with request body: MISSING (`POST /solution-architect/solution-roles/{}/retrieve`) - detachSubcomponent: MISSING (`POST /solution-architect/solution-components/{}/subcomponents/{}/detach`) @@ -548,16 +540,8 @@ _Review only - cross-service overlap is often intentional._ API: /solution-architect/solution-components/wires/{}/detach - findAllSolutionComponents - with full request body: MISSING (`POST /solution-architect/solution-components/by-search-string`) - findSolutionComponents - with full request body: MISSING (`POST /solution-architect/solution-components/by-search-string`) -- getSolutionComponentsByName: MISMATCH `get_solution_components_by_name` - - PATH - SDK: /solution-architect/solution-components/by-name{} - API: /solution-architect/solution-components/by-name - getSolutionComponentsByName - with full request body: MISSING (`POST /solution-architect/solution-components/by-name`) - getSolutionComponentByGUID - with request body: MISSING (`POST /solution-architect/solution-components/{}/retrieve`) -- getSolutionComponentImplementations: MISMATCH `get_solution_component_implementations` - - PATH - SDK: /solution-architect/solution-components/{}/implementations{} - API: /solution-architect/solution-components/{}/implementations ### Service: subject-area diff --git a/pyegeria/omvs/solution_architect.py b/pyegeria/omvs/solution_architect.py index dcba4066..bba65a5c 100644 --- a/pyegeria/omvs/solution_architect.py +++ b/pyegeria/omvs/solution_architect.py @@ -271,7 +271,7 @@ async def _async_get_design_patterns_by_name(self, name: Optional[str] = None, b graph_query_depth: int = 3, output_format: str = "JSON", report_spec: str | dict = "Design-Pattern-DrE", **kwargs): if name is None and "filter_string" in kwargs: name = kwargs.pop("filter_string") - url = f"{self.solution_architect_command_root}/design-patterns/by-name/{name}" + url = f"{self.solution_architect_command_root}/design-patterns/by-name" params = { 'graph_query_depth': graph_query_depth, 'filter_string': name, diff --git a/scripts/omvs_audit.py b/scripts/omvs_audit.py index c3825951..efb3a92f 100644 --- a/scripts/omvs_audit.py +++ b/scripts/omvs_audit.py @@ -99,6 +99,14 @@ # Transparent wrappers around a URL expression, e.g. str(HttpUrl(f"...")). URL_WRAPPERS = {"str", "HttpUrl", "AnyUrl", "quote", "urljoin"} +# Helpers that build a trailing "?key=value&..." query string (or "" when no +# params are set) - safe to elide entirely for path comparison, since +# canon_path already splits on "?". Appearing as f"{root}/foo{query_string(...)}" +# with no resolvable value otherwise renders as an opaque "{expr}" glued +# straight onto the path, producing false PATH mismatches +# (solution_architect.py / collection_manager.py's query_string() helper). +QUERY_STRING_FUNCS = {"query_string"} + # Internal helpers that resolve a name/GUID before the real call. They issue # their own request, so they must not be mistaken for the method's own verb. LOOKUP_HELPER_RE = re.compile(r"get_guid__|__async_get_guid") @@ -293,6 +301,8 @@ def _flatten(node: ast.AST, roots: dict[str, str]) -> str: if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): return _flatten(node.left, roots) + _flatten(node.right, roots) if isinstance(node, ast.Name): + if node.id in roots: + return roots[node.id] return "{%s}" % node.id if isinstance(node, ast.Attribute): if isinstance(node.value, ast.Name) and node.value.id == "self": @@ -310,6 +320,8 @@ def _flatten(node: ast.AST, roots: dict[str, str]) -> str: # collapse to "" / "{}" - so look it up by name, not by call shape. if fname in roots: return roots[fname] + if fname in QUERY_STRING_FUNCS: + return "" # Unwrap URL-normalising wrappers, e.g. str(HttpUrl(f"...")). if fname in URL_WRAPPERS and node.args: return _flatten(node.args[0], roots) @@ -414,11 +426,24 @@ def parse_py_file(filepath: str, helper_verbs: dict[str, str], if names: body_class = frozenset(names) + # Local variables assigned from a query_string(...)-style helper + # earlier in the method, e.g. `possible_query_params = query_string(...)` + # then interpolated as f"{url}{possible_query_params}". Scoped to this + # function only - resolve_roots only sees self. assignments. + fn_roots = dict(roots) + for node in ast.walk(fn): + if (isinstance(node, ast.Assign) and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + and isinstance(node.value, ast.Call)): + callee = (node.value.func.id if isinstance(node.value.func, ast.Name) else None) + if callee in QUERY_STRING_FUNCS: + fn_roots[node.targets[0].id] = "" + for node in ast.walk(fn): # url = ... if (raw_url is None and isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == "url" for t in node.targets)): - raw_url = _flatten(node.value, roots) + raw_url = _flatten(node.value, fn_roots) lint = lint_url(raw_url) path = canon_path(raw_url) From bd4c4ee26db974e05be5524b905052b0bbe09408 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 18:15:51 +0100 Subject: [PATCH 6/8] fix(pyegeria): 4 methods called a helper that does not exist -- guaranteed AttributeError on every call schema_maker.delete_schema_type, schema_maker.delete_schema_attribute, subject_area.delete_subject_area, and time_keeper.delete_context_event all called self._async_delete_element_body_request(url, body). That method does not exist anywhere in the codebase -- not on ServerClient, not on any base class. Every call to any of these four methods raised AttributeError before any request was sent. Found while investigating a "BODY sends DeleteElementRequestBody != MetadataSourceRequestBody" audit finding for delete_schema_type; the audit's body-class check doesn't (and can't) detect a call to a name that isn't defined at all, since it only compares declared types, so this was more broken than the audit reported. Fix depends on what each method's ground truth (.http file) actually documents as its request body: - schema_maker.delete_schema_type / delete_schema_attribute: Egeria documents "class": "MetadataSourceRequestBody" for both (confirmed against Egeria-api-schema-maker.http). Routed to the existing, correct _async_metadata_source_body_request helper (validates MetadataSourceRequestBody via _metadata_source_request_adapter) and corrected the misleading DeleteElementRequestBody annotations on all 4 signatures (both async/sync pairs) to match. - subject_area.delete_subject_area / time_keeper.delete_context_event: Egeria documents "class": "DeleteElementRequestBody" for both (confirmed against Egeria-api-subject-area.http / Egeria-api-time-keeper.http) -- these two already had the correct annotation, just the wrong helper name. Routed to the existing _async_delete_element_request helper instead. Verified: full pyegeria/ compiles, all four touched modules import, zero remaining references to the nonexistent helper name anywhere in the tree, micro-tests pass, functional tests collect. Audit: schema-maker now 11 OK / 3 mismatch (was 10 / 4) -- the 3 remaining are Egeria-api-schema-maker.http itself missing the {{schemaAttributeGUID}} path segment on updateSchemaAttribute/deleteSchemaAttribute (every sibling type in the same file correctly includes it) plus one stray '"}' copy-paste artifact on getSchemaAttributeByGUID's line -- ground-truth data-quality issues, not SDK bugs, left unchanged. Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 7 ++----- pyegeria/omvs/schema_maker.py | 13 +++++++------ pyegeria/omvs/subject_area.py | 2 +- pyegeria/omvs/time_keeper.py | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index b2be5b35..14338f07 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 642 | -| Mismatch (verb/path/body) | 17 | +| OK | 643 | +| Mismatch (verb/path/body) | 16 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -487,8 +487,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: schema-maker -- deleteSchemaType: MISMATCH `delete_schema_type` - - BODY sends DeleteElementRequestBody != MetadataSourceRequestBody - updateSchemaAttribute: MISMATCH `update_schema_attribute` - PATH SDK: /schema-maker/schema-attributes/{}/update @@ -497,7 +495,6 @@ _Review only - cross-service overlap is often intentional._ - PATH SDK: /schema-maker/schema-attributes/{}/delete API: /schema-maker/schema-attributes/delete - - BODY sends DeleteElementRequestBody != MetadataSourceRequestBody - getSchemaAttributeByGUID: MISMATCH `get_schema_attribute_by_guid` - PATH SDK: /schema-maker/schema-attributes/{}/retrieve diff --git a/pyegeria/omvs/schema_maker.py b/pyegeria/omvs/schema_maker.py index 344fe9d0..85d97f7e 100644 --- a/pyegeria/omvs/schema_maker.py +++ b/pyegeria/omvs/schema_maker.py @@ -20,6 +20,7 @@ SearchStringRequestBody, GetRequestBody, ReferenceableProperties, + MetadataSourceRequestBody, ) from pyegeria.view.output_formatter import ( populate_common_columns, @@ -130,12 +131,12 @@ def update_schema_type(self, schema_type_guid: str, body: dict | UpdateElementRe @dynamic_catch async def _async_delete_schema_type( - self, schema_type_guid: str, body: dict | DeleteElementRequestBody + self, schema_type_guid: str, body: dict | MetadataSourceRequestBody ) -> None: url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/schema-maker/schema-types/{schema_type_guid}/delete" - await self._async_delete_element_body_request(url, body) + await self._async_metadata_source_body_request(url, body) - def delete_schema_type(self, schema_type_guid: str, body: dict | DeleteElementRequestBody) -> None: + def delete_schema_type(self, schema_type_guid: str, body: dict | MetadataSourceRequestBody) -> None: loop = asyncio.get_event_loop() loop.run_until_complete(self._async_delete_schema_type(schema_type_guid, body)) @@ -165,13 +166,13 @@ def update_schema_attribute( @dynamic_catch async def _async_delete_schema_attribute( - self, schema_attribute_guid: str, body: dict | DeleteElementRequestBody + self, schema_attribute_guid: str, body: dict | MetadataSourceRequestBody ) -> None: url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/schema-maker/schema-attributes/{schema_attribute_guid}/delete" - await self._async_delete_element_body_request(url, body) + await self._async_metadata_source_body_request(url, body) def delete_schema_attribute( - self, schema_attribute_guid: str, body: dict | DeleteElementRequestBody + self, schema_attribute_guid: str, body: dict | MetadataSourceRequestBody ) -> None: loop = asyncio.get_event_loop() loop.run_until_complete(self._async_delete_schema_attribute(schema_attribute_guid, body)) diff --git a/pyegeria/omvs/subject_area.py b/pyegeria/omvs/subject_area.py index d7bf490f..b8fd6fe0 100644 --- a/pyegeria/omvs/subject_area.py +++ b/pyegeria/omvs/subject_area.py @@ -370,7 +370,7 @@ async def _async_delete_subject_area( ``` """ url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/subject-area/collections/{subject_area_guid}/delete" - await self._async_delete_element_body_request(url, body) + await self._async_delete_element_request(url, body) def delete_subject_area(self, subject_area_guid: str, body: dict | DeleteElementRequestBody) -> None: """Delete a subject area. diff --git a/pyegeria/omvs/time_keeper.py b/pyegeria/omvs/time_keeper.py index db3e08fb..649da55b 100644 --- a/pyegeria/omvs/time_keeper.py +++ b/pyegeria/omvs/time_keeper.py @@ -411,7 +411,7 @@ async def _async_delete_context_event( ``` """ url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/time-keeper/context-events/{context_event_guid}/delete" - await self._async_delete_element_body_request(url, body) + await self._async_delete_element_request(url, body) def delete_context_event(self, context_event_guid: str, body: dict | DeleteElementRequestBody) -> None: """Delete a context event. From 4e405bb679faf95fe2ec46e86d001dc560d9123c Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 18:20:00 +0100 Subject: [PATCH 7/8] fix(pyegeria): glossary-manager -- 3 term-classification clears sent the wrong body class remove_is_abstract_concept, remove_is_context_definition, and remove_activity_description all called _async_delete_classification_request with a DeleteClassificationRequestBody annotation, but Egeria-api-glossary-manager.http documents different classes per endpoint: - clearTermAsAbstractConcept -> DeleteElementRequestBody - clearTermAsActivity -> DeleteRelationshipRequestBody - clearTermAsContext -> DeleteRelationshipRequestBody Egeria's own API is inconsistent here -- other clear_* siblings in this same file (glossary_as_taxonomy, is_data_value, term_as_question, is_prime_word, is_modifier, is_class_word) genuinely do use DeleteClassificationRequestBody and were correctly left untouched; the audit didn't flag them because they already match ground truth. Fixed the 3 that don't, matching each to its own documented class and helper (_async_delete_element_request / _async_delete_relationship_request), both async and sync signatures. Audit now reports glossary-manager at 0 mismatches (was 3). Overall: 646 OK / 13 mismatch. Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 10 ++-------- pyegeria/omvs/glossary_manager.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 14338f07..4d93d02a 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 643 | -| Mismatch (verb/path/body) | 16 | +| OK | 646 | +| Mismatch (verb/path/body) | 13 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -320,12 +320,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: glossary-manager - getTermRelationshipTypeNames: MISSING (`GET /glossary-manager/glossaries/terms/relationships/type-names`) -- clearTermAsAbstractConcept: MISMATCH `remove_is_abstract_concept` - - BODY sends DeleteClassificationRequestBody != DeleteElementRequestBody -- clearTermAsActivity: MISMATCH `remove_activity_description` - - BODY sends DeleteClassificationRequestBody != DeleteRelationshipRequestBody -- clearTermAsContext: MISMATCH `remove_is_context_definition` - - BODY sends DeleteClassificationRequestBody != DeleteRelationshipRequestBody ### Service: governance-officer diff --git a/pyegeria/omvs/glossary_manager.py b/pyegeria/omvs/glossary_manager.py index 26565865..38afa6d6 100644 --- a/pyegeria/omvs/glossary_manager.py +++ b/pyegeria/omvs/glossary_manager.py @@ -1834,7 +1834,7 @@ def add_is_abstract_concept( @dynamic_catch async def _async_remove_is_abstract_concept( - self, term_guid: str, body: Optional[dict | DeleteClassificationRequestBody] = None, + self, term_guid: str, body: Optional[dict | DeleteElementRequestBody] = None, ) -> None: """Remove the abstract concept designation from the glossary term. Async Version. @@ -1876,12 +1876,12 @@ async def _async_remove_is_abstract_concept( f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/glossary-manager/glossaries/" f"terms/{term_guid}/is-abstract-concept/delete" ) - await self._async_delete_classification_request(url, body) + await self._async_delete_element_request(url, body) logger.info(f"Removed AbstractConcept classification to {term_guid}") @dynamic_catch def remove_is_abstract_concept( - self, term_guid: str, body: Optional[dict | DeleteClassificationRequestBody] = None, + self, term_guid: str, body: Optional[dict | DeleteElementRequestBody] = None, ) -> None: """Remove the abstract concept designation from the glossary term. @@ -2026,7 +2026,7 @@ def add_is_context_definition( @dynamic_catch async def _async_remove_is_context_definition( - self, term_guid: str, body: Optional[dict | DeleteClassificationRequestBody] = None, + self, term_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None, ) -> None: """Remove the context definition designation from the glossary term. Async Version. @@ -2067,12 +2067,12 @@ async def _async_remove_is_context_definition( f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/glossary-manager/glossaries/" f"terms/{term_guid}/is-context-definition/delete" ) - await self._async_delete_classification_request(url, body) + await self._async_delete_relationship_request(url, body) logger.info(f"Removed ContextDefinition classification to {term_guid}") @dynamic_catch def remove_is_context_definition( - self, term_guid: str, body: Optional[dict | DeleteClassificationRequestBody] = None, + self, term_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None, ) -> None: """Remove the context definition designation from the glossary term. @@ -2407,7 +2407,7 @@ def add_activity_description( @dynamic_catch async def _async_remove_activity_description( - self, term_guid: str, body: Optional[dict | DeleteClassificationRequestBody] = None, + self, term_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None, ) -> None: """Remove the activity designation from the glossary term. Async Version. @@ -2449,12 +2449,12 @@ async def _async_remove_activity_description( f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/glossary-manager/glossaries/" f"terms/{term_guid}/is-activity/remove" ) - await self._async_delete_classification_request(url, body) + await self._async_delete_relationship_request(url, body) logger.info(f"Removed ActivityDescription classification to {term_guid}") @dynamic_catch def remove_activity_description( - self, term_guid: str, body: Optional[dict | DeleteClassificationRequestBody] = None, + self, term_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None, ) -> None: """Remove the activity designation from the glossary term. From 9017566985b580b1108948ff9d3d144f6c1f4826 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 18:29:07 +0100 Subject: [PATCH 8/8] fix(pyegeria): 6 more wrong-helper/wrong-path bugs across 5 modules; audit resolver picks up local URL-root variables Fixed the remaining audit mismatches down to 7 (were 13), all confirmed against ground truth individually -- this batch skews toward more severe, functionally-wrong-operation bugs than earlier ones. Endpoint fixes: - project_manager.clear_project_classification called _async_new_classification_request (the ADD-classification helper) instead of _async_delete_classification_request, despite its own URL (.../declassify) and docstring both documenting a remove operation. Calling "clear" would have added a ProjectClassification instead of removing one -- the opposite of what the method claims to do. - valid_metadata.link_specification_property called _async_create_element_body_request (validates NewElementRequestBody) instead of _async_new_relationship_request (NewRelationshipRequestBody, confirmed via Egeria-api-valid-metadata.http) for an /attach relationship endpoint. - my_profile.get_my_profile hand-rolled a POST to a GET-only, bodyless endpoint (Egeria-api-my-profile.http: plain GET, no request body -- the profile is derived from the bearer token). Every call would have hit the wrong HTTP method. - actor_manager.delete_actor_role annotated + called the DeleteElementRequestBody path, but both ground truth and the method's own docstring sample document DeleteRelationshipRequestBody. Switched to _async_delete_relationship_request. - data_designer.detach_specialized_data_value_specification used ".../specialized-data-value-specification-definition/..." (singular, "-definition" suffix); its attach sibling in the same file already uses the correct ".../specialized-data-value-specifications/..." (plural). - lineage_linker.link_lineage used a made-up ".../elements/{}/{}/{}/attach" path with no model validation at all (raw dict passed straight to _async_make_request). A correct sibling, link_data_flow, already builds the real ".../from-elements/{}/via/{}/to-elements/{}/attach" shape -- matched it and routed through _async_new_relationship_request instead of an unvalidated raw POST. Tooling (scripts/omvs_audit.py): - Generalized the per-function local-variable root resolution (added for query_string() in the previous commit) to also catch a plain local assignment building a URL prefix inside a method body, e.g. `base = f"{self.platform_url}/servers/.../api/open-metadata/"` (collection_manager.py). resolve_roots only sees self. assignments in __init__; this covers the same pattern scoped to a local variable. Cleared 1 more false mismatch (detach_associated_skill_set). Not changed -- flagged as ambiguous, not mechanically fixable: - detachSolutionLinkingWire (MISMATCH) and detachAllSolutionLinkingWire (MISSING) are two distinct, correctly-implemented Egeria endpoints (detach_solution_linking_wire and detach_solution_linking_wire_by_guid) whose ground-truth names are too similar for the audit's exact-name matching to disambiguate -- adding a NAME_OVERRIDES entry would just move the false mismatch from one ground-truth row to the other rather than resolve it. Both underlying SDK methods are already correct; this is a naming-convention decision for a human, not a bug. - updateActorRole/deleteActorRole and updateSchemaAttribute/ deleteSchemaAttribute PATH findings, and 2 stray '"}' artifacts (still ground-truth data-quality issues identified in earlier commits, unchanged). Verified: full pyegeria/ compiles, all touched modules import, micro-tests pass, functional tests collect. Audit: 652 OK / 7 mismatch (was 646 / 13). Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 29 ++++++++--------------------- pyegeria/omvs/actor_manager.py | 6 +++--- pyegeria/omvs/data_designer.py | 2 +- pyegeria/omvs/lineage_linker.py | 5 ++--- pyegeria/omvs/my_profile.py | 14 ++++++-------- pyegeria/omvs/project_manager.py | 3 +-- pyegeria/omvs/valid_metadata.py | 2 +- scripts/omvs_audit.py | 25 +++++++++++++++++-------- 8 files changed, 39 insertions(+), 47 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 4d93d02a..8c70ec2c 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 646 | -| Mismatch (verb/path/body) | 13 | +| OK | 652 | +| Mismatch (verb/path/body) | 7 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -15,6 +15,9 @@ Subject: `pyegeria/omvs` (43 modules) _Review only - cross-service overlap is often intentional._ +- `GET /my-profile` + - `my-profile.py`: `_async_get_my_profile` + - `my-profile.py`: `_async_get_my_profile_by_get` - `POST /automated-curation/governance-action-types/initiate` - `automated-curation.py`: `_async_initiate_gov_action_type` - `automated-curation.py`: `_async_initiate_survey` @@ -62,6 +65,9 @@ _Review only - cross-service overlap is often intentional._ - `action-author.py`: `_async_get_governance_action_process` - `governance-officer.py`: `_async_get_governance_definition_by_guid` - `governance-officer.py`: `_async_get_governance_action_process` +- `POST /lineage-linker/from-elements/{}/via/{}/to-elements/{}/attach` + - `lineage-linker.py`: `_async_link_lineage` + - `lineage-linker.py`: `_async_link_data_flow` - `POST /metadata-expert/metadata-elements/{}/history` - `metadata-expert.py`: `_async_get_element_history` - `metadata-expert.py`: `_async_get_metadata_element_history` @@ -123,7 +129,6 @@ _Review only - cross-service overlap is often intentional._ - PATH SDK: /actor-manager/actor-roles/{}/delete API: /actor-manager/actor-roles/delete - - BODY sends DeleteElementRequestBody != DeleteRelationshipRequestBody - getActorRoleByGUID: MISMATCH `get_actor_role_by_guid` - PATH SDK: /actor-manager/actor-roles/{}/retrieve @@ -223,10 +228,6 @@ _Review only - cross-service overlap is often intentional._ - attachSmartQuery: MISSING (`POST /collection-manager/collections/results-sets/{}/smart-query/{}/attach`) - detachSmartQuery: MISSING (`POST /collection-manager/collections/results-sets/{}/smart-query/{}/detach`) - attachAssociatedSkillSet: MISSING (`POST /collection-manager/actors/{}/associated-skill-sets/{}/attach`) -- detachAssociatedSkillSet: MISMATCH `detach_associated_skill_set` - - PATH - SDK: /{}/actors/{}/associated-skill-sets/{}/detach - API: /collection-manager/actors/{}/associated-skill-sets/{}/detach - updateCollectionMembership: MISSING (`POST /collection-manager/collections/{}/members/{}/update`) ### Service: community-matters @@ -248,10 +249,6 @@ _Review only - cross-service overlap is often intentional._ - getDataFieldsByName - with full request body: MISSING (`POST /data-designer/data-fields/by-name`) - getDataFieldByGUID - with request body: MISSING (`POST /data-designer/data-fields/{}/retrieve`) - createDataValueSpecificationFromTemplate: MISSING (`POST /data-designer/data-value-specifications/from-template`) -- detachSpecializedDataValueSpecification: MISMATCH `detach_specialized_data_value_specification` - - PATH - SDK: /data-designer/data-value-specifications/{}/specialized-data-value-specification-definition/{}/detach - API: /data-designer/data-value-specifications/{}/specialized-data-value-specifications/{}/detach - assignDataValueSpecification: MISSING (`POST /data-designer/elements/{}/data-value-specifications/{}/attach`) - detachDataValueSpecificationAssignment: MISSING (`POST /data-designer/elements/{}/data-value-specifications/{}/detach`) - findAllDataClasses: MISSING (`POST /data-designer/data-value-specifications/by-search-string`) @@ -357,10 +354,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: lineage-linker -- linkLineage: MISMATCH `link_lineage` - - PATH - SDK: /lineage-linker/elements/{}/{}/{}/attach - API: /lineage-linker/from-elements/{}/via/{}/to-elements/{}/attach ### Service: location-arena @@ -387,8 +380,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: my-profile -- getMyProfile: MISMATCH `get_my_profile` - - VERB POST != GET - Get My Profile: MISSING (`POST /my-profile`) - Add My Profile: MISSING (`POST /my-profile/new`) @@ -440,8 +431,6 @@ _Review only - cross-service overlap is often intentional._ - createClassifiedProject: MISSING (`POST /project-manager/projects`) - createCampaign: MISSING (`POST /project-manager/projects`) - createTaskForProject: MISSING (`POST /project-manager/projects/{}/task`) -- clearProjectClassification: MISMATCH `clear_project_classification` - - BODY sends NewClassificationRequestBody != DeleteClassificationRequestBody - setupProjectDependency: MISSING (`POST /project-manager/projects/{}/project-dependencies/{}/attach`) - setupProjectHierarchy: MISSING (`POST /project-manager/projects/{}/project-hierarchies/{}/attach`) @@ -562,5 +551,3 @@ _Review only - cross-service overlap is often intentional._ - getAttributeTypes: MISSING (`GET /valid-metadata/open-metadata-types/attribute-defs`) - getTypeDefByName: MISSING (`GET /valid-metadata/open-metadata-types/name/{}`) - setUpSpecificationProperty: MISSING (`POST /valid-metadata/elements/{}/specification-properties`) -- linkSpecificationProperty: MISMATCH `link_specification_property` - - BODY sends NewElementRequestBody != NewRelationshipRequestBody diff --git a/pyegeria/omvs/actor_manager.py b/pyegeria/omvs/actor_manager.py index f198c53c..7d3ee49a 100644 --- a/pyegeria/omvs/actor_manager.py +++ b/pyegeria/omvs/actor_manager.py @@ -2188,7 +2188,7 @@ def detach_it_profile_role_from_it_profile(self, it_profile_role_guid: str, it_p @dynamic_catch async def _async_delete_actor_role(self, actor_role_guid: str, - body: Optional[dict | DeleteElementRequestBody] = None, + body: Optional[dict | DeleteRelationshipRequestBody] = None, cascade: bool = False) -> None: """ Delete an actor role. Async Version. @@ -2231,11 +2231,11 @@ async def _async_delete_actor_role(self, actor_role_guid: str, """ url = f"{self.command_root}/actor-roles/{actor_role_guid}/delete" - await self._async_delete_element_request(url, body, cascade) + await self._async_delete_relationship_request(url, body, cascade) logger.debug(f"Deleted actor role {actor_role_guid} with cascade {cascade}") @dynamic_catch - def delete_actor_role(self, actor_role_guid: str, body: Optional[dict | DeleteElementRequestBody] = None, + def delete_actor_role(self, actor_role_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None, cascade: bool = False) -> None: """ Delete an actor role. Async Version. diff --git a/pyegeria/omvs/data_designer.py b/pyegeria/omvs/data_designer.py index a30e9a83..b5bc05f0 100644 --- a/pyegeria/omvs/data_designer.py +++ b/pyegeria/omvs/data_designer.py @@ -267,7 +267,7 @@ async def _async_detach_specialized_data_value_specification( """ url = ( f"{self.ref_data_designer_command_base}/data-value-specifications/{spec_guid}" - f"/specialized-data-value-specification-definition/{grain_guid}/detach" + f"/specialized-data-value-specifications/{grain_guid}/detach" ) await self._async_delete_relationship_request(url, body, cascade_delete) diff --git a/pyegeria/omvs/lineage_linker.py b/pyegeria/omvs/lineage_linker.py index db8025e7..a8d24586 100644 --- a/pyegeria/omvs/lineage_linker.py +++ b/pyegeria/omvs/lineage_linker.py @@ -159,9 +159,8 @@ async def _async_link_lineage( } ``` """ - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/lineage-linker/elements/{element_one_guid}/{relationship_type_name}/{element_two_guid}/attach" - response = await self._async_make_request("POST", url, body) - return response.json().get("guid") + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/lineage-linker/from-elements/{element_one_guid}/via/{relationship_type_name}/to-elements/{element_two_guid}/attach" + return await self._async_new_relationship_request(url, ["LineageRelationshipProperties"], body) def link_lineage( self, diff --git a/pyegeria/omvs/my_profile.py b/pyegeria/omvs/my_profile.py index e9982991..10d53b03 100644 --- a/pyegeria/omvs/my_profile.py +++ b/pyegeria/omvs/my_profile.py @@ -376,14 +376,12 @@ async def _async_get_my_profile( Egeria errors. """ url = self.my_profile_command_root - # response = await self._async_get_request_body_request(url=url, _type="Actor", - # _gen_output=self._generate_my_profile_output, - # output_format=output_format, report_spec=report_spec, - # body=body,**kwargs) - if body is None: - response = await self._async_make_request("POST", url) - else: - response = await self._async_make_request("POST", url, body_slimmer(body)) + # Egeria-api-my-profile.http documents this endpoint as a plain GET + # with no request body -- it derives the profile from the bearer + # token, not from filter criteria. The `body`/GetRequestBody + # parameter is accepted for signature consistency with sibling + # methods but intentionally unused here. + response = await self._async_make_request("GET", url) elements = response.json().get("element", NO_ELEMENTS_FOUND) if isinstance(elements, dict): self.my_profile_guid = elements.get("elementHeader", {}).get("guid") diff --git a/pyegeria/omvs/project_manager.py b/pyegeria/omvs/project_manager.py index 820036ad..86b75bf9 100644 --- a/pyegeria/omvs/project_manager.py +++ b/pyegeria/omvs/project_manager.py @@ -459,8 +459,7 @@ async def _async_clear_project_classification( f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/project-manager/" f"projects/{project_guid}/classification-properties/declassify" ) - response = await self._async_new_classification_request(url, ["ProjectClassificationProperties"], - body=body) + response = await self._async_delete_classification_request(url, body=body) return response @dynamic_catch diff --git a/pyegeria/omvs/valid_metadata.py b/pyegeria/omvs/valid_metadata.py index a56fd8a9..ebeb6663 100644 --- a/pyegeria/omvs/valid_metadata.py +++ b/pyegeria/omvs/valid_metadata.py @@ -2689,7 +2689,7 @@ async def _async_link_specification_property( """ url = (f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/valid-metadata/" f"elements/{element_guid}/specification-properties/{vv_def_guid}/attach") - await self._async_create_element_body_request(url, ["SpecificationPropertyAssignmentProperties"], body) + await self._async_new_relationship_request(url, ["SpecificationPropertyAssignmentProperties"], body) logger.info(f"Linked specification property {vv_def_guid} to element {element_guid}") @dynamic_catch diff --git a/scripts/omvs_audit.py b/scripts/omvs_audit.py index efb3a92f..3541877e 100644 --- a/scripts/omvs_audit.py +++ b/scripts/omvs_audit.py @@ -426,18 +426,27 @@ def parse_py_file(filepath: str, helper_verbs: dict[str, str], if names: body_class = frozenset(names) - # Local variables assigned from a query_string(...)-style helper - # earlier in the method, e.g. `possible_query_params = query_string(...)` - # then interpolated as f"{url}{possible_query_params}". Scoped to this - # function only - resolve_roots only sees self. assignments. + # Local variables assigned a URL prefix inside the method body, rather + # than in __init__ (resolve_roots only sees self. assignments + # there) - covers two shapes seen in the OMVS clients: + # base = f"{self.platform_url}/servers/{...}/api/open-metadata/..." + # possible_query_params = query_string([...]) + # both later interpolated straight into the url f-string. Scoped to + # this function only. fn_roots = dict(roots) for node in ast.walk(fn): - if (isinstance(node, ast.Assign) and len(node.targets) == 1 - and isinstance(node.targets[0], ast.Name) - and isinstance(node.value, ast.Call)): + if not (isinstance(node, ast.Assign) and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name)): + continue + target = node.targets[0].id + if isinstance(node.value, ast.Call): callee = (node.value.func.id if isinstance(node.value.func, ast.Name) else None) if callee in QUERY_STRING_FUNCS: - fn_roots[node.targets[0].id] = "" + fn_roots[target] = "" + else: + value = _flatten(node.value, fn_roots) + if "/open-metadata/" in value: + fn_roots[target] = value for node in ast.walk(fn): # url = ...