Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5b988d3
Fix role-appointment class names and mismatched delete/detach bodies …
dwolfson Oct 23, 2026
1a6db16
omvs_audit: check request bodies and class names against Egeria's Jav…
dwolfson Oct 23, 2026
f3ea671
Document the omvs_audit Java sweep findings (ISSUE-164); correct ISSU…
dwolfson Oct 23, 2026
42ef8b5
omvs_audit: field-level checks for silently dropped request fields (I…
dwolfson Oct 23, 2026
abf71a5
ISSUE-164: record field-level sweep findings against Egeria main 4486…
dwolfson Oct 23, 2026
3bb233c
Send anchor scope and find options under Egeria's field names (ISSUE-…
dwolfson Oct 23, 2026
046b36e
omvs_audit: read serialization/validation aliases; tag JAVA-MISSING w…
dwolfson Oct 23, 2026
b8b4567
Fix always-400 deletes and wrong body/properties class names (ISSUE-164)
dwolfson Oct 23, 2026
a2db026
omvs_audit: accept registered @JsonSubTypes names and aliases as vali…
dwolfson Oct 23, 2026
13c17e3
Add missing Egeria fields to request models; fix Dr.Egeria/CLI body k…
dwolfson Oct 23, 2026
e11dc1e
ISSUE-164: record live verification and the Egeria 6 ElementHeader shape
dwolfson Oct 23, 2026
340c2a0
ISSUE-164: scope the ElementHeader note to the view-service shape; Me…
dwolfson Oct 23, 2026
85e54cd
Add new Egeria APIs: usage activity, member popularity, undo/restore,…
dwolfson Oct 23, 2026
58dfaef
ISSUE-165: document the 19 SDK methods calling endpoints Egeria doesn…
dwolfson Oct 23, 2026
238aeeb
omvs_audit: tag JAVA-MISSING rows whose path moved to another service…
dwolfson Oct 23, 2026
3debc7d
Repoint subject-area hierarchy link/detach to Egeria's URLs (ISSUE-165)
dwolfson Oct 23, 2026
3d7cfd5
Record live verification of the new APIs; document count-reset on act…
dwolfson Oct 23, 2026
80f5f6f
Merge upstream/main (#482, ISSUE-162) into fix/role-appointment-class…
dwolfson Oct 23, 2026
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
43 changes: 40 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,49 @@ after a real defect slipped through an earlier, weaker version of this script:
trailing verbs (`/attach` vs `/detach`) or the leading service segment; an
earlier version did, and that masked exactly the bugs worth finding.
- **BODY** — compares the request-body class (`FilterRequestBody` vs
`ResultsRequestBody`, etc.). Heuristic: it takes the first `*RequestBody`
identifier in the method, which may be a signature annotation, so treat body
findings as lower-confidence than verb/path.
`ResultsRequestBody`, etc.). When the method delegates to a shared
`_async_*_request` helper it uses the classes that helper accepts (what is
actually sent); otherwise it falls back to the method's own `body`
annotation, which is lower-confidence.
- **LINT** — structural URL defects detectable without ground truth: double
slashes, a path parameter with no separator before it (`/agreements{guid}`),
and underscores in path segments (Egeria uses hyphens).
- **RENAMED** — a method found by verb+path under an unrelated name now gets
the verb/body checks too. It used to be reported as RENAMED and skipped,
which is how the IT-profile-role and profile-identity detaches (ISSUE-163)
sent the wrong body unchecked.
- **Java checks** (needs `~/localGit/egeria-v6/egeria`, or `--java-dir` /
`$EGERIA_JAVA_DIR`; `--no-java` skips) — the Spring `@RequestBody` types are
a second ground truth, added after ISSUE-163, where the `.http` file named
the same wrong class as the SDK. **JAVA-BODY REJECTED** (defect) only fires
when the declared type sits under a `@JsonTypeInfo(property="class")` base.
Among request bodies that's only `DeleteRequestBody` →
`Delete{Element,Relationship,Classification}RequestBody`, where a sibling
class is a 400. Elsewhere Egeria ignores `"class"` and maps fields by name,
so a mismatch there is **CLASS-IGNORED** (review: only dropped fields
matter). **UNKNOWN-CLASS** (defect) flags any `"…Properties"` /
`"…RequestBody"` string in `pyegeria/omvs` that isn't an Egeria Java class.
The properties bases are polymorphic, so sending one is a 400; this is what
caught `PersonAppointmentProperties`. **JAVA-MISSING** (review) lists SDK
endpoints with no Spring mapping, meaning no URL, not a missing class. Each row is tagged
"MOVED?" when the same path tail is served under another service, and with `--live`
as served or absent on the running platform. ISSUE-165 records the validated list.
- **Field-level checks** — these catch the "call succeeds but silently ignores
an option" bugs that no class check can see.
- **FIELD-DROP** (defect): a method passes an argument whose JSON key the
server-side class (and its ancestors) doesn't declare. The helper-argument
→ JSON-key map is read from each helper's dict literals, following simple
local aliases, so a parameter counts as dropped only if *every* key it can
fill is dropped.
- **CLIENT-DROP** (defect): a dict in our own code names a pyegeria model
via `"class"` but uses a key that model reads neither as alias nor as field
name, so Pydantic discards it before sending (e.g. `"zoneFilter"` vs
`governanceZoneFilter`).
- **MODEL-FIELD gaps** (review): pyegeria model fields the Java class
lacks, with the methods that use each pair. A near-miss spelling there
(`anchorScopeGuids` vs `anchorScopeGUIDs`) is a bug.
- `adapters/` is excluded from the Java index, because connector-internal
DTOs reuse API class names.

URL extraction is AST-based, not regex — multi-line f-strings silently produced
empty paths under the regex version, which made whole modules look clean.
Expand Down
209 changes: 205 additions & 4 deletions PYEGERIA_ISSUES.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions commands/cat/glossary_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def create_term(server, url, userid, password, glossary_name, term_name, summary
"class": "NewElementRequestBody",
"parentGUID" : glossary_guid,
"parentRelationshipTypeName" : "CollectionMembership",
"anchorScopeGUID" : glossary_guid,
"anchorScopeGUIDs" : [glossary_guid],
"isOwnAnchor": True,
"parentAtEnd1": True,
"properties": {
Expand Down Expand Up @@ -178,7 +178,7 @@ def update_term(server, url, userid, password, term_name, summary, description,
body = {
"class": "UpdateElementRequestBody",
"mergeUpdate": True,
"elementProperties": {
"properties": {
"class": "GlossaryTermProperties",
"displayName": term_name,
"summary": summary,
Expand Down
4 changes: 2 additions & 2 deletions commands/my/todo_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def change_todo_status(server, url, userid, password, timeout, todo_guid, new_st
if new_status not in ACTIVITY_STATUS:
raise PyegeriaInvalidParameterException(context={"reason":"Invalid activity status"})
body = {
"class": "UpdataElementRequestBody",
"class": "UpdateElementRequestBody",
"properties": {
"class": "ToDoProperties",
"activityStatus": new_status
Expand Down Expand Up @@ -234,7 +234,7 @@ def mark_todo_complete(server, url, userid, password, timeout, todo_guid):

m_client.create_egeria_bearer_token()
body = {
"class": "UpdataElementRequestBody",
"class": "UpdateElementRequestBody",
"properties": {
"class": "ToDoProperties",
"activityStatus": "COMPLETED",
Expand Down
8 changes: 4 additions & 4 deletions md_processing/md_processing_utils/common_md_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def set_find_body(object_type: str, attributes: dict)->dict:
"class": "SearchStringRequestBody",
"searchString": search_string,
"startsWith": attributes.get('Start With', {}).get('value', True),
"endWith": attributes.get('End With', {}).get('value', False),
"endsWith": attributes.get('End With', {}).get('value', False),
"ignoreCase": attributes.get('Ignore Case', {}).get('value', False),
"limitResultsByStatus": attributes.get('Limit Results By Status', {}).get('value', []),
"startFrom": int(attributes.get('Start From', {}).get('value', 0)),
Expand Down Expand Up @@ -294,7 +294,6 @@ def set_create_body(object_type: str, attributes: dict)->dict:
"externalSourceGUID": attributes.get('External Source GUID', {}).get('guid', None),
"externalSourceName": attributes.get('External Source Name', {}).get('value', None),
"effectiveTime": attributes.get('Effective Time', {}).get('value', None),
"governanceZoneFilter" : attributes.get('Governance Zone Filter', {}).get('value', None),
"forLineage": attributes.get('For Lineage', {}).get('value', False),
"forDuplicateProcessing": attributes.get('For Duplicate Processing', {}).get('value', False),
"anchorGUID": attributes.get('Anchor ID', {}).get('guid', None),
Expand All @@ -303,7 +302,9 @@ def set_create_body(object_type: str, attributes: dict)->dict:
"parentRelationshipTypeName": attributes.get('Parent Relationship Type Name', {}).get('value', None) or attributes.get('Parent Relationship Type', {}).get('value', None),
"parentRelationshipProperties": attributes.get('Parent Relationship Attributes', {}).get('value', None) or attributes.get('Parent Relationship Properties', {}).get('value', None),
"parentAtEnd1": attributes.get('Parent at End1', {}).get('value', True),
"anchorScopeGUID": attributes.get('Anchor Scope ID', {}).get('guid', None),
# Egeria reads a list, anchorScopeGUIDs; the singular key was dropped (ISSUE-164)
"anchorScopeGUIDs": ([attributes['Anchor Scope ID']['guid']]
if attributes.get('Anchor Scope ID', {}).get('guid') else None),
"properties": "",
"initialClassifications": {}}

Expand All @@ -324,7 +325,6 @@ def set_update_body(object_type: str, attributes: dict)->dict:
"class" : "UpdateElementRequestBody",
"externalSourceGUID": attributes.get('External Source GUID', {}).get('guid', None),
"externalSourceName": attributes.get('External Source Name', {}).get('value', None),
"governanceZoneFilter": attributes.get('Governance Zone Filter', {}).get('value', None),
"effectiveTime": attributes.get('Effective Time', {}).get('value', None),
"forLineage": attributes.get('For Lineage', {}).get('value', False),
"forDuplicateProcessing": attributes.get('For Duplicate Processing', {}).get('value', False),
Expand Down
2 changes: 1 addition & 1 deletion md_processing/v2/actor_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ async def apply_changes(self) -> str:
# Canonical: IT Profile Role, IT Profile
role_guid = attributes.get("IT Profile Role", {}).get("guid")
profile_guid = attributes.get("IT Profile", {}).get("guid")
link_coro = self.client.actor_manager._async_link_it_profile_role_to_profile
link_coro = self.client.actor_manager._async_link_it_profile_role_to_it_profile
detach_coro = self.client.actor_manager._async_detach_it_profile_role_from_it_profile
else:
raise PyegeriaException(f"Unsupported role appointment type: {object_type}")
Expand Down
4 changes: 2 additions & 2 deletions md_processing/v2/engine_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
Both commands call pyegeria's `AutomatedCuration` client directly
(`initiate_engine_action` / `cancel_engine_action`) rather than going through
the generic AssetMaker/Referenceable create path: `initiate_engine_action`'s
request body (qualifiedName/domainIdentifier/requestSourceGUIDs/
request body (qualifiedName/domainIdentifier/actionSourceGUIDs/
actionTargets/receivedGuards/requestType/requestParameters/processName/...)
is its own bespoke shape (`GovernanceActionRequestBody`), not a
is its own bespoke shape (`InitiateEngineActionRequestBody`), not a
NewElementRequestBody-wrapped Properties object, and the element it creates
is populated entirely server-side as the engine runs -- there is nothing to
declare beyond the initiation request itself. `cancel_engine_action` takes
Expand Down
2 changes: 2 additions & 0 deletions pyegeria/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
from pyegeria.omvs.notification_manager import NotificationManager
from pyegeria.omvs.people_organizer import PeopleOrganizer
from pyegeria.omvs.product_manager import ProductManager
from pyegeria.omvs.product_catalog import ProductCatalog
from pyegeria.omvs.project_manager import ProjectManager
from pyegeria.omvs.reference_data import ReferenceDataManager
from pyegeria.omvs.registered_info import RegisteredInfo
Expand Down Expand Up @@ -189,6 +190,7 @@
"NotificationManager",
"PeopleOrganizer",
"ProductManager",
"ProductCatalog",
"ProjectManager",
"ReferenceDataManager",
"RegisteredInfo",
Expand Down
17 changes: 8 additions & 9 deletions pyegeria/core/_server_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5652,7 +5652,7 @@ async def _async_remove_search_keyword(
"""

url = f"{self.command_root}classification-explorer/search-keywords/{keyword_guid}/remove"
await self._async_delete_relationship_request(url = url, body = None, cascade_delete = False)
await self._async_delete_element_request(url = url, body = None, cascade_delete = False)

@dynamic_catch
def remove_search_keyword(
Expand Down Expand Up @@ -6520,20 +6520,20 @@ async def _async_find_request(self, url: str, _type: str, _gen_output: Callable[
"limitResultsByStatus": limit_results_by_status,
"sequencingOrder": sequencing_order,
"sequencingProperty": sequencing_property,
"start_from": start_from,
"startFrom": start_from,
"pageSize": page_size,
"metadataElementSubtypeNames": metadata_element_subtypes,
"skipRelationships": skip_relationships,
"includeOnlyRelationships": include_only_relationships,
"relationshipPageSize": relationship_page_size,
"relationshipsPageSize": relationship_page_size,
"skipClassifiedElements": skip_classified_elements,
"includeOnlyClassifiedElements": include_only_classified_elements,
"graphQueryDepth": graph_query_depth,
"maxMermaidNodeCount": max_mermaid_node_count,
"anchorGUID": anchor_guid,
"anchorTypeName": anchor_type_name,
"anchorDomainName": anchor_domain,
"anchorScopeGuid": anchor_scope_guid,
"anchorScopeGUID": anchor_scope_guid,
"governanceZoneFilter": governance_zone_filter,
**kwargs
}
if not metadata_element_type:
Expand All @@ -6547,15 +6547,14 @@ async def _async_find_request(self, url: str, _type: str, _gen_output: Callable[
"endsWith": ends_with,
"ignoreCase": ignore_case,
"anchorGUID": anchor_guid,
"anchorTypeName": anchor_type_name,
"anchorDomainName": anchor_domain,
"anchorScopeGuid": anchor_scope_guid,
"zoneFilter": governance_zone_filter,
"anchorScopeGUID": anchor_scope_guid,
"governanceZoneFilter": governance_zone_filter,
"metadataElementTypeName": metadata_element_type,
"metadataElementSubtypeNames": metadata_element_subtypes,
"skipRelationships": skip_relationships,
"includeOnlyRelationships": include_only_relationships,
"relationshipPageSize": relationship_page_size,
"relationshipsPageSize": relationship_page_size,
"skipClassifiedElements": skip_classified_elements,
"includeOnlyClassifiedElements": include_only_classified_elements,
"graphQueryDepth": graph_query_depth,
Expand Down
2 changes: 2 additions & 0 deletions pyegeria/egeria_tech_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from pyegeria.omvs.notification_manager import NotificationManager
from pyegeria.omvs.people_organizer import PeopleOrganizer
from pyegeria.omvs.product_manager import ProductManager
from pyegeria.omvs.product_catalog import ProductCatalog
from pyegeria.omvs.project_manager import ProjectManager
from pyegeria.omvs.reference_data import ReferenceDataManager
from pyegeria.omvs.registered_info import RegisteredInfo
Expand Down Expand Up @@ -123,6 +124,7 @@ def __init__(
"connection_maker": ConnectionMaker,
"time_keeper": TimeKeeper,
"product_manager": ProductManager,
"product_catalog": ProductCatalog,
"location_arena": LocationArena,
"data_discovery": DataDiscovery,
"data_engineer": DataEngineer,
Expand Down
59 changes: 54 additions & 5 deletions pyegeria/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,16 @@ class RequestBody(PyegeriaModel):
class NewRelationshipRequestBody(RequestBody):
class_: Annotated[Literal["NewRelationshipRequestBody"], Field(alias="class")]
make_anchor: bool | None = False # do I make element at end2 an anchor to end1
anchor_scope_guids: list[str] | None = None # in anchor classification for end2
anchor_scope_guids: list[str] | None = Field(
# Egeria's MakeAnchorOptions reads "anchorScopeGUIDs"; the camel-case
# generator produced "anchorScopeGuids", which Egeria dropped (and the
# documented "anchorScopeGUIDs" was dropped here by extra='ignore'), so
# anchor scope could never be set (ISSUE-164). Old spellings still read.
# (in the anchor classification for end2)
default=None,
validation_alias=AliasChoices("anchorScopeGUIDs", "anchorScopeGuids", "anchor_scope_guids"),
serialization_alias="anchorScopeGUIDs",
)
properties: dict | None = None
relationship_properties: dict | None = Field(default=None, alias="relationshipProperties")

Expand Down Expand Up @@ -489,7 +498,15 @@ class NewElementRequestBody(RequestBody):
class_: Annotated[Literal["NewElementRequestBody"], Field(alias="class")]
anchor_guid: str | None = None
is_own_anchor: bool | None = True
anchor_scope_guids: list[str] | None = None
anchor_scope_guids: list[str] | None = Field(
# Egeria's MakeAnchorOptions reads "anchorScopeGUIDs"; the camel-case
# generator produced "anchorScopeGuids", which Egeria dropped (and the
# documented "anchorScopeGUIDs" was dropped here by extra='ignore'), so
# anchor scope could never be set (ISSUE-164). Old spellings still read.
default=None,
validation_alias=AliasChoices("anchorScopeGUIDs", "anchorScopeGuids", "anchor_scope_guids"),
serialization_alias="anchorScopeGUIDs",
)
initial_classifications: Dict[str, InitialClassifications] | None = None
parent_relationship_properties: dict | None = None
properties: dict | None = None
Expand Down Expand Up @@ -527,10 +544,27 @@ class TemplateRequestBody(RequestBody):
class_: Annotated[Literal["TemplateRequestBody"], Field(alias="class")]
anchor_guid: str | None = None
is_own_anchor: bool | None = True
anchor_scope_guids: list[str] | None = None
initial_classifications: dict | None = None # unsure
anchor_scope_guids: list[str] | None = Field(
# Egeria's MakeAnchorOptions reads "anchorScopeGUIDs"; the camel-case
# generator produced "anchorScopeGuids", which Egeria dropped (and the
# documented "anchorScopeGUIDs" was dropped here by extra='ignore'), so
# anchor scope could never be set (ISSUE-164). Old spellings still read.
default=None,
validation_alias=AliasChoices("anchorScopeGUIDs", "anchorScopeGuids", "anchor_scope_guids"),
serialization_alias="anchorScopeGUIDs",
)
# Egeria's TemplateRequestBody calls this replacementClassifications ("classifications
# to include in the entity creation request"); initialClassifications was dropped by
# the server (ISSUE-164). The old key is still read.
initial_classifications: dict | None = Field(
default=None,
validation_alias=AliasChoices("replacementClassifications", "initialClassifications",
"initial_classifications"),
serialization_alias="replacementClassifications",
)
parent_guid: str | None = None
parent_relationship_type_name: str | None = None
parent_relationship_properties: dict | None = None
parent_at_end_1: bool | None = True
template_guid: str
replacement_properties: dict[str, Any] | None = None
Expand Down Expand Up @@ -784,7 +818,22 @@ class ActionRequestBody(RequestBody):
action_sponsor_guid: Optional[str] = Field(None, alias="actionSponsorGUID")
originator_guid: Optional[str] = Field(None, alias="originatorGUID")
assign_to_actor_guid: Optional[str] = Field(None, alias="assignToActorGUID")
action_target_guids: Optional[list[dict[str,str]]] = Field(None, alias="actionTargetGUIDs")
# Egeria's ActionRequestBody (via AnchorOptions/MakeAnchorOptions) also takes these; the
# model lacked them, so e.g. My Profile's "isOwnAnchor" was dropped client-side (ISSUE-164).
anchor_guid: Optional[str] = Field(None, alias="anchorGUID")
is_own_anchor: Optional[bool] = None
anchor_scope_guids: Optional[list[str]] = Field(
default=None,
validation_alias=AliasChoices("anchorScopeGUIDs", "anchorScopeGuids", "anchor_scope_guids"),
serialization_alias="anchorScopeGUIDs",
)
initial_classifications: Optional[dict] = None
# [{"actionTargetName": ..., "actionTargetGUID": ...}]; Egeria calls it newActionTargets.
new_action_targets: Optional[list[dict[str, str]]] = Field(
default=None,
validation_alias=AliasChoices("newActionTargets", "actionTargetGUIDs", "action_target_guids"),
serialization_alias="newActionTargets",
)



Expand Down
2 changes: 2 additions & 0 deletions pyegeria/omvs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from pyegeria.omvs.notification_manager import NotificationManager
from pyegeria.omvs.people_organizer import PeopleOrganizer
from pyegeria.omvs.product_manager import ProductManager
from pyegeria.omvs.product_catalog import ProductCatalog
from pyegeria.omvs.project_manager import ProjectManager
from pyegeria.omvs.reference_data import ReferenceDataManager
from pyegeria.omvs.registered_info import RegisteredInfo
Expand Down Expand Up @@ -65,6 +66,7 @@
"NotificationManager",
"PeopleOrganizer",
"ProductManager",
"ProductCatalog",
"ProjectManager",
"ReferenceDataManager",
"RegisteredInfo",
Expand Down
Loading
Loading