Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions PYEGERIA_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,67 @@ 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).

**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.

---

### 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).

**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.

---

### 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**
Expand Down
3 changes: 3 additions & 0 deletions pyegeria/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
DeleteElementRequestBody,
DeleteRelationshipRequestBody,
NewSubscriptionTypeRequestBody,
OpenMetadataEnumDef,
OpenMetadataTypeDef,
OpenMetadataTypeDefPatch,
DeleteClassificationRequestBody,
InitialClassifications,
NewElementRequestBody,
Expand Down
43 changes: 43 additions & 0 deletions pyegeria/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
209 changes: 208 additions & 1 deletion pyegeria/omvs/metadata_expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -4375,3 +4376,209 @@ 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.

.. 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
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.
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))

@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.

.. 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
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.

.. 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
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.

.. 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
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))
Loading
Loading