diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 5505a8ff..8c70ec2c 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 | 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,12 @@ _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` - `POST /metadata-expert/related-elements` - `collection-manager.py`: `_async_link_saved_query_to_results_set` - `metadata-expert.py`: `_async_create_related_elements` @@ -108,15 +117,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`) @@ -124,27 +129,12 @@ _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 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`) -- 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,16 +184,8 @@ _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 - 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`) @@ -213,29 +195,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`) @@ -266,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 @@ -291,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`) @@ -306,8 +260,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 @@ -365,12 +317,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 @@ -397,8 +343,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`) @@ -410,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 @@ -433,78 +373,13 @@ _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 != 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 -- getMyProfile: MISMATCH `get_my_profile` - - VERB POST != GET - Get My Profile: MISSING (`POST /my-profile`) - Add My Profile: MISSING (`POST /my-profile/new`) @@ -556,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`) @@ -597,8 +470,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 @@ -607,7 +478,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 @@ -620,10 +490,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`) @@ -644,10 +510,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`) @@ -658,21 +520,11 @@ _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 -- 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 +538,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 @@ -701,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/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..7d3ee49a 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: @@ -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, @@ -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: @@ -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, @@ -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. @@ -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: @@ -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}") @@ -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..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, @@ -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_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/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/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. 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/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/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/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/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/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/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/pyegeria/omvs/subject_area.py b/pyegeria/omvs/subject_area.py index 09578e78..b8fd6fe0 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. @@ -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 c9a6f8f2..649da55b 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. @@ -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. 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 3115a37c..3541877e 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": @@ -301,10 +311,18 @@ 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] + 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) # e.g. name.lower() - name the receiver so lint can classify it @@ -350,6 +368,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 +401,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] = [] @@ -384,11 +426,33 @@ def parse_py_file(filepath: str, helper_verbs: dict[str, str], if names: body_class = frozenset(names) + # 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 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[target] = "" + else: + value = _flatten(node.value, fn_roots) + if "/open-metadata/" in value: + fn_roots[target] = value + 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)