From 262d5e0fc5326f324bb46a4d1aff1c1518d30a85 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Fri, 23 Oct 2026 16:52:08 -0400 Subject: [PATCH 1/2] Route removed endpoints to their Egeria replacements, deprecated (ISSUE-165) Per the Egeria team (Mandy Chessell, 2026-10-23), each obsolete method now warns (DeprecationWarning) and delegates, following the ISSUE-153 pattern: - analysis reports -> SurveyReport assets via AssetMaker (SurveyReport now inherits from Report); link/detach -> ReportSubject (link/unlink_report_subject). - add/remove_element_to/from_subject_area -> CollectionManager membership on the subject area (SubjectArea now inherits from Collection); new optional subject_area_guid, without which they raise an error naming the replacement. - find_governance_action_processes -> find_governance_definitions for type GovernanceActionProcess (now a GovernanceDefinition). - get_gov_def_in_context -> get_governance_definition_by_guid (context is returned by default); Dr.Egeria View Governance Definition Context too. - data classifications -> data value specifications and DataValueAssignment (a best-effort mapping; the Egeria team was not certain). 8 micro-tests; two generic by-name tests no longer list the delegating DataDiscovery wrapper (its target is listed). Not verified live: Egeria was being reset. Signed-off-by: Dan Wolfson --- PYEGERIA_ISSUES.md | 13 ++ md_processing/v2/governance.py | 4 +- pyegeria/omvs/classification_explorer.py | 57 ++++++-- pyegeria/omvs/data_designer.py | 57 ++++++-- pyegeria/omvs/data_discovery.py | 102 ++++++++----- pyegeria/omvs/governance_officer.py | 63 +++----- .../test_get_by_name_filter_string.py | 4 +- .../micro-tests/test_issue165_replacements.py | 138 ++++++++++++++++++ .../test_issue_135_by_name_body_only.py | 4 +- 9 files changed, 329 insertions(+), 113 deletions(-) create mode 100644 tests/micro-tests/test_issue165_replacements.py diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index 02bfbd5f..cff7c39a 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -319,6 +319,19 @@ Three other JAVA-MISSING rows are served live, so they are Java-scan false posit **Every call above returns a 404 today.** Before ISSUE-164, pyegeria reported a 404 as "unable to connect to the Egeria platform", so a caller could not tell "this API is gone" from "the server is down". +**Egeria team's answer (Mandy Chessell, 2026-10-23), and what pyegeria now does** (branch `fix/issue-165-replacements`). Each obsolete method is now a deprecated wrapper (`DeprecationWarning`) that delegates to the replacement, following the ISSUE-153 pattern; removal comes in a later release: + +| Obsolete | Why (Mandy) | Now delegates to | +|---|---|---| +| analysis-report create/update/delete/get/find | SurveyReport now inherits from Report, so it is an Asset | `AssetMaker.create_asset(["SurveyReportProperties"])` / `update_asset` / `delete_asset` / `get_asset_by_guid` / `get_assets_by_name` / `find_assets`, with type `SurveyReport`. A body naming `AnalysisReportProperties` is rewritten to `SurveyReportProperties`. | +| `link/detach_analysis_report_to/from_asset` | use the ReportSubject relationship, `/elements/{subjectGUID}/reports/{reportGUID}/attach` | `AssetMaker.link_report_subject` / `unlink_report_subject(asset_guid, report_guid)` | +| `add/remove_element_to/from_subject_area` | SubjectArea now inherits from Collection; use collection membership | `CollectionManager.add_to_collection` / `remove_from_collection(subject_area_guid, element_guid)`. The old methods never took the subject area, so they gain an optional `subject_area_guid`; without it they raise an error naming the replacement. | +| `find_governance_action_processes` | GovernanceActionProcess now inherits from GovernanceDefinition | `find_governance_definitions(..., metadata_element_type="GovernanceActionProcess")` | +| `get_gov_def_in_context` | no longer needed: OpenMetadataRootElement returns the context by default | `get_governance_definition_by_guid`. Dr.Egeria's `View Governance Definition Context` calls it directly. | +| data-classification create/update/link/detach | "probably the old data class operations", now data value specifications (the common root of DataClass and DataGrain); **Mandy was not certain** | `create/update_data_value_specification` (a body naming `DataClassificationProperties` is rewritten to `DataClassProperties`) and `link/detach_data_value_assignment(data_field_guid, spec_guid)` (`DataValueAssignmentProperties`). A best-effort mapping. | + +Still open: `get_platform_organization` (removed in `816076ab50`; not covered by the answer). Tests: `tests/micro-tests/test_issue165_replacements.py` (8). Not verified live: Egeria was reset at 21:10Z on 2026-10-23. + **Proposed (not made):** 1. ~~Repoint `link/detach_subject_area_hierarchy`~~ (done). 2. Mark the analysis-report, data-classification and `get_gov_def_in_context` methods deprecated, raising a clear "not supported by Egeria" error, before removing them in a later release. Dr.Egeria/CLI usage needs checking first. diff --git a/md_processing/v2/governance.py b/md_processing/v2/governance.py index 954d95f8..da94c815 100644 --- a/md_processing/v2/governance.py +++ b/md_processing/v2/governance.py @@ -772,7 +772,9 @@ async def apply_changes(self) -> str: return self.command.raw_block body = set_update_body(self.command.object_type, attributes) - struct = await self.client._async_get_gov_def_in_context(guid, body=body, output_format=output_format) + # get_governance_definition_by_guid returns the context by default; the old + # retrieve-context endpoint never existed on Egeria 6 (ISSUE-165). + struct = await self.client._async_get_governance_definition_by_guid(guid, body=body, output_format=output_format) if output_format.upper() == "DICT": return f"\n# Context Graph for {guid}\n\n```json\n{json.dumps(struct, indent=4)}\n```" diff --git a/pyegeria/omvs/classification_explorer.py b/pyegeria/omvs/classification_explorer.py index 0a6cbd79..e6cae990 100644 --- a/pyegeria/omvs/classification_explorer.py +++ b/pyegeria/omvs/classification_explorer.py @@ -12290,11 +12290,38 @@ def clear_semantic_assignment_classification( ) ) + # Egeria removed elements/{guid}/subject-area-member when SubjectArea became a Collection + # (Egeria 6a49a3e23f; ISSUE-165): membership is now the CollectionMembership relationship, which + # needs the subject area's GUID. These methods are deprecated; pass subject_area_guid to have + # them delegate, or call CollectionManager.add_to_collection / remove_from_collection directly. + + def _collection_client(self): + from pyegeria.omvs.collection_manager import CollectionManager + client = getattr(self, "_collection_mgr", None) + if client is None: + client = CollectionManager(self.view_server, self.platform_url, self.user_id, self.user_pwd, + token=self.token, timeout=getattr(self, "timeout", None)) + self._collection_mgr = client + return client + + @staticmethod + def _subject_area_moved(name: str, use: str, subject_area_guid: Optional[str]) -> None: + import warnings + warnings.warn(f"ClassificationExplorer.{name} is deprecated; subject-area membership is now collection " + f"membership - use CollectionManager.{use}(subject_area_guid, element_guid).", + DeprecationWarning, stacklevel=3) + if not subject_area_guid: + raise PyegeriaInvalidParameterException(additional_info={ + "reason": f"{name}: Egeria no longer has a subject-area-member classification. Pass " + f"subject_area_guid (membership is CollectionMembership on the subject area) or call " + f"CollectionManager.{use}(subject_area_guid, element_guid)."}) + async def _async_add_element_to_subject_area( self, element_guid: str, body: dict | NewClassificationRequestBody, timeout: int = default_timeout, + subject_area_guid: Optional[str] = None, ) -> None: """ Classify the element to assert that the definitions it represents are part of a subject area definition. @@ -12338,19 +12365,20 @@ async def _async_add_element_to_subject_area( } """ - url = ( - f"{self.classification_command_root}/elements/{element_guid}/subject-area-member" - ) - - await self._async_new_classification_request( - url, prop=["SubjectAreaMemberProperties"], body=body - ) + self._subject_area_moved("add_element_to_subject_area", "add_to_collection", subject_area_guid) + # The old body carried SubjectAreaMemberProperties (gone); membership takes no properties here. + rel_body = None + if isinstance(body, dict): + rel_body = {k: v for k, v in body.items() if k not in ("class", "properties")} + rel_body["class"] = "NewRelationshipRequestBody" + await self._collection_client()._async_add_to_collection(subject_area_guid, element_guid, rel_body) def add_element_to_subject_area( self, element_guid: str, body: dict | NewClassificationRequestBody, timeout: int = default_timeout, + subject_area_guid: Optional[str] = None, ) -> None: """ Classify the element to assert that the definitions it represents are part of a subject area definition. @@ -12399,6 +12427,7 @@ def add_element_to_subject_area( element_guid, body, timeout, + subject_area_guid, ) ) @@ -12409,6 +12438,7 @@ async def _async_remove_element_from_subject_area( for_duplicate_processing: bool = False, effective_time: Optional[str] = None, timeout: int = default_timeout, + subject_area_guid: Optional[str] = None, ) -> None: """ Remove the subject area designation from the identified element. Async version. @@ -12444,15 +12474,10 @@ async def _async_remove_element_from_subject_area( """ - url = ( - f"{self.classification_command_root}/elements/{element_guid}/subject-area-member" - f"/remove" - ) - - body = {"class": "DeleteClassificationRequestBody", "effectiveTime": effective_time, "forLineage": for_lineage, + self._subject_area_moved("remove_element_from_subject_area", "remove_from_collection", subject_area_guid) + body = {"class": "DeleteRelationshipRequestBody", "effectiveTime": effective_time, "forLineage": for_lineage, "forDuplicateProcessing": for_duplicate_processing} - - await self._async_delete_classification_request(url, body) + await self._collection_client()._async_remove_from_collection(subject_area_guid, element_guid, body) def remove_element_from_subject_area( self, @@ -12461,6 +12486,7 @@ def remove_element_from_subject_area( for_duplicate_processing: bool = False, effective_time: Optional[str] = None, timeout: int = default_timeout, + subject_area_guid: Optional[str] = None, ) -> None: """ Remove the subject area designation from the identified element. @@ -12506,6 +12532,7 @@ def remove_element_from_subject_area( for_duplicate_processing, effective_time, timeout, + subject_area_guid, ) ) diff --git a/pyegeria/omvs/data_designer.py b/pyegeria/omvs/data_designer.py index 397aaf07..959797fb 100644 --- a/pyegeria/omvs/data_designer.py +++ b/pyegeria/omvs/data_designer.py @@ -3294,6 +3294,27 @@ def get_data_grain_by_guid(self, guid: str = None, element_type: Optional[str] = self._async_get_data_value_specification_by_guid( guid=guid, element_type=element_type or "DataGrain", body=body, output_format=output_format, report_spec=report_spec, **kwargs)) + @staticmethod + def _warn_data_classification(name: str, use: str) -> None: + import warnings + warnings.warn(f"DataDesigner.{name} is deprecated; Egeria has no data-classification API - use {use} " + "(data value specifications cover DataClass and DataGrain).", DeprecationWarning, stacklevel=3) + + @staticmethod + def _as_data_class(body): + # DataClassificationProperties never existed in Egeria; the nearest type is DataClass. + if isinstance(body, dict) and isinstance(body.get("properties"), dict) \ + and body["properties"].get("class") == "DataClassificationProperties": + body = {**body, "properties": {**body["properties"], "class": "DataClassProperties"}} + return body + + @staticmethod + def _as_value_assignment(body): + if isinstance(body, dict) and isinstance(body.get("properties"), dict) \ + and body["properties"].get("class") == "DataFieldClassificationProperties": + body = {**body, "properties": {**body["properties"], "class": "DataValueAssignmentProperties"}} + return body + @dynamic_catch async def _async_create_data_classification(self, body: dict | NewElementRequestBody) -> str: """ @@ -3373,9 +3394,11 @@ async def _async_create_data_classification(self, body: dict | NewElementRequest """ - url = f"{self.ref_data_designer_command_base}/data-classifications" - - return await self._async_create_element_body_request(url, ["DataClassification"], body) + # Egeria has no data-classifications endpoints (never in 6.x Java; ISSUE-165). Per the Egeria team + # these were the old data-class operations, now data value specifications (the common root of + # DataClass and DataGrain) - a best-effort mapping, not confirmed against a live round trip. + self._warn_data_classification("create_data_classification", "create_data_value_specification") + return await self._async_create_data_value_specification(self._as_data_class(body)) @dynamic_catch def create_data_classification(self, body: dict | NewElementRequestBody) -> str: @@ -3525,9 +3548,11 @@ async def _async_update_data_classification(self, data_classification_guid: str, """ - url = f"{self.ref_data_designer_command_base}/data-classifications/{data_classification_guid}/update" - await self._async_update_element_body_request(url, ["DataClassification"], body) - logger.info(f"Data classification {data_classification_guid} updated.") + # Egeria has no data-classifications endpoints (never in 6.x Java; ISSUE-165). Per the Egeria team + # these were the old data-class operations, now data value specifications (the common root of + # DataClass and DataGrain) - a best-effort mapping, not confirmed against a live round trip. + self._warn_data_classification("update_data_classification", "update_data_value_specification") + await self._async_update_data_value_specification(data_classification_guid, self._as_data_class(body)) @dynamic_catch def update_data_classification(self, data_classification_guid: str, body: dict | UpdateElementRequestBody) -> None: @@ -3646,10 +3671,12 @@ async def _async_link_data_classification_to_data_field(self, data_classificatio """ - url = (f"{self.ref_data_designer_command_base}/data-classifications/{data_classification_guid}" - f"/data-fields/{data_field_guid}/attach") - await self._async_new_relationship_request(url, ["DataFieldClassificationProperties"], body) - logger.info(f"Data classification {data_classification_guid} linked to data field {data_field_guid}.") + # Egeria has no data-classifications endpoints (never in 6.x Java; ISSUE-165). Per the Egeria team + # these were the old data-class operations, now data value specifications (the common root of + # DataClass and DataGrain) - a best-effort mapping, not confirmed against a live round trip. + self._warn_data_classification("link_data_classification_to_data_field", "link_data_value_assignment(data_field_guid, spec_guid)") + await self._async_link_data_value_assignment(data_field_guid, data_classification_guid, + self._as_value_assignment(body)) @dynamic_catch @@ -3751,11 +3778,11 @@ async def _async_detach_data_classification_from_data_field(self, data_classific """ - url = (f"{self.ref_data_designer_command_base}/data-classifications/{data_classification_guid}" - f"/data-fields/{data_field_guid}/detach") - - await self._async_delete_relationship_request(url, body, cascade_delete) - logger.info(f"Data classification {data_classification_guid} detached from data field {data_field_guid}.") + # Egeria has no data-classifications endpoints (never in 6.x Java; ISSUE-165). Per the Egeria team + # these were the old data-class operations, now data value specifications (the common root of + # DataClass and DataGrain) - a best-effort mapping, not confirmed against a live round trip. + self._warn_data_classification("detach_data_classification_from_data_field", "detach_data_value_assignment(data_field_guid, spec_guid)") + await self._async_detach_data_value_assignment(data_field_guid, data_classification_guid, body) @dynamic_catch diff --git a/pyegeria/omvs/data_discovery.py b/pyegeria/omvs/data_discovery.py index 66d6aeca..13c4005d 100644 --- a/pyegeria/omvs/data_discovery.py +++ b/pyegeria/omvs/data_discovery.py @@ -1685,98 +1685,132 @@ def get_annotation_by_guid( ) # - # Analysis Reports + # Analysis Reports (deprecated) # + # Egeria removed the data-discovery/analysis-reports endpoints when SurveyReport became a Report, and + # so an Asset (Egeria e17cc09d44; ISSUE-165). These wrappers are deprecated for one release: they + # delegate to AssetMaker's asset calls for type SurveyReport, and to the ReportSubject relationship + # for linking a report to the asset it describes. + + def _asset_maker_client(self): + from pyegeria.omvs.asset_maker import AssetMaker + client = getattr(self, "_asset_maker", None) + if client is None: + client = AssetMaker(self.view_server, self.platform_url, self.user_id, self.user_pwd, + token=self.token, timeout=getattr(self, "timeout", None)) + self._asset_maker = client + return client + + @staticmethod + def _warn_analysis_report(name: str, use: str) -> None: + import warnings + warnings.warn(f"DataDiscovery.{name} is deprecated; Egeria no longer has analysis-report endpoints " + f"(a SurveyReport is now a Report asset) - use {use}.", DeprecationWarning, stacklevel=3) + + @staticmethod + def _as_survey_report(body): + # AnalysisReportProperties no longer exists in Egeria; the type is SurveyReport. + if isinstance(body, dict) and isinstance(body.get("properties"), dict) \ + and body["properties"].get("class") in (None, "AnalysisReportProperties"): + body = {**body, "properties": {**body["properties"], "class": "SurveyReportProperties"}} + return body @dynamic_catch async def _async_create_analysis_report(self, body: dict | NewElementRequestBody) -> str: - """Create an analysis report. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports" - return await self._async_create_element_body_request(url, ["AnalysisReportProperties"], body) + """Deprecated - use AssetMaker.create_asset(["SurveyReportProperties"], body). Async version.""" + self._warn_analysis_report("create_analysis_report", 'AssetMaker.create_asset(["SurveyReportProperties"], body)') + return await self._asset_maker_client()._async_create_asset(["SurveyReportProperties"], + self._as_survey_report(body)) def create_analysis_report(self, body: dict | NewElementRequestBody) -> str: - """Create an analysis report.""" + """Deprecated - use AssetMaker.create_asset(["SurveyReportProperties"], body).""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_create_analysis_report(body)) @dynamic_catch async def _async_update_analysis_report(self, report_guid: str, body: dict | UpdateElementRequestBody) -> None: - """Update an analysis report. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports/{report_guid}/update" - await self._async_update_element_body_request(url, ["AnalysisReportProperties"], body) + """Deprecated - use AssetMaker.update_asset. Async version.""" + self._warn_analysis_report("update_analysis_report", "AssetMaker.update_asset") + await self._asset_maker_client()._async_update_asset(report_guid, self._as_survey_report(body)) def update_analysis_report(self, report_guid: str, body: dict | UpdateElementRequestBody) -> None: - """Update an analysis report.""" + """Deprecated - use AssetMaker.update_asset.""" loop = asyncio.get_event_loop() loop.run_until_complete(self._async_update_analysis_report(report_guid, body)) @dynamic_catch async def _async_delete_analysis_report(self, report_guid: str, body: Optional[dict | DeleteElementRequestBody] = None) -> None: - """Delete an analysis report. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports/{report_guid}/delete" - await self._async_delete_element_request(url, body) + """Deprecated - use AssetMaker.delete_asset. Async version.""" + self._warn_analysis_report("delete_analysis_report", "AssetMaker.delete_asset") + await self._asset_maker_client()._async_delete_asset(report_guid, body) def delete_analysis_report(self, report_guid: str, body: Optional[dict | DeleteElementRequestBody] = None) -> None: - """Delete an analysis report.""" + """Deprecated - use AssetMaker.delete_asset.""" loop = asyncio.get_event_loop() loop.run_until_complete(self._async_delete_analysis_report(report_guid, body)) @dynamic_catch async def _async_get_analysis_reports_by_name(self, name: str | None = None, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: - """Get analysis reports by name. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports/by-name" - return await self._async_get_name_request(url, _type="AnalysisReport", _gen_output=self._generate_annotation_output, filter_string=name, start_from=start_from, page_size=page_size, **kwargs) + """Deprecated - use AssetMaker.get_assets_by_name(name, metadata_element_type_name="SurveyReport"). Async version.""" + self._warn_analysis_report("get_analysis_reports_by_name", + 'AssetMaker.get_assets_by_name(name, metadata_element_type_name="SurveyReport")') + return await self._asset_maker_client()._async_get_assets_by_name( + filter_string=name, metadata_element_type_name="SurveyReport", start_from=start_from, + page_size=page_size, **kwargs) def get_analysis_reports_by_name(self, name: str | None = None, start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: - """Get analysis reports by name.""" + """Deprecated - use AssetMaker.get_assets_by_name(name, metadata_element_type_name="SurveyReport").""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_get_analysis_reports_by_name(name, start_from, page_size, **kwargs)) @dynamic_catch async def _async_find_analysis_reports(self, search_string: str = "*", start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: - """Find analysis reports. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports/by-search-string" - return await self._async_find_request(url, _type="AnalysisReport", _gen_output=self._generate_annotation_output, search_string=search_string, start_from=start_from, page_size=page_size, **kwargs) + """Deprecated - use AssetMaker.find_assets(search_string, metadata_element_type="SurveyReport"). Async version.""" + self._warn_analysis_report("find_analysis_reports", + 'AssetMaker.find_assets(search_string, metadata_element_type="SurveyReport")') + return await self._asset_maker_client()._async_find_assets( + search_string=search_string, metadata_element_type="SurveyReport", start_from=start_from, + page_size=page_size, **kwargs) def find_analysis_reports(self, search_string: str = "*", start_from: int = 0, page_size: int = 100, **kwargs) -> list | str: - """Find analysis reports.""" + """Deprecated - use AssetMaker.find_assets(search_string, metadata_element_type="SurveyReport").""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_find_analysis_reports(search_string, start_from, page_size, **kwargs)) @dynamic_catch async def _async_get_analysis_report_by_guid(self, guid: str, **kwargs) -> dict | str: - """Get analysis report by GUID. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports/{guid}/retrieve" - return await self._async_get_guid_request(url, _type="AnalysisReport", _gen_output=self._generate_annotation_output, **kwargs) + """Deprecated - use AssetMaker.get_asset_by_guid. Async version.""" + self._warn_analysis_report("get_analysis_report_by_guid", "AssetMaker.get_asset_by_guid") + return await self._asset_maker_client()._async_get_asset_by_guid(guid, **kwargs) def get_analysis_report_by_guid(self, guid: str, **kwargs) -> dict | str: - """Get analysis report by GUID.""" + """Deprecated - use AssetMaker.get_asset_by_guid.""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_get_analysis_report_by_guid(guid, **kwargs)) # - # Linking + # Linking (deprecated): a report is linked to its subject with the ReportSubject relationship. # @dynamic_catch async def _async_link_analysis_report_to_asset(self, analysis_report_guid: str, asset_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None) -> None: - """Link an analysis report to an asset. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports/{analysis_report_guid}/assets/{asset_guid}/attach" - await self._async_new_relationship_request(url, body=body) + """Deprecated - use AssetMaker.link_report_subject(asset_guid, report_guid). Async version.""" + self._warn_analysis_report("link_analysis_report_to_asset", "AssetMaker.link_report_subject(asset_guid, report_guid)") + await self._asset_maker_client()._async_link_report_subject(asset_guid, analysis_report_guid, body) def link_analysis_report_to_asset(self, analysis_report_guid: str, asset_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None) -> None: - """Link an analysis report to an asset.""" + """Deprecated - use AssetMaker.link_report_subject(asset_guid, report_guid).""" loop = asyncio.get_event_loop() loop.run_until_complete(self._async_link_analysis_report_to_asset(analysis_report_guid, asset_guid, body)) @dynamic_catch async def _async_detach_analysis_report_from_asset(self, analysis_report_guid: str, asset_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None: - """Detach an analysis report from an asset. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/analysis-reports/{analysis_report_guid}/assets/{asset_guid}/detach" - await self._async_delete_relationship_request(url, body) + """Deprecated - use AssetMaker.unlink_report_subject(asset_guid, report_guid). Async version.""" + self._warn_analysis_report("detach_analysis_report_from_asset", "AssetMaker.unlink_report_subject(asset_guid, report_guid)") + await self._asset_maker_client()._async_unlink_report_subject(asset_guid, analysis_report_guid, body) def detach_analysis_report_from_asset(self, analysis_report_guid: str, asset_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None: - """Detach an analysis report from an asset.""" + """Deprecated - use AssetMaker.unlink_report_subject(asset_guid, report_guid).""" loop = asyncio.get_event_loop() loop.run_until_complete(self._async_detach_analysis_report_from_asset(analysis_report_guid, asset_guid, body)) diff --git a/pyegeria/omvs/governance_officer.py b/pyegeria/omvs/governance_officer.py index 58beedea..30cfbdb8 100644 --- a/pyegeria/omvs/governance_officer.py +++ b/pyegeria/omvs/governance_officer.py @@ -2355,30 +2355,17 @@ async def _async_find_governance_action_processes( PyegeriaException If there are issues in communications, message format, or Egeria errors. """ - url = ( - f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/" - f"{self.url_marker}/governance-action-processes/" - f"by-search-string") - - params = { - 'graph_query_depth': graph_query_depth, - 'search_string': search_string, - 'body': body, - 'starts_with': starts_with, - 'ends_with': ends_with, - 'ignore_case': ignore_case, - 'start_from': start_from, - 'page_size': page_size, - 'output_format': output_format, - 'report_spec': report_spec - } - params.update(kwargs) - params = {k: v for k, v in params.items() if v is not None or k == 'search_string'} - - response = await self._async_find_request(url, _type="GovernanceActionProcess", - _gen_output=self._generate_governance_definition_output, **params) - - return response + # Egeria removed governance-action-processes/by-search-string when GovernanceActionProcess became a + # GovernanceDefinition (Egeria d299b120f4; ISSUE-165): search the definitions for that type instead. + import warnings + warnings.warn("GovernanceOfficer.find_governance_action_processes is deprecated; use " + 'find_governance_definitions(..., metadata_element_type="GovernanceActionProcess").', + DeprecationWarning, stacklevel=2) + kwargs.setdefault("metadata_element_type", "GovernanceActionProcess") + return await self._async_find_governance_definitions( + search_string=search_string, body=body, starts_with=starts_with, ends_with=ends_with, + ignore_case=ignore_case, start_from=start_from, page_size=page_size, + graph_query_depth=graph_query_depth, output_format=output_format, report_spec=report_spec, **kwargs) @dynamic_catch def find_governance_action_processes( @@ -2823,26 +2810,14 @@ async def _async_get_gov_def_in_context(self, guid: str, if guid is None and "definition_guid" in kwargs: guid = kwargs.pop("definition_guid") - url = ( - f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/" - f"{self.url_marker}/governance-definitions/{guid}/retrieve-context") - - element_type = element_type if element_type else "GovernanceDefinition" - - params = { - 'graph_query_depth': graph_query_depth, - 'output_format': output_format, - 'report_spec': report_spec, - 'body': body - } - params.update(kwargs) - params = {k: v for k, v in params.items() if v is not None} - - response = await self._async_get_guid_request(url, _type=element_type, - _gen_output=self._generate_governance_definition_output, - **params) - - return response + # Egeria removed governance-definitions/{guid}/retrieve-context (it never existed on 6.x): the + # OpenMetadataRootElement returned by retrieve now carries the context by default (ISSUE-165). + import warnings + warnings.warn("GovernanceOfficer.get_gov_def_in_context is deprecated; get_governance_definition_by_guid " + "returns the context by default.", DeprecationWarning, stacklevel=2) + return await self._async_get_governance_definition_by_guid( + guid, element_type=element_type, body=body, graph_query_depth=graph_query_depth, + output_format=output_format, report_spec=report_spec, **kwargs) @dynamic_catch def get_gov_def_in_context(self, guid: str, diff --git a/tests/micro-tests/test_get_by_name_filter_string.py b/tests/micro-tests/test_get_by_name_filter_string.py index 2bb30938..df0d6e8d 100644 --- a/tests/micro-tests/test_get_by_name_filter_string.py +++ b/tests/micro-tests/test_get_by_name_filter_string.py @@ -11,7 +11,6 @@ import pytest -from pyegeria.omvs.data_discovery import DataDiscovery from pyegeria.omvs.schema_maker import SchemaMaker @@ -19,7 +18,8 @@ @pytest.mark.parametrize("cls, method", [ (SchemaMaker, "_async_get_schema_types_by_name"), (SchemaMaker, "_async_get_schema_attributes_by_name"), - (DataDiscovery, "_async_get_analysis_reports_by_name"), + # DataDiscovery.get_analysis_reports_by_name now delegates (deprecated) to + # AssetMaker.get_assets_by_name, which is listed; see test_issue165_replacements.py. ]) async def test_name_is_sent_as_filter_string(cls, method): captured = {} diff --git a/tests/micro-tests/test_issue165_replacements.py b/tests/micro-tests/test_issue165_replacements.py new file mode 100644 index 00000000..4e3d3e21 --- /dev/null +++ b/tests/micro-tests/test_issue165_replacements.py @@ -0,0 +1,138 @@ +""" +ISSUE-165: SDK methods whose Egeria endpoints were removed now delegate, deprecated, to the +replacements the Egeria team named (Mandy Chessell, 2026-10-23): + analysis reports -> SurveyReport assets (AssetMaker) and the ReportSubject relationship + subject-area membership -> CollectionMembership on the subject area + find_governance_action_processes -> find_governance_definitions(type GovernanceActionProcess) + get_gov_def_in_context -> get_governance_definition_by_guid (context returned by default) + data classifications -> data value specifications (best-effort mapping) +No live server: requests are captured at the ServerClient level, so delegated clients are seen too. +""" +import json + +import pytest + +from pyegeria.core._server_client import ServerClient +from pyegeria.core._exceptions import PyegeriaException + + +class _Response: + def json(self): + return {"class": "GUIDResponse", "guid": "new-guid", "elements": [], "element": {}, + "relatedHTTPCode": 200} + + +@pytest.fixture +def calls(monkeypatch): + captured: list = [] + + async def fake_make_request(self, method, url, payload=None, *args, **kwargs): + captured.append({"method": method, "url": url, + "body": json.loads(payload) if isinstance(payload, str) else payload}) + return _Response() + + monkeypatch.setattr(ServerClient, "check_connection", lambda self: "test") + monkeypatch.setattr(ServerClient, "_async_make_request", fake_make_request) + return captured + + +def _make(cls): + return cls("view-server", "https://localhost:1", "user", "pwd") + + +@pytest.mark.asyncio +async def test_analysis_reports_become_survey_report_assets(calls): + from pyegeria.omvs.data_discovery import DataDiscovery + dd = _make(DataDiscovery) + with pytest.warns(DeprecationWarning, match="SurveyReport is now a Report asset"): + await dd._async_create_analysis_report({ + "class": "NewElementRequestBody", + "properties": {"class": "AnalysisReportProperties", "qualifiedName": "q", "displayName": "r"}}) + assert "/api/open-metadata/asset-maker/assets" in calls[0]["url"] + assert calls[0]["body"]["properties"]["class"] == "SurveyReportProperties" + + with pytest.warns(DeprecationWarning): + await dd._async_find_analysis_reports("weekly") + assert calls[1]["url"].endswith("/asset-maker/assets/by-search-string") + assert calls[1]["body"]["metadataElementTypeName"] == "SurveyReport" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method, verb", [("_async_link_analysis_report_to_asset", "attach"), + ("_async_detach_analysis_report_from_asset", "detach")]) +async def test_analysis_report_link_is_report_subject(calls, method, verb): + from pyegeria.omvs.data_discovery import DataDiscovery + dd = _make(DataDiscovery) + with pytest.warns(DeprecationWarning, match="report_subject"): + await getattr(dd, method)("report-guid", "asset-guid") + # ReportSubject: /elements/{subjectGUID}/reports/{reportGUID}/attach - the asset is the subject + assert calls[0]["url"].endswith(f"/asset-maker/elements/asset-guid/reports/report-guid/{verb}") + + +@pytest.mark.asyncio +async def test_find_governance_action_processes_searches_definitions_of_that_type(calls): + from pyegeria.omvs.governance_officer import GovernanceOfficer + go = _make(GovernanceOfficer) + with pytest.warns(DeprecationWarning, match="find_governance_definitions"): + await go._async_find_governance_action_processes("onboarding") + assert calls[0]["url"].endswith("/governance-definitions/by-search-string") + assert calls[0]["body"]["metadataElementTypeName"] == "GovernanceActionProcess" + assert calls[0]["body"]["searchString"] == "onboarding" + + +@pytest.mark.asyncio +async def test_get_gov_def_in_context_is_a_plain_retrieve(calls): + from pyegeria.omvs.governance_officer import GovernanceOfficer + go = _make(GovernanceOfficer) + with pytest.warns(DeprecationWarning, match="returns the context by default"): + await go._async_get_gov_def_in_context("def-guid") + assert calls[0]["url"].endswith("/governance-definitions/def-guid/retrieve") + + +@pytest.mark.asyncio +async def test_data_classifications_map_to_data_value_specifications(calls): + from pyegeria.omvs.data_designer import DataDesigner + dsn = _make(DataDesigner) + with pytest.warns(DeprecationWarning, match="data value specifications"): + await dsn._async_create_data_classification({ + "class": "NewElementRequestBody", + "properties": {"class": "DataClassificationProperties", "qualifiedName": "q"}}) + assert calls[0]["url"].endswith("/data-designer/data-value-specifications") + assert calls[0]["body"]["properties"]["class"] == "DataClassProperties" + + with pytest.warns(DeprecationWarning): + await dsn._async_link_data_classification_to_data_field("spec-guid", "field-guid") + assert calls[1]["url"].endswith("/data-designer/elements/field-guid/data-value-specifications/spec-guid/attach") + assert calls[1]["body"]["properties"]["class"] == "DataValueAssignmentProperties" + + with pytest.warns(DeprecationWarning): + await dsn._async_detach_data_classification_from_data_field("spec-guid", "field-guid") + assert calls[2]["url"].endswith("/data-designer/elements/field-guid/data-value-specifications/spec-guid/detach") + + +@pytest.mark.asyncio +async def test_subject_area_membership_needs_the_subject_area_guid(calls): + from pyegeria.omvs.classification_explorer import ClassificationExplorer + ce = _make(ClassificationExplorer) + # Without the subject area's GUID there is nothing to delegate to: a clear error, no request. + with pytest.warns(DeprecationWarning): + with pytest.raises(PyegeriaException): + await ce._async_add_element_to_subject_area("el-guid", {"class": "NewClassificationRequestBody"}) + assert calls == [] + + with pytest.warns(DeprecationWarning, match="add_to_collection"): + await ce._async_add_element_to_subject_area("el-guid", {"class": "NewClassificationRequestBody"}, + subject_area_guid="sa-guid") + assert calls[0]["url"].endswith("/collections/sa-guid/members/el-guid/attach") + + with pytest.warns(DeprecationWarning, match="remove_from_collection"): + await ce._async_remove_element_from_subject_area("el-guid", subject_area_guid="sa-guid") + assert calls[1]["url"].endswith("/collections/sa-guid/members/el-guid/detach") + + +def test_dr_egeria_governance_context_no_longer_calls_the_removed_endpoint(): + import inspect + from md_processing.v2.governance import GovernanceContextProcessor + src = inspect.getsource(GovernanceContextProcessor) + assert "_async_get_governance_definition_by_guid" in src + assert "_async_get_gov_def_in_context" not in src diff --git a/tests/micro-tests/test_issue_135_by_name_body_only.py b/tests/micro-tests/test_issue_135_by_name_body_only.py index 5789d15c..319406bb 100644 --- a/tests/micro-tests/test_issue_135_by_name_body_only.py +++ b/tests/micro-tests/test_issue_135_by_name_body_only.py @@ -12,7 +12,6 @@ from pyegeria.omvs.asset_maker import AssetMaker from pyegeria.omvs.connection_maker import ConnectionMaker from pyegeria.omvs.data_designer import DataDesigner -from pyegeria.omvs.data_discovery import DataDiscovery from pyegeria.omvs.product_manager import ProductManager from pyegeria.omvs.schema_maker import SchemaMaker from pyegeria.omvs.solution_architect import SolutionArchitect @@ -27,7 +26,8 @@ (ConnectionMaker, "get_endpoints_by_name"), (DataDesigner, "get_data_fields_by_name"), (DataDesigner, "get_data_value_specifications_by_name"), - (DataDiscovery, "get_analysis_reports_by_name"), + # DataDiscovery.get_analysis_reports_by_name now delegates (deprecated) to + # AssetMaker.get_assets_by_name, which is listed; see test_issue165_replacements.py. (ProductManager, "get_governance_action_processes_by_name"), (SchemaMaker, "get_schema_types_by_name"), (SchemaMaker, "get_schema_attributes_by_name"), From f0f64fa3a08dfd9d9dd67259cb2a3ce7b42c4d1c Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Sat, 24 Oct 2026 15:14:07 -0400 Subject: [PATCH 2/2] ISSUE-165: record live verification of the replacement wrappers Signed-off-by: Dan Wolfson --- PYEGERIA_ISSUES.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index cff7c39a..4ebf2089 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -288,7 +288,7 @@ they're holding real port materialization until this exists server-side ### ISSUE-165: 19 SDK methods called endpoints Egeria doesn't serve: 13 removed upstream, 6 never existed (2 since repointed) -**Layer:** Pyegeria · **Status:** open, documented, not changed (owner to decide: remove/deprecate, repoint, or raise with the Egeria team) · **Found:** 2026-10-23 by `scripts/omvs_audit.py` JAVA-MISSING (ISSUE-164). +**Layer:** Pyegeria · **Status:** replacements implemented as deprecated wrappers and verified live (branch `fix/issue-165-replacements`); `get_platform_organization` still open · **Found:** 2026-10-23 by `scripts/omvs_audit.py` JAVA-MISSING (ISSUE-164). **Validation.** Each URL was checked three ways: 1. **No Spring mapping** in Egeria `main` 4486d1a7f1 (2026-10-23), the checkout `omvs_audit.py` reads. @@ -330,12 +330,19 @@ Three other JAVA-MISSING rows are served live, so they are Java-scan false posit | `get_gov_def_in_context` | no longer needed: OpenMetadataRootElement returns the context by default | `get_governance_definition_by_guid`. Dr.Egeria's `View Governance Definition Context` calls it directly. | | data-classification create/update/link/detach | "probably the old data class operations", now data value specifications (the common root of DataClass and DataGrain); **Mandy was not certain** | `create/update_data_value_specification` (a body naming `DataClassificationProperties` is rewritten to `DataClassProperties`) and `link/detach_data_value_assignment(data_field_guid, spec_guid)` (`DataValueAssignmentProperties`). A best-effort mapping. | -Still open: `get_platform_organization` (removed in `816076ab50`; not covered by the answer). Tests: `tests/micro-tests/test_issue165_replacements.py` (8). Not verified live: Egeria was reset at 21:10Z on 2026-10-23. +Still open: `get_platform_organization` (removed in `816076ab50`; not covered by the answer). Tests: `tests/micro-tests/test_issue165_replacements.py` (8). + +**Live on quickstart 9443, 2026-10-24** (Egeria 81beb8e5 after the reset; one peer-cleared run as `peterprofile` with throwaway `pyeg-test::issue165::` elements, all 5 deleted and read back as NotFound by GUID, and the prefix find was empty afterwards). Each wrapper reached its replacement and the replacement had the intended effect: +- `find_governance_action_processes("*")` returned 100 GovernanceActionProcess-family elements (GovernanceActionProcess plus its Provisioning/Deleting/Subscribing subtypes). +- `get_gov_def_in_context` returned the same element as `get_governance_definition_by_guid`, including `relatedActions`, `associatedGovernanceActions` and `subscribers`, which confirms Mandy's point that the context now comes by default. +- `add_element_to_subject_area` without `subject_area_guid` raised the guidance error. With it, the term was listed in the subject area's collection members; after `remove_element_from_subject_area` the subject area had no members. +- `create_analysis_report` created a `SurveyReport`; `get_analysis_report_by_guid` and `find_analysis_reports` returned it. After `link_analysis_report_to_asset(report, term)` the report showed the term under `reportSubjects`; after the detach it didn't. +- `create_data_classification` created a `DataClass`. After `link_data_classification_to_data_field` the field showed it under `assignedDataValueSpecifications`; after the detach it didn't. **Proposed (not made):** 1. ~~Repoint `link/detach_subject_area_hierarchy`~~ (done). -2. Mark the analysis-report, data-classification and `get_gov_def_in_context` methods deprecated, raising a clear "not supported by Egeria" error, before removing them in a later release. Dr.Egeria/CLI usage needs checking first. -3. Ask the Egeria team for the intended replacements for subject-area membership, governance-action-process search and the platform organization name. +2. ~~Mark the obsolete methods deprecated~~ (done, as delegating wrappers rather than errors, per the Egeria team's answer). +3. ~~Ask the Egeria team for the intended replacements~~ (answered, except the platform organization name). ---