From caff1eb1fca16687044a14d07b5f63f0c392a20c Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Sat, 19 Sep 2026 13:48:06 -0500 Subject: [PATCH] feat(metadata-expert): add re-identify, re-type, re-home operations Egeria PR odpi/egeria#9315 added six new Metadata Expert OMVS endpoints for changing an instance's control information (unique identifier, type, and home repository) for both metadata elements and relationships. This adds the matching pyegeria surface: three new request-body models (ReIdentifyRequestBody, ReTypeRequestBody, ReHomeRequestBody), their validation/serialization plumbing in ServerClient, and six async+sync method pairs on the MetadataExpert client. Verified against the merged endpoint definitions on odpi/egeria main (the local http-clients copy predates this PR) via scripts/omvs_audit.py (22/22 OK, 0 mismatches) and the full tests/micro-tests suite. Signed-off-by: Dan Wolfson --- pyegeria/core/_server_client.py | 55 ++++++++ pyegeria/models/__init__.py | 6 + pyegeria/models/models.py | 16 +++ pyegeria/omvs/metadata_expert.py | 231 +++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+) diff --git a/pyegeria/core/_server_client.py b/pyegeria/core/_server_client.py index 45305db8..93d72c93 100644 --- a/pyegeria/core/_server_client.py +++ b/pyegeria/core/_server_client.py @@ -35,6 +35,7 @@ NewExternalIdRequestBody, DeleteElementRequestBody, DeleteRelationshipRequestBody, DeleteClassificationRequestBody, LevelIdentifierQueryBody, UpdatePropertiesRequestBody, MetadataSourceRequestBody, + ReIdentifyRequestBody, ReTypeRequestBody, ReHomeRequestBody, UpdateEffectivityDatesRequestBody, OpenMetadataDeleteRequestBody, NewOpenMetadataElementRequestBody, NewRelatedElementsRequestBody, FindPropertyNamesRequestBody, FindRequestBody, @@ -68,6 +69,9 @@ LevelIdentifierQueryBody, UpdatePropertiesRequestBody, MetadataSourceRequestBody, + ReIdentifyRequestBody, + ReTypeRequestBody, + ReHomeRequestBody, UpdateEffectivityDatesRequestBody, OpenMetadataDeleteRequestBody, NewOpenMetadataElementRequestBody, @@ -191,6 +195,9 @@ def __init__( self._level_identifier_query_body = TypeAdapter(LevelIdentifierQueryBody) self._update_properties_request_adapter = TypeAdapter(UpdatePropertiesRequestBody) self._metadata_source_request_adapter = TypeAdapter(MetadataSourceRequestBody) + self._re_identify_request_adapter = TypeAdapter(ReIdentifyRequestBody) + self._re_type_request_adapter = TypeAdapter(ReTypeRequestBody) + self._re_home_request_adapter = TypeAdapter(ReHomeRequestBody) self._update_effectivity_dates_request_adapter = TypeAdapter(UpdateEffectivityDatesRequestBody) self._open_metadata_delete_request_adapter = TypeAdapter(OpenMetadataDeleteRequestBody) self._new_open_metadata_element_request_adapter = TypeAdapter(NewOpenMetadataElementRequestBody) @@ -6269,6 +6276,39 @@ def validate_metadata_source_request(self, body: Optional[dict | MetadataSourceR validated_body = self._validate_body(MetadataSourceRequestBody.model_validate, body) return validated_body + @dynamic_catch + def validate_re_identify_request(self, body: Optional[dict | ReIdentifyRequestBody] = None) -> ReIdentifyRequestBody | None: + if isinstance(body, ReIdentifyRequestBody): + validated_body = body + elif isinstance(body, dict): + validated_body = self._validate_body(self._re_identify_request_adapter.validate_python, body) + else: + body = {"class": "ReIdentifyRequestBody"} + validated_body = self._validate_body(ReIdentifyRequestBody.model_validate, body) + return validated_body + + @dynamic_catch + def validate_re_type_request(self, body: Optional[dict | ReTypeRequestBody] = None) -> ReTypeRequestBody | None: + if isinstance(body, ReTypeRequestBody): + validated_body = body + elif isinstance(body, dict): + validated_body = self._validate_body(self._re_type_request_adapter.validate_python, body) + else: + body = {"class": "ReTypeRequestBody"} + validated_body = self._validate_body(ReTypeRequestBody.model_validate, body) + return validated_body + + @dynamic_catch + def validate_re_home_request(self, body: Optional[dict | ReHomeRequestBody] = None) -> ReHomeRequestBody | None: + if isinstance(body, ReHomeRequestBody): + validated_body = body + elif isinstance(body, dict): + validated_body = self._validate_body(self._re_home_request_adapter.validate_python, body) + else: + body = {"class": "ReHomeRequestBody"} + validated_body = self._validate_body(ReHomeRequestBody.model_validate, body) + return validated_body + @dynamic_catch def validate_update_effectivity_dates_request(self, body: Optional[dict | UpdateEffectivityDatesRequestBody] = None) -> UpdateEffectivityDatesRequestBody | None: if isinstance(body, UpdateEffectivityDatesRequestBody): @@ -6340,6 +6380,21 @@ async def _async_metadata_source_body_request(self, url: str, body: Optional[dic validated_body = self.validate_metadata_source_request(body) await self._async_make_request("POST", url, validated_body.model_dump(by_alias=True, exclude_none=True)) + @dynamic_catch + async def _async_re_identify_body_request(self, url: str, body: Optional[dict | ReIdentifyRequestBody] = None) -> None: + validated_body = self.validate_re_identify_request(body) + await self._async_make_request("POST", url, validated_body.model_dump(by_alias=True, exclude_none=True)) + + @dynamic_catch + async def _async_re_type_body_request(self, url: str, body: Optional[dict | ReTypeRequestBody] = None) -> None: + validated_body = self.validate_re_type_request(body) + await self._async_make_request("POST", url, validated_body.model_dump(by_alias=True, exclude_none=True)) + + @dynamic_catch + async def _async_re_home_body_request(self, url: str, body: Optional[dict | ReHomeRequestBody] = None) -> None: + validated_body = self.validate_re_home_request(body) + await self._async_make_request("POST", url, validated_body.model_dump(by_alias=True, exclude_none=True)) + @dynamic_catch async def _async_update_effectivity_dates_body_request(self, url: str, body: Optional[dict | UpdateEffectivityDatesRequestBody] = None) -> None: validated_body = self.validate_update_effectivity_dates_request(body) diff --git a/pyegeria/models/__init__.py b/pyegeria/models/__init__.py index e16aff0a..7d4fe548 100644 --- a/pyegeria/models/__init__.py +++ b/pyegeria/models/__init__.py @@ -29,6 +29,9 @@ UpdateWithTemplateRequestBody, UpdatePropertiesRequestBody, MetadataSourceRequestBody, + ReIdentifyRequestBody, + ReTypeRequestBody, + ReHomeRequestBody, UpdateEffectivityDatesRequestBody, OpenMetadataDeleteRequestBody, NewOpenMetadataElementRequestBody, @@ -122,6 +125,9 @@ "UpdateWithTemplateRequestBody", "UpdatePropertiesRequestBody", "MetadataSourceRequestBody", + "ReIdentifyRequestBody", + "ReTypeRequestBody", + "ReHomeRequestBody", "UpdateEffectivityDatesRequestBody", "OpenMetadataDeleteRequestBody", "NewOpenMetadataElementRequestBody", diff --git a/pyegeria/models/models.py b/pyegeria/models/models.py index e968fc37..ee47adb3 100644 --- a/pyegeria/models/models.py +++ b/pyegeria/models/models.py @@ -565,6 +565,22 @@ class MetadataSourceRequestBody(RequestBody): class_: Annotated[Literal["MetadataSourceRequestBody"], Field(alias="class")] +class ReIdentifyRequestBody(RequestBody): + class_: Annotated[Literal["ReIdentifyRequestBody"], Field(alias="class")] + new_guid: str | None = None + + +class ReTypeRequestBody(RequestBody): + class_: Annotated[Literal["ReTypeRequestBody"], Field(alias="class")] + new_type_name: str | None = None + + +class ReHomeRequestBody(RequestBody): + class_: Annotated[Literal["ReHomeRequestBody"], Field(alias="class")] + new_home_metadata_collection_id: str | None = None + new_home_metadata_collection_name: str | None = None + + class UpdateEffectivityDatesRequestBody(RequestBody): class_: Annotated[Literal["UpdateEffectivityDatesRequestBody"], Field(alias="class")] effective_from: datetime | None = None diff --git a/pyegeria/omvs/metadata_expert.py b/pyegeria/omvs/metadata_expert.py index da286ff7..168ed806 100644 --- a/pyegeria/omvs/metadata_expert.py +++ b/pyegeria/omvs/metadata_expert.py @@ -11,6 +11,7 @@ from httpx import Response from pyegeria.models import (NewOpenMetadataElementRequestBody, TemplateRequestBody, UpdatePropertiesRequestBody, MetadataSourceRequestBody, + ReIdentifyRequestBody, ReTypeRequestBody, ReHomeRequestBody, UpdateEffectivityDatesRequestBody, OpenMetadataDeleteRequestBody, NewClassificationRequestBody, NewRelatedElementsRequestBody, SearchStringRequestBody, @@ -245,6 +246,121 @@ def update_metadata_element_properties(self, metadata_element_guid: str, body: O loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_update_metadata_element_properties(metadata_element_guid, body)) + @dynamic_catch + async def _async_reidentify_metadata_element(self, metadata_element_guid: str, body: Optional[dict | ReIdentifyRequestBody] = None) -> None: + """ + Change the unique identifier of a metadata element. This is used if two different elements are discovered + to have the same unique identifier. The request is routed to the repository that is the home of the + element. Async version. + + Parameters + ---------- + metadata_element_guid : str + Current unique identifier of the metadata element. + body : dict | ReIdentifyRequestBody, optional + The new unique identifier for the metadata element. + + Notes + ----- + Sample JSON body: + { + "class" : "ReIdentifyRequestBody", + "externalSourceGUID" : "", + "externalSourceName" : "", + "forLineage" : false, + "forDuplicateProcessing" : false, + "effectiveTime" : "2024-01-01T00:00:00.000+00:00", + "newGUID" : "new-guid-value" + } + """ + url = f"{self.command_root}/metadata-elements/{metadata_element_guid}/re-identify" + await self._async_re_identify_body_request(url, body) + + @dynamic_catch + def reidentify_metadata_element(self, metadata_element_guid: str, body: Optional[dict | ReIdentifyRequestBody] = None) -> None: + """ + Change the unique identifier of a metadata element. + """ + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_reidentify_metadata_element(metadata_element_guid, body)) + + @dynamic_catch + async def _async_retype_metadata_element(self, metadata_element_guid: str, body: Optional[dict | ReTypeRequestBody] = None) -> None: + """ + Change the type of a metadata element. Typically, this action is taken to move an element's type to either + a supertype (so the subtype can be deleted) or a new subtype (so additional properties can be added). The + request is routed to the repository that is the home of the element. Async version. + + Parameters + ---------- + metadata_element_guid : str + Unique identifier of the metadata element to update. + body : dict | ReTypeRequestBody, optional + The name of the new type for the metadata element. + + Notes + ----- + Sample JSON body: + { + "class" : "ReTypeRequestBody", + "externalSourceGUID" : "", + "externalSourceName" : "", + "forLineage" : false, + "forDuplicateProcessing" : false, + "effectiveTime" : "2024-01-01T00:00:00.000+00:00", + "newTypeName" : "NewTypeName" + } + """ + url = f"{self.command_root}/metadata-elements/{metadata_element_guid}/re-type" + await self._async_re_type_body_request(url, body) + + @dynamic_catch + def retype_metadata_element(self, metadata_element_guid: str, body: Optional[dict | ReTypeRequestBody] = None) -> None: + """ + Change the type of a metadata element. + """ + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_retype_metadata_element(metadata_element_guid, body)) + + @dynamic_catch + async def _async_rehome_metadata_element(self, metadata_element_guid: str, body: Optional[dict | ReHomeRequestBody] = None) -> None: + """ + Change the home repository of a metadata element. This action is taken, for example, if the original home + repository becomes permanently unavailable, or if the user community updating this element moves to + working from a different repository in the open metadata repository cohort. Async version. + + Parameters + ---------- + metadata_element_guid : str + Unique identifier of the metadata element to update. + body : dict | ReHomeRequestBody, optional + Details of the new home metadata collection. + + Notes + ----- + Sample JSON body: + { + "class" : "ReHomeRequestBody", + "externalSourceGUID" : "", + "externalSourceName" : "", + "forLineage" : false, + "forDuplicateProcessing" : false, + "effectiveTime" : "2024-01-01T00:00:00.000+00:00", + "newHomeMetadataCollectionId" : "new-home-collection-guid", + "newHomeMetadataCollectionName" : "New Home Collection Name" + } + """ + url = f"{self.command_root}/metadata-elements/{metadata_element_guid}/re-home" + await self._async_re_home_body_request(url, body) + + @dynamic_catch + def rehome_metadata_element(self, metadata_element_guid: str, body: Optional[dict | ReHomeRequestBody] = None) -> None: + """ + Change the home repository of a metadata element. + """ + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_rehome_metadata_element(metadata_element_guid, body)) + @dynamic_catch async def _async_publish_metadata_element(self, metadata_element_guid: str, body: Optional[dict | MetadataSourceRequestBody] = None) -> None: """ @@ -709,6 +825,121 @@ def update_related_elements_properties(self, relationship_guid: str, body: Optio loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_update_related_elements_properties(relationship_guid, body)) + @dynamic_catch + async def _async_reidentify_related_elements(self, relationship_guid: str, body: Optional[dict | ReIdentifyRequestBody] = None) -> None: + """ + Change the unique identifier of a relationship. This is used if two different relationships are discovered + to have the same unique identifier. The request is routed to the repository that is the home of the + relationship. Async version. + + Parameters + ---------- + relationship_guid : str + Current unique identifier of the relationship. + body : dict | ReIdentifyRequestBody, optional + The new unique identifier for the relationship. + + Notes + ----- + Sample JSON body: + { + "class" : "ReIdentifyRequestBody", + "externalSourceGUID" : "", + "externalSourceName" : "", + "forLineage" : false, + "forDuplicateProcessing" : false, + "effectiveTime" : "2024-01-01T00:00:00.000+00:00", + "newGUID" : "new-guid-value" + } + """ + url = f"{self.command_root}/related-elements/{relationship_guid}/re-identify" + await self._async_re_identify_body_request(url, body) + + @dynamic_catch + def reidentify_related_elements(self, relationship_guid: str, body: Optional[dict | ReIdentifyRequestBody] = None) -> None: + """ + Change the unique identifier of a relationship. + """ + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_reidentify_related_elements(relationship_guid, body)) + + @dynamic_catch + async def _async_retype_related_elements(self, relationship_guid: str, body: Optional[dict | ReTypeRequestBody] = None) -> None: + """ + Change the type of a relationship. Typically, this action is taken to move a relationship's type to either + a supertype (so the subtype can be deleted) or a new subtype (so additional properties can be added). The + request is routed to the repository that is the home of the relationship. Async version. + + Parameters + ---------- + relationship_guid : str + Unique identifier of the relationship to update. + body : dict | ReTypeRequestBody, optional + The name of the new type for the relationship. + + Notes + ----- + Sample JSON body: + { + "class" : "ReTypeRequestBody", + "externalSourceGUID" : "", + "externalSourceName" : "", + "forLineage" : false, + "forDuplicateProcessing" : false, + "effectiveTime" : "2024-01-01T00:00:00.000+00:00", + "newTypeName" : "NewTypeName" + } + """ + url = f"{self.command_root}/related-elements/{relationship_guid}/re-type" + await self._async_re_type_body_request(url, body) + + @dynamic_catch + def retype_related_elements(self, relationship_guid: str, body: Optional[dict | ReTypeRequestBody] = None) -> None: + """ + Change the type of a relationship. + """ + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_retype_related_elements(relationship_guid, body)) + + @dynamic_catch + async def _async_rehome_related_elements(self, relationship_guid: str, body: Optional[dict | ReHomeRequestBody] = None) -> None: + """ + Change the home repository of a relationship. This action is taken, for example, if the original home + repository becomes permanently unavailable, or if the user community updating this relationship moves to + working from a different repository in the open metadata repository cohort. Async version. + + Parameters + ---------- + relationship_guid : str + Unique identifier of the relationship to update. + body : dict | ReHomeRequestBody, optional + Details of the new home metadata collection. + + Notes + ----- + Sample JSON body: + { + "class" : "ReHomeRequestBody", + "externalSourceGUID" : "", + "externalSourceName" : "", + "forLineage" : false, + "forDuplicateProcessing" : false, + "effectiveTime" : "2024-01-01T00:00:00.000+00:00", + "newHomeMetadataCollectionId" : "new-home-collection-guid", + "newHomeMetadataCollectionName" : "New Home Collection Name" + } + """ + url = f"{self.command_root}/related-elements/{relationship_guid}/re-home" + await self._async_re_home_body_request(url, body) + + @dynamic_catch + def rehome_related_elements(self, relationship_guid: str, body: Optional[dict | ReHomeRequestBody] = None) -> None: + """ + Change the home repository of a relationship. + """ + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_rehome_related_elements(relationship_guid, body)) + @dynamic_catch async def _async_update_related_elements_effectivity(self, relationship_guid: str, body: Optional[dict | UpdateEffectivityDatesRequestBody] = None) -> None: """