From 4fd0ebbb6497bb106cea851585bf5c3e7dfa996a Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Mon, 5 Oct 2026 07:56:47 -0500 Subject: [PATCH 1/5] feat(metadata_expert): add/update/delete open metadata type definitions Adds add_enum_def, add_type_def, update_type_def, delete_type_def and delete_enum_def (Egeria-api-metadata-expert.http, open-metadata-types and open-metadata-attribute-types/enum-defs). The scripts/omvs_audit.py check for metadata-expert now reports 0 missing. The OpenMetadataEnumDef / OpenMetadataTypeDef / OpenMetadataTypeDefPatch models declare only the documented fields and use extra='allow', so the rest of these deep Egeria beans (relationship ends, enum elements, ...) is passed through instead of being silently dropped by PyegeriaModel's extra='ignore'. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Dan Wolfson --- pyegeria/models/__init__.py | 3 + pyegeria/models/models.py | 43 ++++ pyegeria/omvs/metadata_expert.py | 186 +++++++++++++++++- .../test_metadata_expert_type_defs.py | 142 +++++++++++++ 4 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 tests/micro-tests/test_metadata_expert_type_defs.py diff --git a/pyegeria/models/__init__.py b/pyegeria/models/__init__.py index 09a1b77b..322cdcb5 100644 --- a/pyegeria/models/__init__.py +++ b/pyegeria/models/__init__.py @@ -18,6 +18,9 @@ DeleteElementRequestBody, DeleteRelationshipRequestBody, NewSubscriptionTypeRequestBody, + OpenMetadataEnumDef, + OpenMetadataTypeDef, + OpenMetadataTypeDefPatch, DeleteClassificationRequestBody, InitialClassifications, NewElementRequestBody, diff --git a/pyegeria/models/models.py b/pyegeria/models/models.py index ea9622c6..0dc4d88d 100644 --- a/pyegeria/models/models.py +++ b/pyegeria/models/models.py @@ -940,3 +940,46 @@ class NewSubscriptionTypeRequestBody(RequestBody): service_level_objective_guid: str | None = None notification_interval: int | None = None # minutes monitored_resource_guids: list[str] | None = Field(None, alias="monitoredResourceGUIDs") + + +# --------------------------------------------------------------------------- +# Open metadata type definitions (MetadataExpert add/update/delete type defs). +# +# These mirror Egeria's OpenMetadataEnumDef / OpenMetadataTypeDef / OpenMetadataTypeDefPatch +# beans, which are deep graphs (attribute definitions, relationship ends, enum elements, +# links to other types by name). Only the fields the .http ground truth documents are +# declared; extra="allow" lets everything else in the bean (relationshipAttributes, endDefs, +# propagationRule, validEntityDefs, ...) pass through unchanged rather than being silently +# dropped by PyegeriaModel's extra="ignore" -- see the CLAUDE.md gotcha on request models. +# --------------------------------------------------------------------------- +class OpenMetadataEnumDef(PyegeriaModel): + model_config = ConfigDict(extra='allow') + class_: Annotated[Literal["OpenMetadataEnumDef"], Field(alias="class")] + name: str + description: str | None = None + element_defs: list[dict[str, Any]] | None = None + default_value: dict[str, Any] | None = None + + +class OpenMetadataTypeDef(PyegeriaModel): + """An entity, relationship or classification type definition (the bean's concrete subclass is + chosen by `class`). Links to other types - supertype, attribute types, relationship ends - + only need the name of the type.""" + model_config = ConfigDict(extra='allow') + class_: Annotated[Literal["OpenMetadataEntityDef", "OpenMetadataRelationshipDef", + "OpenMetadataClassificationDef"], Field(alias="class")] + name: str + description: str | None = None + super_type: dict[str, Any] | None = None + attribute_definitions: list[dict[str, Any]] | None = None + + +class OpenMetadataTypeDefPatch(PyegeriaModel): + """Carries only what changes in a type definition that was added through the API.""" + model_config = ConfigDict(extra='allow') + class_: Annotated[Literal["OpenMetadataTypeDefPatch"], Field(alias="class")] + type_def_guid: str + type_def_name: str + apply_to_version: int + description: str | None = None + attribute_definitions: list[dict[str, Any]] | None = None diff --git a/pyegeria/omvs/metadata_expert.py b/pyegeria/omvs/metadata_expert.py index 80b1e791..f2611624 100644 --- a/pyegeria/omvs/metadata_expert.py +++ b/pyegeria/omvs/metadata_expert.py @@ -17,7 +17,8 @@ SearchStringRequestBody, FilterRequestBody, GetRequestBody, ResultsRequestBody, SearchStringRequestBody as SearchStringBody, DeleteElementRequestBody, - DeleteRelationshipRequestBody) + DeleteRelationshipRequestBody, + OpenMetadataEnumDef, OpenMetadataTypeDef, OpenMetadataTypeDefPatch) from pyegeria.core.utils import body_slimmer, dynamic_catch from pyegeria.core._server_client import ServerClient, max_paging_size from pyegeria.core._globals import default_timeout, NO_ELEMENTS_FOUND @@ -4375,3 +4376,186 @@ def get_property_comparison_operator_list(self) -> list: """Retrieve the list of valid property comparison operators.""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_get_property_comparison_operator_list()) + + # + # Maintain the open metadata types that are defined through the API. These are validated against the types + # already defined, stored in the metadata access store's local repository, and announced to the rest of the + # cohort. Types from the open metadata archives cannot be changed this way. (Egeria-api-metadata-expert.http) + # + + async def _async_type_def_request(self, url: str, body: dict | Any, model: type, + params: dict | None = None) -> Any: + """POST a type-definition body (validated through `model`, with undeclared bean fields passed + through) and return the response's "guid" if it has one, else the whole response payload.""" + if isinstance(body, model): + validated = body + elif isinstance(body, dict): + validated = self._validate_body(model.model_validate, body) + else: + raise ValueError(f"body must be a dict or {model.__name__}") + response = await self._async_make_request( + "POST", url, validated.model_dump_json(indent=2, exclude_none=True), params=params) + payload = response.json() + return payload.get("guid", payload) if isinstance(payload, dict) else payload + + @dynamic_catch + async def _async_add_enum_def(self, body: dict | OpenMetadataEnumDef) -> str | dict: + """ + Add a new enum definition, which can then be used as the type of attributes in new type definitions. + Async version. + + Parameters + ---------- + body : dict | OpenMetadataEnumDef + The enum definition. + + Returns + ------- + str | dict + The unique identifier (GUID) of the new enum definition, or the raw response if it carries none. + + Notes + ----- + Sample JSON body: + { + "class" : "OpenMetadataEnumDef", + "name" : "CuisineType", + "description" : "The style of cooking that a recipe belongs to.", + "elementDefs" : [ + { "ordinal" : 0, "value" : "Unclassified", "description" : "The cuisine has not been recorded." }, + { "ordinal" : 99, "value" : "Other", "description" : "Another cuisine." } + ], + "defaultValue" : { "ordinal" : 0, "value" : "Unclassified", "description" : "..." } + } + """ + url = f"{self.command_root}/open-metadata-attribute-types/enum-defs" + return await self._async_type_def_request(url, body, OpenMetadataEnumDef) + + @dynamic_catch + def add_enum_def(self, body: dict | OpenMetadataEnumDef) -> str | dict: + """Add a new enum definition, which can then be used as the type of attributes in new type definitions.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_add_enum_def(body)) + + @dynamic_catch + async def _async_add_type_def(self, body: dict | OpenMetadataTypeDef) -> str | dict: + """ + Add a new type definition for an entity, relationship or classification. Links to other types - the + supertype, attribute types, relationship ends - only need the name of the type. Async version. + + Parameters + ---------- + body : dict | OpenMetadataTypeDef + The type definition; `class` is OpenMetadataEntityDef, OpenMetadataRelationshipDef or + OpenMetadataClassificationDef. + + Returns + ------- + str | dict + The unique identifier (GUID) of the new type definition, or the raw response if it carries none. + + Notes + ----- + Sample JSON body: + { + "class" : "OpenMetadataEntityDef", + "name" : "Recipe", + "description" : "A description of how to prepare a dish.", + "superType" : { "name" : "Referenceable" }, + "attributeDefinitions" : [ + { "attributeName" : "cuisine", + "attributeType" : { "class" : "OpenMetadataEnumDef", "name" : "CuisineType" }, + "attributeDescription" : "The style of cooking." }, + { "attributeName" : "servings", + "attributeType" : { "class" : "OpenMetadataPrimitiveDef", "name" : "int" }, + "attributeDescription" : "The number of people the recipe serves." } + ] + } + """ + url = f"{self.command_root}/open-metadata-types" + return await self._async_type_def_request(url, body, OpenMetadataTypeDef) + + @dynamic_catch + def add_type_def(self, body: dict | OpenMetadataTypeDef) -> str | dict: + """Add a new type definition for an entity, relationship or classification.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_add_type_def(body)) + + @dynamic_catch + async def _async_update_type_def(self, body: dict | OpenMetadataTypeDefPatch) -> None: + """ + Update a type definition that was added through the API. The patch names the version it applies to + and carries only what changes. Async version. + + Parameters + ---------- + body : dict | OpenMetadataTypeDefPatch + The patch. + + Notes + ----- + Sample JSON body: + { + "class" : "OpenMetadataTypeDefPatch", + "typeDefGUID" : "add guid here", + "typeDefName" : "Recipe", + "applyToVersion" : 1, + "attributeDefinitions" : [ + { "attributeName" : "preparationTimeMinutes", + "attributeType" : { "class" : "OpenMetadataPrimitiveDef", "name" : "int" }, + "attributeDescription" : "How long the recipe takes to prepare." } + ] + } + """ + url = f"{self.command_root}/open-metadata-types/update" + await self._async_type_def_request(url, body, OpenMetadataTypeDefPatch) + + @dynamic_catch + def update_type_def(self, body: dict | OpenMetadataTypeDefPatch) -> None: + """Update a type definition that was added through the API.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_update_type_def(body)) + + @dynamic_catch + async def _async_delete_type_def(self, type_def_guid: str, type_def_name: str) -> None: + """ + Delete a type definition that was added through the API. This fails while there are any instances of + the type, including soft-deleted ones, or while another type definition refers to it. Async version. + + Parameters + ---------- + type_def_guid : str + Unique identifier of the type definition. + type_def_name : str + Name of the type definition (the server checks it against the GUID). + """ + url = f"{self.command_root}/open-metadata-types/guid/{type_def_guid}/delete" + await self._async_make_request("POST", url, params={"typeDefName": type_def_name}) + + @dynamic_catch + def delete_type_def(self, type_def_guid: str, type_def_name: str) -> None: + """Delete a type definition that was added through the API.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_delete_type_def(type_def_guid, type_def_name)) + + @dynamic_catch + async def _async_delete_enum_def(self, enum_def_guid: str, enum_def_name: str) -> None: + """ + Delete an enum definition that was added through the API. This fails while any type definition has an + attribute of this type. Async version. + + Parameters + ---------- + enum_def_guid : str + Unique identifier of the enum definition. + enum_def_name : str + Name of the enum definition (the server checks it against the GUID). + """ + url = f"{self.command_root}/open-metadata-attribute-types/enum-defs/guid/{enum_def_guid}/delete" + await self._async_make_request("POST", url, params={"enumDefName": enum_def_name}) + + @dynamic_catch + def delete_enum_def(self, enum_def_guid: str, enum_def_name: str) -> None: + """Delete an enum definition that was added through the API.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_delete_enum_def(enum_def_guid, enum_def_name)) diff --git a/tests/micro-tests/test_metadata_expert_type_defs.py b/tests/micro-tests/test_metadata_expert_type_defs.py new file mode 100644 index 00000000..d89aff92 --- /dev/null +++ b/tests/micro-tests/test_metadata_expert_type_defs.py @@ -0,0 +1,142 @@ +""" +MetadataExpert type-definition maintenance: add/update/delete enum and type +definitions through the API (Egeria-api-metadata-expert.http). No live server: +the HTTP call is captured and the URL, verb, body and query params asserted. +""" +import json + +import pytest + +from pyegeria.omvs.metadata_expert import MetadataExpert + +ROOT = "/api/open-metadata/metadata-expert" + + +class _Response: + def __init__(self, payload): + self._payload = payload + + def json(self): + return self._payload + + +def _client(calls: list, payload: dict, monkeypatch) -> MetadataExpert: + monkeypatch.setattr(MetadataExpert, "check_connection", lambda self: "test") + client = MetadataExpert("view-server", "https://localhost:1", "user", "pwd") + + async def fake_make_request(method, url, payload_=None, *args, params=None, **kwargs): + calls.append({"method": method, "url": url, + "body": json.loads(payload_) if isinstance(payload_, str) else payload_, + "params": params}) + return _Response(payload) + + client._async_make_request = fake_make_request + return client + + +ENUM = { + "class": "OpenMetadataEnumDef", "name": "CuisineType", "description": "The style of cooking.", + "elementDefs": [{"ordinal": 0, "value": "Unclassified", "description": "d"}, + {"ordinal": 99, "value": "Other", "description": "o"}], + "defaultValue": {"ordinal": 0, "value": "Unclassified", "description": "d"}, +} +ENTITY = { + "class": "OpenMetadataEntityDef", "name": "Recipe", "description": "How to prepare a dish.", + "superType": {"name": "Referenceable"}, + "attributeDefinitions": [ + {"attributeName": "cuisine", "attributeType": {"class": "OpenMetadataEnumDef", "name": "CuisineType"}, + "attributeDescription": "The style of cooking."}], +} +PATCH = { + "class": "OpenMetadataTypeDefPatch", "typeDefGUID": "type-guid", "typeDefName": "Recipe", + "applyToVersion": 1, + "attributeDefinitions": [{"attributeName": "preparationTimeMinutes", + "attributeType": {"class": "OpenMetadataPrimitiveDef", "name": "int"}}], +} + + +@pytest.mark.asyncio +async def test_add_enum_def(monkeypatch): + calls: list = [] + client = _client(calls, {"class": "GUIDResponse", "guid": "enum-guid"}, monkeypatch) + + guid = await client._async_add_enum_def(ENUM) + + assert guid == "enum-guid" + assert calls[0]["method"] == "POST" + assert calls[0]["url"].endswith(f"{ROOT}/open-metadata-attribute-types/enum-defs") + assert calls[0]["body"] == ENUM + + +@pytest.mark.asyncio +async def test_add_type_def_keeps_nested_and_undeclared_fields(monkeypatch): + calls: list = [] + client = _client(calls, {"guid": "type-guid"}, monkeypatch) + body = {**ENTITY, "relationshipAttributes": [{"relationshipType": {"name": "Foo"}}]} + + guid = await client._async_add_type_def(body) + + assert guid == "type-guid" + assert calls[0]["url"].endswith(f"{ROOT}/open-metadata-types") + # nothing the caller supplied is silently dropped (extra="allow") + assert calls[0]["body"] == body + + +@pytest.mark.asyncio +async def test_add_type_def_without_guid_returns_payload(monkeypatch): + client = _client([], {"class": "VoidResponse", "relatedHTTPCode": 200}, monkeypatch) + + assert await client._async_add_type_def(ENTITY) == {"class": "VoidResponse", "relatedHTTPCode": 200} + + +@pytest.mark.asyncio +async def test_update_type_def(monkeypatch): + calls: list = [] + client = _client(calls, {"class": "VoidResponse"}, monkeypatch) + + result = await client._async_update_type_def(PATCH) + + assert result is None + assert calls[0]["url"].endswith(f"{ROOT}/open-metadata-types/update") + assert calls[0]["body"] == PATCH + + +@pytest.mark.asyncio +async def test_delete_type_def_sends_name_as_query_param_and_no_body(monkeypatch): + calls: list = [] + client = _client(calls, {"class": "VoidResponse"}, monkeypatch) + + await client._async_delete_type_def("type-guid", "Recipe") + + assert calls[0]["method"] == "POST" + assert calls[0]["url"].endswith(f"{ROOT}/open-metadata-types/guid/type-guid/delete") + assert calls[0]["params"] == {"typeDefName": "Recipe"} + assert calls[0]["body"] is None + + +@pytest.mark.asyncio +async def test_delete_enum_def_sends_name_as_query_param_and_no_body(monkeypatch): + calls: list = [] + client = _client(calls, {"class": "VoidResponse"}, monkeypatch) + + await client._async_delete_enum_def("enum-guid", "CuisineType") + + assert calls[0]["url"].endswith(f"{ROOT}/open-metadata-attribute-types/enum-defs/guid/enum-guid/delete") + assert calls[0]["params"] == {"enumDefName": "CuisineType"} + assert calls[0]["body"] is None + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method, bad_body", [ + ("_async_add_enum_def", {"class": "OpenMetadataEnumDef"}), # name missing + ("_async_add_type_def", {"class": "NotATypeDef", "name": "x"}), # unknown class + ("_async_update_type_def", {"class": "OpenMetadataTypeDefPatch", "typeDefName": "x"}), # guid/version missing +]) +async def test_invalid_bodies_are_rejected_before_any_request(method, bad_body, monkeypatch): + calls: list = [] + client = _client(calls, {}, monkeypatch) + + with pytest.raises(Exception): + await getattr(client, method)(bad_body) + + assert calls == [] From 47764513c3e2614fb90cf4a6b1492e801483d8d8 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Mon, 5 Oct 2026 09:02:08 -0500 Subject: [PATCH 2/5] docs(metadata_expert): record the live findings on update_type_def and delete_enum_def Live verification (2026-10-05): add_enum_def and add_type_def return the new GUID and the read-back is intact; delete_type_def works. update_type_def is rejected by the server (updatedBy is never set on the patch -- Egeria bug, ISSUE-124) and delete_enum_def answers 500 'unknown TypeDef' (ISSUE-125), leaving the throwaway enum behind. Docstrings now say so. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Dan Wolfson --- PYEGERIA_ISSUES.md | 49 ++++++++++++++++++++++++++++++++ pyegeria/omvs/metadata_expert.py | 13 +++++++++ 2 files changed, 62 insertions(+) diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index 06c91aa3..00da2b48 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -1257,6 +1257,55 @@ queries, rendering "Not measured" instead of `0` (odpi/egeria-workspaces --- +### ISSUE-124: `updateTypeDef` (Metadata Expert `open-metadata-types/update`) can never succeed -- the server builds the patch without `updatedBy`, which the repository services require + +**Layer:** Egeria Server (not pyegeria) · **Status:** open, not yet reported upstream · **Found:** +2026-10-05, live verification of `MetadataExpert.update_type_def` (PR #428). + +**What:** posting a valid `OpenMetadataTypeDefPatch` (type added through the API moments earlier, +`applyToVersion` 1) answers a 500 wrapping +`OMRS-REPOSITORY-400-069 Method updateTypeDef has detected that a TypeDef patch from qs-metadata-store has the +mandatory field updatedBy set to null ... TypeDefPatch{typeDefGUID=..., typeDefName=`PyegeriaTmpRecipe`, +applyToVersion=1, updateToVersion=2, newVersionName=`2.0`, updatedBy=`null`, ...}`. The request itself arrived +intact (the attribute definition is visible in the message). + +**Cause, from Egeria's source (`origin/main`, 2026-10-01):** `OpenMetadataStoreRESTServices.updateTypeDef` +calls `converter.getTypeDefPatch(requestBody, methodName)`; `OMRSTypeDefConverter.getTypeDefPatch` takes no user +id and has no `setUpdatedBy` call, while `OMRSMetadataCollection.updateTypeDef` rejects a patch whose +`updatedBy` is null. `OpenMetadataTypeDefPatch` has no `updatedBy` field either, so a client cannot supply it. +Not a pyegeria defect: the SDK sends the documented body. Fix belongs upstream (pass `userId` to the converter and +set it). + +**Effect here:** `update_type_def` is correct but unusable against current Egeria; its docstring says so. + +--- + +### ISSUE-125: `deleteEnumDef` answers 500 "unknown TypeDef" for an enum the server lists -- an enum added through the API cannot be removed + +**Layer:** Egeria Server (not pyegeria) · **Status:** open, not yet reported upstream · **Found:** +2026-10-05, live verification of `MetadataExpert.delete_enum_def` (PR #428). **Left a stray type behind.** + +**What:** after `add_enum_def` (returned GUID `118441be-...`), `add_type_def` of an entity type using it, and a +successful `delete_type_def` of that entity type, `delete_enum_def(guid, name)` answered +`OMRS-CONTENT-MANAGER-500-001 The repository content manager method getAttributeTypeDef has detected an unknown +TypeDef 118441be-... from qs-metadata-store on behalf of method deleteAttributeTypeDef` ("Open up a Github issue"). +Yet `ValidMetadataManager.get_attribute_types()` still lists the enum, so the server knows it by one route and +not the other. + +**From Egeria's source:** `OMRSRepositoryContentManager.getAttributeTypeDef(sourceName, guid, methodName)` +throws `BAD_TYPEDEF` when the GUID is not in `knownAttributeTypeDefGUIDs`. Hypothesis, not traced: an enum added +at runtime is registered in the name-keyed map but not that GUID-keyed one, or deleting the type that uses it +drops the GUID entry. + +**Left on the shared dev platform:** enum `PyegeriaTmpCuisineType` (`118441be-6e03-4442-96c6-e431f75fcb3f`), +unused, announced to the cohort. Harmless but visible in type listings. A retry after the platform's next restart +is worthwhile (the .http says API-defined types survive a restart when the repository is persistent, so the content +manager may then know it by GUID). + +**Workaround:** none through the API. Avoid creating throwaway enums on a shared platform. + +--- + ### ISSUE-122: `AssetMaker.get_catalog_targets` / `get_catalog_target` send `metadataElementTypeName="CatalogTarget"` (a relationship type) — server rejects with OMAG-COMMON-400-019, surfaced as SERVER_ERROR_500 **Status: fixed on branch `fix/issue-122-catalog-target-type` (2026-10-04), pending PR/merge** diff --git a/pyegeria/omvs/metadata_expert.py b/pyegeria/omvs/metadata_expert.py index f2611624..c14cbfed 100644 --- a/pyegeria/omvs/metadata_expert.py +++ b/pyegeria/omvs/metadata_expert.py @@ -4487,6 +4487,13 @@ async def _async_update_type_def(self, body: dict | OpenMetadataTypeDefPatch) -> Update a type definition that was added through the API. The patch names the version it applies to and carries only what changes. Async version. + .. warning:: + Against Egeria as of 2026-10-05 (including its main branch) every patch is rejected by the server + with OMRS-REPOSITORY-400-069, "mandatory field updatedBy set to null": the server builds the patch + with a converter that is never given the calling user. Nothing a client sends can supply it. See + PYEGERIA_ISSUES.md, ISSUE-124. This method matches the documented API and will work once that + is fixed upstream. + Parameters ---------- body : dict | OpenMetadataTypeDefPatch @@ -4544,6 +4551,12 @@ async def _async_delete_enum_def(self, enum_def_guid: str, enum_def_name: str) - Delete an enum definition that was added through the API. This fails while any type definition has an attribute of this type. Async version. + .. warning:: + Observed live (2026-10-05) to fail with a 500, OMRS-CONTENT-MANAGER-500-001 "unknown TypeDef", even + after the only type using the enum had been deleted and while the enum was still listed by + ``get_attribute_types``: an enum added through the API could not be removed again. Delete enum + definitions only if you can live with that. See PYEGERIA_ISSUES.md, ISSUE-125. + Parameters ---------- enum_def_guid : str From e9ce5ffed32cc70d6ae84ed538ee79406227c8b5 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Mon, 5 Oct 2026 10:01:33 -0500 Subject: [PATCH 3/5] docs(issues): ISSUE-125 still fails after the platform redeploy Re-tested live on the 2026-10-05 rebuilt platform: the stray enum survives the restart and delete_enum_def fails with the identical OMRS-CONTENT-MANAGER-500-001. Closes the 'retry after a restart' idea. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Dan Wolfson --- PYEGERIA_ISSUES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index 00da2b48..dde8539c 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -1302,6 +1302,12 @@ unused, announced to the cohort. Harmless but visible in type listings. A retry is worthwhile (the .http says API-defined types survive a restart when the repository is persistent, so the content manager may then know it by GUID). +**Re-tested 2026-10-05 after the platform was rebuilt and restarted** (image built 14:19Z from egeria main +`449ad06894`, `egeria-main` restarted 14:39Z): the enum survived the restart and is still listed, and +`delete_enum_def` fails with the identical `OMRS-CONTENT-MANAGER-500-001`. So a restart does not repopulate the +GUID map; the persisted type is loaded without being registered under its GUID (or the delete looks in the wrong +map). The retry-after-restart idea above is closed. + **Workaround:** none through the API. Avoid creating throwaway enums on a shared platform. --- From 36e6454a11e7a446a56466f3045d4efd65fcaefb Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Mon, 5 Oct 2026 10:08:09 -0500 Subject: [PATCH 4/5] docs(issues): ISSUE-124 still fails after the platform redeploy Re-tested live on the 2026-10-05 rebuilt platform: a patch to a throwaway entity type is rejected with the identical 'updatedBy set to null' error. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Dan Wolfson --- PYEGERIA_ISSUES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index dde8539c..10036dfd 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -1276,6 +1276,12 @@ id and has no `setUpdatedBy` call, while `OMRSMetadataCollection.updateTypeDef` Not a pyegeria defect: the SDK sends the documented body. Fix belongs upstream (pass `userId` to the converter and set it). +**Re-tested 2026-10-05 after the platform was rebuilt and restarted** (image built 14:19Z from egeria main +`449ad06894`): a throwaway entity type (primitive attribute only) was added and patched with one new attribute. +The server rejected the patch with the identical `OMRS-REPOSITORY-400-069 ... updatedBy set to null` +(`TypeDefPatch{... applyToVersion=1, updateToVersion=2, newVersionName=2.0, updatedBy=null ...}`). The type was +then deleted and verified gone. **Confirmed still present on the rebuilt platform.** + **Effect here:** `update_type_def` is correct but unusable against current Egeria; its docstring says so. --- From 3dfb0224fd9ba3bc370b775342f0d8a1c8e4553b Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Mon, 5 Oct 2026 10:32:03 -0500 Subject: [PATCH 5/5] docs(metadata_expert): warn on add_enum_def that the enum cannot be removed (ISSUE-125) The warning belongs on the method a caller reaches first, not only on delete_enum_def. Also notes on add_type_def that only entity types have been verified live. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Dan Wolfson --- pyegeria/omvs/metadata_expert.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyegeria/omvs/metadata_expert.py b/pyegeria/omvs/metadata_expert.py index c14cbfed..f4af34e8 100644 --- a/pyegeria/omvs/metadata_expert.py +++ b/pyegeria/omvs/metadata_expert.py @@ -4404,6 +4404,11 @@ async def _async_add_enum_def(self, body: dict | OpenMetadataEnumDef) -> str | d Add a new enum definition, which can then be used as the type of attributes in new type definitions. Async version. + .. warning:: + An enum added through the API cannot be removed again: ``delete_enum_def`` currently fails against + Egeria with a 500 "unknown TypeDef" (an Egeria server bug, ISSUE-125, reproduced on Egeria main + 449ad06894, and the enum survives a restart). Do not create throwaway enums on a shared platform. + Parameters ---------- body : dict | OpenMetadataEnumDef @@ -4433,7 +4438,8 @@ async def _async_add_enum_def(self, body: dict | OpenMetadataEnumDef) -> str | d @dynamic_catch def add_enum_def(self, body: dict | OpenMetadataEnumDef) -> str | dict: - """Add a new enum definition, which can then be used as the type of attributes in new type definitions.""" + """Add a new enum definition, which can then be used as the type of attributes in new type definitions. + Note: it cannot be removed again through the API (ISSUE-125); see the async version's warning.""" loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_add_enum_def(body)) @@ -4443,6 +4449,10 @@ async def _async_add_type_def(self, body: dict | OpenMetadataTypeDef) -> str | d Add a new type definition for an entity, relationship or classification. Links to other types - the supertype, attribute types, relationship ends - only need the name of the type. Async version. + .. note:: + Verified against a live server for entity types (with primitive and enum attributes). Relationship and + classification definitions go through the same code and models but have not been exercised live. + Parameters ---------- body : dict | OpenMetadataTypeDef