From d039a69438fa0b570608dbcc6a5c36c69f2983d5 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 15:32:31 +0100 Subject: [PATCH 1/3] fix(pyegeria): reconcile OMVS clients with .http ground truth + rebuild the audit tool The OMVS audit script could not detect the defect classes it existed to find, so a batch of endpoint bugs was reported as "covered". Rebuild the tool, then fix what it actually surfaces. Tooling (scripts/omvs_audit.py): - Verb mapping was wrong: it hardcoded the _async_get_*_request helpers to GET. Every helper in _server_client.py is POST. Verbs are now discovered by parsing that file, and a direct _async_make_request wins over a helper (some methods call a GUID-resolution helper first). - URL extraction was regex-based and yielded empty paths for multi-line f-strings, making whole modules look clean. Now AST-based. - Path comparison stripped the leading service segment and trailing verbs (/attach vs /detach), masking exactly the bugs worth finding. Now compares full normalised paths. - Adds request-body class comparison, structural URL lint, --service/--quiet/ --http-dir, a loud failure when the gitignored ground-truth dir is absent, and exit 1 on defect so it can gate CI. Endpoint fixes: - platform_services: drop the duplicate _async_get_platform_origin / get_platform_origin pair. They shadowed BasePlatformClient's working implementation and omitted is_json=False, so every call raised PyegeriaInvalidParameterException parsing a plain-text response as JSON. - data_discovery: six annotation getters issued GET with query params where Egeria expects POST with a body. Routed through _async_get_name_request (FilterRequestBody) and _async_get_results_body_request (ResultsRequestBody), and corrected three paths: by-type -> by-annotation-type, a spurious path parameter on by-analysis-step, and extensions -> annotation-extensions. - metadata_expert: match-criteria and property-comparison-operators were missing their /metadata-search/ prefix and the -values suffix. - asset_maker, external_links: restore get_actions_for_requester and get_external_identifiers_by_guid. The renames diverged from Egeria's own spelling (getActionsForRequester, getExternalIdentifiersByGUID) and broke test_asset_maker.py and test_external_links.py. - server_operations: ops_command_root carried a trailing slash, so four of five call sites built //servers/. Normalised the root instead. - actor_manager: it_profiles -> it-profiles on detach (attach was correct). - collection_manager: missing / before {agreement_guid} on agreement-item detach produced /agreements/. Audit now reports 557 OK / 102 mismatch / 0 lint, down from 344 mismatches (mostly false positives) under the old script. Remaining mismatches concentrate in connection-maker (34), governance-officer (18) and metadata-expert (15) and are untriaged. Verified: micro-tests pass, functional tests collect, the previously broken call sites resolve. The tool is regression-checked against a reconstructed pre-fix method to confirm it flags the original defect. Signed-off-by: Dan Wolfson --- CLAUDE.md | 45 ++ omvs_audit_report.md | 914 ++++++++++++++++++++++++++++ pyegeria/omvs/action_author.py | 4 +- pyegeria/omvs/actor_manager.py | 2 +- pyegeria/omvs/asset_maker.py | 4 +- pyegeria/omvs/collection_manager.py | 2 +- pyegeria/omvs/data_discovery.py | 829 ++++++++++++++++++++++++- pyegeria/omvs/governance_officer.py | 2 +- pyegeria/omvs/metadata_expert.py | 68 ++- pyegeria/omvs/people_organizer.py | 41 ++ pyegeria/omvs/platform_services.py | 71 +-- pyegeria/omvs/server_operations.py | 5 +- pyegeria/omvs/solution_architect.py | 44 +- pyegeria/omvs/subject_area.py | 4 +- pyegeria/omvs/time_keeper.py | 2 +- scripts/omvs_audit.py | 539 ++++++++++++++++ 16 files changed, 2467 insertions(+), 109 deletions(-) create mode 100644 omvs_audit_report.md create mode 100644 scripts/omvs_audit.py diff --git a/CLAUDE.md b/CLAUDE.md index 87801f1d..07accb27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,6 +99,51 @@ going forward, so there's no mirroring step: once the API edit + `refresh_specs` regeneration + processor wiring are done and verified, that's the complete change. +## Auditing the OMVS clients against the `.http` ground truth + +`scripts/omvs_audit.py` reconciles every `_async_*` method in `pyegeria/omvs/` +against the `.http` collections, which are the ground truth for URLs, verbs, +and request-body classes. + +```bash +# pyegeria/http clients/ is GITIGNORED — absent in fresh worktrees. +# Point the script at a checkout that has it: +export PYEGERIA_HTTP_DIR="/path/to/egeria-python/pyegeria/http clients" + +python scripts/omvs_audit.py --quiet # full audit -> omvs_audit_report.md +python scripts/omvs_audit.py --service location-arena # one service +``` + +Exit status is 1 when any confirmed defect is found, so it can gate CI. + +**What it checks, and why each check exists** — every category below was added +after a real defect slipped through an earlier, weaker version of this script: + +- **VERB** — compares the actual HTTP verb. Helper verbs are discovered by + parsing `_server_client.py` rather than hardcoded: *every* `_async_*_request` + helper is POST, and an earlier hardcoded table wrongly mapped the `get_*` + helpers to GET, which is how a batch of GET-vs-POST defects went undetected. + A direct `_async_make_request` wins over a helper, because some methods call + a GUID-resolution helper first. +- **PATH** — compares full normalised paths. It deliberately does **not** strip + 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. +- **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). + +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. + +When triaging a finding, confirm it against the `.http` file before changing +code, and prefer the SDK's existing helpers (`_async_get_name_request`, +`_async_get_results_body_request`, …) over hand-rolled `_async_make_request` +calls, so verb and body shape stay correct by construction. + ## Commits - Always use `git commit -s` to sign off commits. This appends `Signed-off-by: Dan Wolfson ` — DCO is enforced on this repo and unsigned commits will be rejected. diff --git a/omvs_audit_report.md b/omvs_audit_report.md new file mode 100644 index 00000000..171acf23 --- /dev/null +++ b/omvs_audit_report.md @@ -0,0 +1,914 @@ +# OMVS Audit Report + +Ground truth: `pyegeria/http clients` (40 collections) +Subject: `pyegeria/omvs` (43 modules) + +| Result | Count | +|---|---| +| OK | 557 | +| Mismatch (verb/path/body) | 102 | +| Missing | 288 | +| Found in another module | 13 | +| URL lint | 0 | + +## Duplicate endpoints (same verb + path) + +_Review only - cross-service overlap is often intentional._ + +- `POST /automated-curation/governance-action-types/initiate` + - `automated-curation.py`: `_async_initiate_gov_action_type` + - `automated-curation.py`: `_async_initiate_survey` +- `POST /classification-explorer/elements/by-ownership` + - `classification-explorer.py`: `_async_get_owners_elements` + - `classification-explorer.py`: `_async_get_subject_area_members` +- `POST /collection-manager/collections` + - `collection-manager.py`: `_async_create_collection` + - `collection-manager.py`: `_async_create_data_spec_collection` + - `collection-manager.py`: `_async_create_report_type_collection` + - `collection-manager.py`: `_async_create_question_spec_folder` + - `collection-manager.py`: `_async_create_security_list` + - `collection-manager.py`: `_async_create_data_dictionary_collection` + - `collection-manager.py`: `_async_create_skill_set_collection` + - `collection-manager.py`: `_async_create_reference_list_collection` + - `collection-manager.py`: `_async_create_digital_product` + - `collection-manager.py`: `_async_create_digital_product_catalog` + - `collection-manager.py`: `_async_create_agreement` + - `collection-manager.py`: `_async_create_digital_subscription` +- `POST /collection-manager/collections/{}/update` + - `collection-manager.py`: `_async_update_collection` + - `collection-manager.py`: `_async_update_digital_product` + - `collection-manager.py`: `_async_update_agreement` + - `collection-manager.py`: `_async_update_digital_subscription` +- `POST /data-designer/data-value-specifications` + - `data-designer.py`: `_async_create_data_value_specification` + - `data-designer.py`: `_async_create_data_grain` + - `data-designer.py`: `_async_create_data_class` +- `POST /data-designer/data-value-specifications/{}/delete` + - `data-designer.py`: `_async_delete_data_value_specification` + - `data-designer.py`: `_async_delete_data_class` +- `POST /data-designer/data-value-specifications/{}/retrieve` + - `data-designer.py`: `_async_get_data_value_specification_by_guid` + - `data-designer.py`: `_async_get_data_class_by_guid` +- `POST /glossary-manager/glossaries/terms` + - `glossary-manager.py`: `_async_create_glossary_term` + - `glossary-manager.py`: `_async_create_question` +- `POST /governance-officer/governance-definitions/{}/retrieve` + - `action-author.py`: `_async_get_governance_action_process` + - `governance-officer.py`: `_async_get_governance_action_process` +- `POST /metadata-expert/related-elements` + - `collection-manager.py`: `_async_link_saved_query_to_results_set` + - `metadata-expert.py`: `_async_create_related_elements` +- `POST /metadata-expert/related-elements/{}/delete` + - `collection-manager.py`: `_async_detach_saved_query_from_results_set` + - `metadata-expert.py`: `_async_delete_related_elements` +- `POST /platform-services/server-platform/servers/{}/instance` + - `platform-services.py`: `_async_activate_server_stored_config` + - `platform-services.py`: `_async_activate_server_supplied_config` +- `POST /product-manager/collections` + - `product-manager.py`: `_async_create_digital_product` + - `product-manager.py`: `_async_create_digital_product_catalog` +- `POST /product-manager/collections/by-name` + - `product-manager.py`: `_async_get_digital_products_by_name` + - `product-manager.py`: `_async_get_digital_product_catalogs_by_name` +- `POST /product-manager/collections/by-search-string` + - `product-manager.py`: `_async_find_digital_products` + - `product-manager.py`: `_async_find_digital_product_catalogs` +- `POST /product-manager/collections/{}/delete` + - `product-manager.py`: `_async_delete_digital_product` + - `product-manager.py`: `_async_delete_digital_product_catalog` +- `POST /product-manager/collections/{}/retrieve` + - `product-manager.py`: `_async_get_digital_product_by_guid` + - `product-manager.py`: `_async_get_digital_product_catalog_by_guid` +- `POST /product-manager/collections/{}/update` + - `product-manager.py`: `_async_update_digital_product` + - `product-manager.py`: `_async_update_digital_product_catalog` +- `POST /runtime-manager/platforms/by-deployed-implementation-type` + - `runtime-manager.py`: `_async_get_platforms_by_type` + - `runtime-manager.py`: `_async_get_platform_templates_by_type` +- `POST /runtime-manager/software-servers/by-deployed-implementation-type` + - `runtime-manager.py`: `_async_get_servers_by_dep_impl_type` + - `runtime-manager.py`: `_async_get_server_templates_by_dep_impl_type` +- `POST /solution-architect/solution-blueprints/{}/update` + - `solution-architect.py`: `_async_update_solution_blueprint_status` + - `solution-architect.py`: `_async_update_solution_blueprint` +- `POST /{}/collections/by-search-string` + - `security-officer.py`: `_async_find_security_roles` + - `security-officer.py`: `_async_find_security_groups` +- `POST /{}/governance-definitions` + - `governance-officer.py`: `_async_create_governance_definition` + - `governance-officer.py`: `_async_create_data_lens` + + +### Service: action-author + + +### Service: actor-manager + +- find All ContributionRecords: MISSING (`POST /actor-manager/contribution-records/by-search-string`) +- updateActorRole: MISMATCH `update_actor_role` + - PATH + SDK: /actor-manager/actor-roles/{}/update + API: /actor-manager/actor-roles/update +- Detach a team role from a team profile.: MISSING (`POST /actor-manager/actor-roles/{}/team-role-appointments/{}/detach`) +- linkITProfileRoleToProfile: MISSING (`POST /actor-manager/actor-roles/{}/it-profile-role-appointments/{}/attach`) +- detachITProfileRoleFromProfile: MISSING (`POST /actor-manager/actor-roles/{}/it-profile-role-appointments/{}/detach`) +- deleteActorRole: MISMATCH `delete_actor_role` + - PATH + SDK: /actor-manager/actor-roles/{}/delete + API: /actor-manager/actor-roles/delete + - BODY DeleteElementRequestBody != DeleteRelationshipRequestBody +- getActorRoleByGUID: MISMATCH `get_actor_role_by_guid` + - PATH + SDK: /actor-manager/actor-roles/{}/retrieve + API: /actor-manager/actor-roles/{}/retrieve"} +- detachProfileIdentity: MISSING (`POST /actor-manager/user-identities/{}/profile-identity/{}/detach`) +- addSecurityGroupMembership: MISMATCH `add_security_group_membership` + - PATH + SDK: /actor-manager/user-identities/{}/security-group-membership/classify + API: /actor-manager/user-identities/{}/security-group-memberships/classify +- updateSecurityGroupMembership: MISMATCH `update_security_group_membership` + - PATH + SDK: /actor-manager/user-identities/{}/security-group-membership/reclassify + API: /actor-manager/user-identities/{}/security-group-memberships/reclassify +- removeAllSecurityGroupMembership: MISSING (`POST /actor-manager/user-identities/{}/security-group-memberships/declassify`) + +### Service: asset-catalog + + +### Service: asset-maker + +- unDeployITAsset: MISSING (`POST /asset-maker/assets/{}/deployed-on/{}/detach`) +- linkSoftwareCapability: MISSING (`POST /asset-maker/assets/{}/supported-software-capabilities/{}/attach`) +- detachSoftwareCapability: MISSING (`POST /asset-maker/assets/{}/supported-software-capabilities/{}/detach`) +- linkSupportedGovernanceService: MISSING (`POST /asset-maker/governance-engines/{}/supported-governance-services/{}/attach`) +- updateSupportedGovernanceService: MISSING (`POST /asset-maker/supported-governance-services/{}/update`) +- detachSupportedGovernanceService: MISSING (`POST /asset-maker/supported-governance-services/{}/detach`) + +### Service: automated-curation + +- getTechnologyTypesForOpenMetadataType: MISSING (`POST /automated-curation/open-metadata-types/{}/technology-types`) +- getTechnologyTypeDetail: MISSING (`POST /automated-curation/technology-types/by-name`) +- getTechnologyTypeHierarchy: MISSING (`POST /automated-curation/technology-types/hierarchy`) +- getTechnologyTypeTemplates: MISSING (`POST /automated-curation/technology-types/elements`) +- createElementFromTemplate: MISSING (`POST /automated-curation/catalog-templates/new-element`) +- getElementFromTemplate: MISSING (`POST /automated-curation/catalog-templates/new-element`) +- createElementFromTemplate - Marquez endpoint: MISSING (`POST /automated-curation/catalog-templates/new-element`) +- initiateGovernanceActionType: MISSING (`POST /automated-curation/governance-action-types/initiate`) +- initiateGovernanceActionProcess: MISSING (`POST /automated-curation/governance-action-processes/initiate`) +- updateEngineActionStatus: MISSING (`POST /automated-curation/engine-actions/{}/status/update`) +- claimEngineAction: MISSING (`POST /automated-curation/engine-actions/{}/claim`) +- getActiveClaimedEngineActions: MISSING (`GET /automated-curation/governance-engines/{}/engine-actions/active-claimed`) +- updateActionTargetStatus: MISSING (`POST /automated-curation/engine-actions/action-targets/update`) +- recordCompletionStatus: MISSING (`POST /automated-curation/engine-actions/{}/completion-status`) + +### Service: classification-explorer + +- getValidMetadataValues - severityLevel values: MISSING (`GET /valid-metadata/get-valid-metadata-values/severityLevel`) +- getImpactClassifiedElements: MISSING (`POST /classification-explorer/elements/by-impact`) +- getValidMetadataValues - confidenceLevel values: MISSING (`GET /valid-metadata/get-valid-metadata-values/confidenceLevel`) +- getConfidenceClassifiedElements: MISSING (`POST /classification-explorer/elements/by-confidence`) +- getValidMetadataValues - criticalityLevel values: MISSING (`GET /valid-metadata/get-valid-metadata-values/criticalityLevel`) +- getCriticalityClassifiedElements: MISSING (`POST /classification-explorer/elements/by-criticality`) +- getValidMetadataValues - confidentialityLevel values: MISSING (`GET /valid-metadata/get-valid-metadata-values/confidentialityLevel`) +- getConfidentialityClassifiedElements: MISSING (`POST /classification-explorer/elements/by-confidentiality`) +- getValidMetadataValues - retentionBasis values: MISSING (`GET /valid-metadata/get-valid-metadata-values/retentionBasis`) +- getRetentionClassifiedElements: MISSING (`POST /classification-explorer/elements/by-retention`) +- addSecurityTags: MISSING (`POST /classification-explorer/elements/{}/security-tags`) +- clearSecurityTags: MISSING (`POST /classification-explorer/elements/{}/security-tags/remove`) +- addAccountingCodes: MISSING (`POST /classification-explorer/elements/{}/accounting-codes`) +- clearAccountingCodes: MISSING (`POST /classification-explorer/elements/{}/accounting-codes/remove`) +- addOwnership: MISSING (`POST /classification-explorer/elements/{}/ownership`) +- clearOwnership: MISSING (`POST /classification-explorer/elements/{}/ownership/remove`) +- clearDigitalResourceOrigin: MISSING (`POST /classification-explorer/elements/{}/digital-resource-origin/remove`) +- setupPeerDuplicates: MISSING (`POST /classification-explorer/related-elements/{}/peer-duplicate/{}/attach`) +- clearSemanticAssignment: MISSING (`POST /classification-explorer/elements/{}/semantic-assignment/terms/{}/detach`) +- getSemanticAssignees: MISMATCH `get_semantic_assignees` + - PATH + SDK: /classification-explorer/glossaries/elements/by-semantic-assignment/{} + API: /classification-explorer/elements/by-semantic-assignment/{} +- addGovernanceDefinitionToElement: MISSING (`POST /classification-explorer/elements/{}/governed-by/definition/{}/attach`) +- removeGovernanceDefinitionFromElement: MISSING (`POST /classification-explorer/elements/{}/governed-by/definition/{}/detach`) +- addGovernanceExpectations: MISSING (`POST /classification-explorer/elements/{}/governance-expectations`) +- updateGovernanceExpectations: MISSING (`POST /classification-explorer/elements/{}/governance-expectations/update`) +- clearGovernanceExpectations: MISSING (`POST /classification-explorer/elements/{}/governance-expectations/remove`) +- addResourceListToElement: MISSING (`POST /classification-explorer/elements/{}/resource-list/{}/attach`) +- removeResourceListFromElement: MISSING (`POST /classification-explorer/elements/{}/resource-list/{}/detach`) +- addMoreInformationToElement: MISSING (`POST /classification-explorer/elements/{}/more-information/{}/attach`) +- removeMoreInformationFromElement: MISSING (`POST /classification-explorer/elements/{}/more-information/{}/detach`) +- getSourceElements: MISMATCH `get_source_elements` + - PATH + SDK: /classification-explorer/glossaries/elements/{}/source + API: /classification-explorer/elements/{}/source +- getElementsSourcedFrom: MISMATCH `get_elements_sourced_from` + - PATH + SDK: /classification-explorer/glossaries/elements/{}/sourced-from + API: /classification-explorer/elements/{}/sourced-from +- removeScopeFromElement: MISSING (`POST /classification-explorer/elements/{}/scoped-by/{}/detach`) +- licenseElement: ELSEWHERE -> `governance-officer.py` +- getLicensedElements: MISMATCH `get_licensed_elements` + - PATH + SDK: /classification-explorer/glossaries/elements/licenses/{} + API: /classification-explorer/elements/licenses/{} +- getLicenses: MISMATCH `get_licenses` + - PATH + SDK: /classification-explorer/glossaries/elements/{}/licenses + API: /classification-explorer/elements/{}/licenses +- certifyElement: ELSEWHERE -> `governance-officer.py` +- getCertifiedElements: MISMATCH `get_certified_elements` + - PATH + SDK: /classification-explorer/glossaries/elements/certifications/{} + API: /classification-explorer/elements/certifications/{} +- getSearchKeywordByGUID: MISSING (`POST /classification-explorer/search-keywords/{}/retrieve`) +- getSearchKeywordsByKeyword: MISSING (`POST /classification-explorer/search-keywords/by-keyword`) +- findSearchKeywords: MISSING (`POST /classification-explorer/search-keywords/by-search-string`) +- getRootElementByGUID: MISSING (`POST /classification-explorer/elements/{}`) +- getRootElementByUniqueName: MISSING (`POST /classification-explorer/elements/by-unique-name`) +- getMetadataElementGUIDByUniqueName: ELSEWHERE -> `metadata-expert.py` +- getRootElementsByType: MISSING (`POST /classification-explorer/elements/by-type`) +- getRootElementsByPropertyValue: MISSING (`POST /classification-explorer/elements/by-exact-property-value`) +- findRootElementsByPropertyValue: MISSING (`POST /classification-explorer/elements/by-property-value-search`) +- getRootElementsByCategory: MISSING (`POST /classification-explorer/elements/by-category`) +- findRootAuthoredElements: MISSING (`POST /classification-explorer/authored-elements/by-search-string`) +- getRootAuthoredElementsByCategory: MISSING (`POST /classification-explorer/authored-elements/by-category`) +- getRootElementsByClassification: MISSING (`POST /classification-explorer/elements/by-classification/{}`) +- getRootElementsByClassificationWithPropertyValue: MISSING (`POST /classification-explorer/elements/by-classification/{}/with-exact-property-value`) +- findRootElementsByClassificationWithPropertyValue: MISSING (`POST /classification-explorer/elements/by-classification/{}/with-property-value-search`) +- getRelatedRootElements: MISSING (`POST /classification-explorer/elements/{}/by-relationship`) +- getRelatedRootElementsWithPropertyValue: MISSING (`POST /classification-explorer/elements/{}/by-relationship/{}/with-exact-property-value`) +- findRelatedRootElementsWithPropertyValue: MISSING (`POST /classification-explorer/elements/{}/by-relationship/{}/with-property-value-search`) +- getRelationshipByGUID: ELSEWHERE -> `metadata-expert.py` + +### Service: collection-manager + +- createGlossary: ELSEWHERE -> `glossary-manager.py` +- createDataSharingAgreementCollection: MISSING (`POST /collection-manager/collections`) +- updateAgreementStatus: MISSING (`POST /collection-manager/collections/{}/update`) +- updateDigitalSubscriptionStatus: MISSING (`POST /collection-manager/collections/{}/update`) +- detachDataDescription: MISSING (`POST /collection-manager/metadata-elements/{}/data-descriptions/{}/detach`) +- attachSmartQuery: MISSING (`POST /collection-manager/collections/results-sets/{}/smart-query/{}/attach`) +- detachSmartQuery: MISSING (`POST /collection-manager/collections/results-sets/{}/smart-query/{}/detach`) +- attachAssociatedSkillSet: MISSING (`POST /collection-manager/actors/{}/associated-skill-sets/{}/attach`) +- detachAssociatedSkillSet: MISMATCH `detach_associated_skill_set` + - PATH + SDK: /{}/actors/{}/associated-skill-sets/{}/detach + API: /collection-manager/actors/{}/associated-skill-sets/{}/detach +- updateCollectionMembership: MISSING (`POST /collection-manager/collections/{}/members/{}/update`) + +### Service: community-matters + + +### Service: connection-maker + +- createConnection: MISMATCH `create_connection` + - PATH + SDK: /{}/connections + API: /connection-maker/connections +- createConnectionFromTemplate: MISMATCH `create_connection_from_template` + - PATH + SDK: /{}/connections/from-template + API: /connection-maker/connections/from-template +- updateConnection: MISMATCH `update_connection` + - PATH + SDK: /{}/connections/{}/update + API: /connection-maker/connections/{}/update +- linkConnectionConnectorType: MISMATCH `link_connection_connector_type` + - PATH + SDK: /{}/connections/{}/connector-types/{}/attach + API: /connection-maker/connections/{}/connector-types/{}/attach +- detachConnectionConnectorType: MISMATCH `detach_connection_connector_type` + - PATH + SDK: /{}/connections/{}/connector-types/{}/detach + API: /connection-maker/connections/{}/connector-types/{}/detach +- linkConnectionEndpoint: MISMATCH `link_connection_endpoint` + - PATH + SDK: /{}/connections/{}/endpoints/{}/attach + API: /connection-maker/connections/{}/endpoints/{}/attach +- detachConnectionEndpoint: MISMATCH `detach_connection_endpoint` + - PATH + SDK: /{}/connections/{}/endpoints/{}/detach + API: /connection-maker/connections/{}/endpoints/{}/detach +- linkEmbeddedConnection: MISMATCH `link_embedded_connection` + - PATH + SDK: /{}/connections/{}/embedded-connections/{}/attach + API: /connection-maker/connections/{}/embedded-connections/{}/attach +- detachEmbeddedConnection: MISMATCH `detach_embedded_connection` + - PATH + SDK: /{}/connections/{}/embedded-connections/{}/detach + API: /connection-maker/connections/{}/embedded-connections/{}/detach +- linkAssetToConnection: MISMATCH `link_asset_to_connection` + - PATH + SDK: /{}/assets/{}/connections/{}/attach + API: /connection-maker/assets/{}/connections/{}/attach +- detachAssetFromConnection: MISMATCH `detach_asset_from_connection` + - PATH + SDK: /{}/assets/{}/connections/{}/detach + API: /connection-maker/assets/{}/connections/{}/detach +- linkEndpointToITAsset: MISMATCH `link_endpoint_to_it_asset` + - PATH + SDK: /{}/assets/{}/endpoints/{}/attach + API: /connection-maker/assets/{}/endpoints/{}/attach +- detachEndpointFromITAsset: MISMATCH `detach_endpoint_from_it_asset` + - PATH + SDK: /{}/assets/{}/endpoints/{}/detach + API: /connection-maker/assets/{}/endpoints/{}/detach +- deleteConnection: MISMATCH `delete_connection` + - PATH + SDK: /{}/connections/{}/delete + API: /connection-maker/connections/{}/delete +- getConnectionsByName: MISMATCH `get_connections_by_name` + - PATH + SDK: /{}/connections/by-name + API: /connection-maker/connections/by-name +- findConnections: MISMATCH `find_connections` + - PATH + SDK: /{}/connections/by-search-string + API: /connection-maker/connections/by-search-string +- getConnectionByGUID: MISMATCH `get_connection_by_guid` + - PATH + SDK: /{}/connections/{}/retrieve + API: /connection-maker/connections/{}/retrieve +- createConnectorType: MISMATCH `create_connector_type` + - PATH + SDK: /{}/connector-types + API: /connection-maker/connector-types +- createConnectorTypeFromTemplate: MISMATCH `create_connector_type_from_template` + - PATH + SDK: /{}/connector-types/from-template + API: /connection-maker/connector-types/from-template +- updateConnectorType: MISMATCH `update_connector_type` + - PATH + SDK: /{}/connector-types/{}/update + API: /connection-maker/connector-types/{}/update +- deleteConnectorType: MISMATCH `delete_connector_type` + - PATH + SDK: /{}/connector-types/{}/delete + API: /connection-maker/connector-types/{}/delete +- getConnectorTypesByName: MISMATCH `get_connector_types_by_name` + - PATH + SDK: /{}/connector-types/by-name + API: /connection-maker/connector-types/by-name +- getConnectorTypesByConnectorProviderClassName: MISMATCH `get_connector_types_by_connector_provider_class_name` + - PATH + SDK: /{}/connector-types/by-connector-provider-class-name + API: /connection-maker/connector-types/by-connector-provider-class-name +- findConnectorTypes: MISMATCH `find_connector_types` + - PATH + SDK: /{}/connector-types/by-search-string + API: /connection-maker/connector-types/by-search-string +- getConnectorTypeByGUID: MISMATCH `get_connector_type_by_guid` + - PATH + SDK: /{}/connector-types/{}/retrieve + API: /connection-maker/connector-types/{}/retrieve +- createEndpoint: MISMATCH `create_endpoint` + - PATH + SDK: /{}/endpoints + API: /connection-maker/endpoints +- createEndpointFromTemplate: MISMATCH `create_endpoint_from_template` + - PATH + SDK: /{}/endpoints/from-template + API: /connection-maker/endpoints/from-template +- updateEndpoint: MISMATCH `update_endpoint` + - PATH + SDK: /{}/endpoints/{}/update + API: /connection-maker/endpoints/{}/update +- deleteEndpoint: MISMATCH `delete_endpoint` + - PATH + SDK: /{}/endpoints/{}/delete + API: /connection-maker/endpoints/{}/delete +- getEndpointsByName: MISMATCH `get_endpoints_by_name` + - PATH + SDK: /{}/endpoints/by-name + API: /connection-maker/endpoints/by-name +- getEndpointsByNetworkAddress: MISMATCH `get_endpoints_by_network_address` + - PATH + SDK: /{}/endpoints/by-network-address + API: /connection-maker/endpoints/by-network-address +- getEndpointsForAsset: MISMATCH `get_endpoints_for_asset` + - PATH + SDK: /{}/assets/{}/endpoints/retrieve + API: /connection-maker/assets/{}/endpoints/retrieve +- findEndpoints: MISMATCH `find_endpoints` + - PATH + SDK: /{}/endpoints/by-search-string + API: /connection-maker/endpoints/by-search-string +- getEndpointByGUID: MISMATCH `get_endpoint_by_guid` + - PATH + SDK: /{}/endpoints/{}/retrieve + API: /connection-maker/endpoints/{}/retrieve + +### Service: data-designer + +- findAllDataStructures - with full request body: MISSING (`POST /data-designer/data-structures/by-search-string`) +- findDataStructures - with full request body: MISSING (`POST /data-designer/data-structures/by-search-string`) +- getDataStructuresByName - with full request body: MISSING (`POST /data-designer/data-structures/by-name`) +- getDataStructureByGUID - with request body: MISSING (`POST /data-designer/data-structures/{}/retrieve`) +- linkNestedDataFields: MISSING (`POST /data-designer/data-fields/{}/nested-data-fields/{}/attach`) +- detachNestedDataFields: MISSING (`POST /data-designer/data-fields/{}/nested-data-fields/{}/detach`) +- findAllDataFields - with full request body: MISSING (`POST /data-designer/data-fields/by-search-string`) +- findDataFields - with full request body: MISSING (`POST /data-designer/data-fields/by-search-string`) +- getDataFieldsByName - with full request body: MISSING (`POST /data-designer/data-fields/by-name`) +- getDataFieldByGUID - with request body: MISSING (`POST /data-designer/data-fields/{}/retrieve`) +- createDataValueSpecificationFromTemplate: MISSING (`POST /data-designer/data-value-specifications/from-template`) +- detachSpecializedDataValueSpecification: MISMATCH `detach_specialized_data_value_specification` + - PATH + SDK: /data-designer/data-value-specifications/{}/specialized-data-value-specification-definition/{}/detach + API: /data-designer/data-value-specifications/{}/specialized-data-value-specifications/{}/detach +- assignDataValueSpecification: MISSING (`POST /data-designer/elements/{}/data-value-specifications/{}/attach`) +- detachDataValueSpecificationAssignment: MISSING (`POST /data-designer/elements/{}/data-value-specifications/{}/detach`) +- findAllDataClasses: MISSING (`POST /data-designer/data-value-specifications/by-search-string`) +- findAllDataGrains: MISSING (`POST /data-designer/data-value-specifications/by-search-string`) +- findDataValueSpecifications - with full request body: MISSING (`POST /data-designer/data-value-specifications/by-search-string`) +- linkDataValueSpecificationDefinition: MISSING (`POST /data-designer/data-definitions/{}/data-value-specification-definition/{}/attach`) +- detachDataValueSpecificationDefinition: MISSING (`POST /data-designer/data-definitions/{}/data-value-specification-definition/{}/detach`) +- detachCertificationTypeToDataStructure: MISSING (`POST /data-designer/certification-types/{}/data-structure-definition/{}/detach`) + +### Service: data-discovery + + +### Service: data-engineer + +- getTabularDataSetReport: MISSING (`GET /data-engineer/tabular-data-sets/{}/report`) + +### Service: digital-business + + +### Service: external-links + +- linkCitedDocumentReference: MISSING (`POST /external-links/elements/{}/cited-document-references/{}/attach`) +- detachCitedDocumentReference: MISSING (`POST /external-links/elements/{}/cited-document-references/{}/detach`) +- deleteExternalReference: MISMATCH `delete_external_reference` + - BODY DeleteRelationshipRequestBody != DeleteElementRequestBody + +### Service: feedback-manager + +- addCommentToElement: MISSING (`POST /feedback-manager/elements/{}/comments`) +- updateComment: MISSING (`POST /feedback-manager/comments/{}/update`) +- setupAcceptedAnswer: MISSING (`POST /feedback-manager/comments/questions/{}/answers/{}`) +- clearAcceptedAnswer: MISSING (`POST /feedback-manager/comments/questions/{}/answers/{}/remove`) +- removeCommentFromElement: MISSING (`POST /feedback-manager/comments/{}/remove`) +- getCommentByGUID: MISSING (`POST /feedback-manager/comments/{}/retrieve`) +- getAttachedComments: MISSING (`POST /feedback-manager/elements/{}/comments/retrieve`) +- findComments: MISSING (`POST /feedback-manager/comments/by-search-string`) +- addLikeToElement: MISSING (`POST /feedback-manager/elements/{}/likes`) +- removeLikeFromElement: MISSING (`POST /feedback-manager/elements/{}/likes/remove`) +- getAttachedLikes: MISSING (`POST /feedback-manager/elements/{}/likes/retrieve`) +- addRatingToElement: MISSING (`POST /feedback-manager/elements/{}/ratings`) +- removeRatingFromElement: MISSING (`POST /feedback-manager/elements/{}/ratings/remove`) +- getAttachedRatings: MISSING (`POST /feedback-manager/elements/{}/ratings/retrieve`) +- createInformalTag: MISSING (`POST /feedback-manager/tags`) +- updateTagDescription: MISSING (`POST /feedback-manager/tags/{}/update`) +- deleteTag: MISSING (`POST /feedback-manager/tags/{}/remove`) +- getTag: MISSING (`POST /feedback-manager/tags/{}/retrieve`) +- getTagsByName: MISSING (`POST /feedback-manager/tags/by-name`) +- findTags: MISSING (`POST /feedback-manager/tags/by-search-string`) +- findMyTags: MISSING (`POST /feedback-manager/tags/private/by-search-string`) +- addTagToElement: MISSING (`POST /feedback-manager/elements/{}/tags/{}`) +- getElementsByTag: MISSING (`POST /feedback-manager/elements/by-tag/{}/retrieve`) +- getAttachedTags: MISSING (`POST /feedback-manager/elements/{}/tags/retrieve`) +- removeTagFromElement: MISSING (`POST /feedback-manager/elements/{}/tags/{}/remove`) +- createNoteLog: MISSING (`POST /feedback-manager/elements/{}/note-logs`) +- updateNoteLog: MISSING (`POST /feedback-manager/note-logs/{}`) +- removeNoteLog: MISSING (`POST /feedback-manager/note-logs/{}/remove`) +- findNoteLogs: MISSING (`POST /feedback-manager/note-logs/by-search-string`) +- getNoteLogsByName: MISSING (`POST /feedback-manager/note-logs/by-name`) +- getNoteLogsForElement: MISSING (`POST /feedback-manager/elements/{}/note-logs/retrieve`) +- getNoteLogByGUID: MISSING (`POST /feedback-manager/note-logs/{}/retrieve`) +- createNote: MISSING (`POST /feedback-manager/assets`) +- updateNote: MISSING (`POST /feedback-manager/assets/{}/update`) +- removeNote: MISSING (`POST /feedback-manager/assets/{}/delete`) +- findNotes: MISSING (`POST /feedback-manager/assets/by-search-string`) +- getNotesForNoteLog: MISSING (`POST /feedback-manager/note-logs/{}/notes/retrieve`) +- getNoteByGUID: MISSING (`POST /feedback-manager/assets/{}/retrieve`) + +### Service: glossary-manager + +- getTermRelationshipTypeNames: MISSING (`GET /glossary-manager/glossaries/terms/relationships/type-names`) +- clearTermAsAbstractConcept: MISMATCH `remove_is_abstract_concept` + - BODY DeleteClassificationRequestBody != DeleteElementRequestBody +- clearTermAsActivity: MISMATCH `remove_activity_description` + - BODY DeleteClassificationRequestBody != DeleteRelationshipRequestBody +- clearTermAsContext: MISMATCH `remove_is_context_definition` + - BODY DeleteClassificationRequestBody != DeleteRelationshipRequestBody + +### Service: governance-officer + +- getValidMetadataValues - domainIdentifier values: MISSING (`GET /valid-metadata/get-valid-metadata-values/domainIdentifier`) +- createGovernanceDefinition: MISMATCH `create_governance_definition` + - PATH + SDK: /{}/governance-definitions + API: /governance-officer/governance-definitions +- createRegulation: MISSING (`POST /governance-officer/governance-definitions`) +- addRegulatorToRegulation: MISMATCH `add_regulator_to_regulation` + - PATH + SDK: /{}/regulations/{}/regulators/organizations/{}/attach + API: /governance-officer/regulations/{}/regulators/organizations/{}/attach +- removeRegulatorFromRegulation: MISSING (`POST /governance-officer/regulations/{}/regulators/organizations/{}/detach`) +- createGovernanceControl: MISSING (`POST /governance-officer/governance-definitions`) +- createDataLens: MISMATCH `create_data_lens` + - PATH + SDK: /{}/governance-definitions + API: /governance-officer/governance-definitions +- createSecurityAccessControl: MISSING (`POST /governance-officer/governance-definitions`) +- createNamingStandardRule: MISSING (`POST /governance-officer/governance-definitions`) +- createCertificationType: MISSING (`POST /governance-officer/governance-definitions`) +- createLicenseType: MISSING (`POST /governance-officer/governance-definitions`) +- createGovernanceDefinitionFromTemplate: MISMATCH `create_governance_definition_from_template` + - PATH + SDK: /{}/governance-definitions/from-template + API: /governance-officer/governance-definitions/from-template +- updateGovernanceDefinition: MISMATCH `update_governance_definition` + - PATH + SDK: /{}/governance-definitions/{}/update + API: /governance-officer/governance-definitions/{}/update +- updateGovernanceDefinitionStatus: MISSING (`POST /governance-officer/governance-definitions/{}/update`) +- detachPeerDefinitions: MISMATCH `detach_peer_definitions` + - PATH + SDK: /{}/governance-definitions/{}/peer-definitions/{}/{}/detach + API: /governance-officer/governance-definitions/{}/peer-definitions/{}/{}/detach +- attachSupportingDefinition: MISSING (`POST /governance-officer/governance-definitions/{}/supporting-definitions/{}/{}/attach`) +- detachSupportingDefinition: MISSING (`POST /governance-officer/governance-definitions/{}/supporting-definitions/{}/{}/detach`) +- findAllGovernanceDefinitions: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) +- findAllGovernanceDefinitions - with full request body: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) +- findGovernanceDefinitions: MISMATCH `find_governance_definitions` + - PATH + SDK: /{}/governance-definitions/by-search-string + API: /governance-officer/governance-definitions/by-search-string +- findGovernanceDefinitions - with full request body: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) +- getGovernanceDefinitionsByName: MISMATCH `get_governance_definitions_by_name` + - PATH + SDK: /{}/governance-definitions/by-name + API: /governance-officer/governance-definitions/by-name +- getGovernanceDefinitionsByName - with full request body: MISSING (`POST /governance-officer/governance-definitions/by-name`) +- getGovernanceDefinitionByGUID: MISMATCH `get_governance_definition_by_guid` + - PATH + SDK: /{}/governance-definitions/{}/retrieve + API: /governance-officer/governance-definitions/{}/retrieve +- getGovernanceDefinitionByGUID - with request body: MISSING (`POST /governance-officer/governance-definitions/{}/retrieve`) +- getAllGovernanceActionTypes: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) +- findGovernanceActionTypes: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) +- getGovernanceActionTypesByName: MISSING (`POST /governance-officer/governance-definitions/by-name`) +- getGovernanceActionTypeByGUID: MISSING (`POST /governance-officer/governance-definitions/{}/retrieve`) +- findGovernanceActionProcesses: MISSING (`POST /governance-officer/governance-action-processes/by-search-string`) +- getAllGovernanceActionProcesses: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) +- getGovernanceActionProcessesByName: MISSING (`POST /governance-officer/governance-definitions/by-name`) +- getGovernanceActionProcessGraph: MISMATCH `get_governance_action_process_graph` + - PATH + SDK: /{}/governance-action-processes/{}/graph + API: /governance-officer/governance-action-processes/{}/graph + - BODY FilterRequestBody != ResultsRequestBody +- addGovernanceDefinitionToElement: MISSING (`POST /governance-officer/elements/{}/governed-by/definition/{}/attach`) +- removeGovernanceDefinitionFromElement: MISSING (`POST /governance-officer/elements/{}/governed-by/definition/{}/detach`) +- linkDesignToImplementation: MISMATCH `link_design_to_implementation` + - PATH + SDK: /{}/designs/{}/implementations/{}/attach + API: /governance-officer/designs/{}/implementations/{}/attach +- detachDesignFromImplementation: MISMATCH `detach_design_from_implementation` + - PATH + SDK: /{}/designs/{}/implementations/{}/detach + API: /governance-officer/designs/{}/implementations/{}/detach + - BODY DeleteElementRequestBody != DeleteRelationshipRequestBody +- linkImplementationResource: MISMATCH `link_implementation_resource` + - PATH + SDK: /{}/designs/{}/implementation-resources/{}/attach + API: /governance-officer/designs/{}/implementation-resources/{}/attach +- detachImplementationResource: MISMATCH `detach_implementation_resource` + - PATH + SDK: /{}/designs/{}/implementation-resources/{}/detach + API: /governance-officer/designs/{}/implementation-resources/{}/detach +- linkApprovedPurpose: MISSING (`POST /governance-officer/elements/{}/approved-purposes/{}/attach`) +- detachApprovedPurpose: MISSING (`POST /governance-officer/elements/{}/approved-purposes/{}/detach`) +- linkGovernanceResults: MISMATCH `link_governance_results` + - PATH + SDK: /{}/governance-metrics/{}/measurements/{}/attach + API: /governance-officer/governance-metrics/{}/measurements/{}/attach +- detachGovernanceResults: MISMATCH `detach_governance_results` + - PATH + SDK: /{}/governance-metrics/{}/measurements/{}/detach + API: /governance-officer/governance-metrics/{}/measurements/{}/detach +- licenseElement: MISMATCH `license_element` + - PATH + SDK: /{}/elements/{}/license-types/{}/license + API: /governance-officer/elements/{}/license-types/{}/license +- updateLicense: ELSEWHERE -> `classification-explorer.py` +- unlicenseElement: ELSEWHERE -> `classification-explorer.py` +- certifyElement: MISMATCH `certify_element` + - PATH + SDK: /{}/elements/{}/certification-types/{}/certify + API: /governance-officer/elements/{}/certification-types/{}/certify +- updateCertification: ELSEWHERE -> `classification-explorer.py` +- decertifyElement: ELSEWHERE -> `classification-explorer.py` + +### Service: lineage-linker + +- linkLineage: MISMATCH `link_lineage` + - PATH + SDK: /lineage-linker/elements/{}/{}/{}/attach + API: /lineage-linker/from-elements/{}/via/{}/to-elements/{}/attach + +### Service: location-arena + +- linkPeerLocation: MISSING (`POST /location-arena/locations/{}/adjacent-locations/{}/attach`) + +### Service: metadata-expert + +- createMetadataElementInStore: MISSING (`POST /metadata-expert/metadata-elements`) +- updateMetadataElementInStore: MISSING (`POST /metadata-expert/metadata-elements/{}/update-properties`) +- updateMetadataElementEffectivityInStore: MISSING (`POST /metadata-expert/metadata-elements/{}/update-effectivity`) +- deleteMetadataElementInStore: MISSING (`POST /metadata-expert/metadata-elements/{}/delete`) +- archiveMetadataElementInStore: MISSING (`POST /metadata-expert/metadata-elements/{}/archive`) +- reclassifyMetadataElementInStore: MISSING (`POST /metadata-expert/metadata-elements/{}/classifications/{}/update-properties`) +- updateClassificationEffectivityInStore: MISSING (`POST /metadata-expert/metadata-elements/{}/classifications/{}/update-effectivity`) +- declassifyMetadataElementInStore: MISSING (`POST /metadata-expert/metadata-elements/{}/classifications/{}/delete`) +- createRelatedElementsInStore: MISSING (`POST /metadata-expert/related-elements`) +- updateRelatedElementsInStore: MISSING (`POST /metadata-expert/related-elements/{}/update-properties`) +- updateRelatedElementsEffectivityInStore: MISSING (`POST /metadata-expert/related-elements/{}/update-effectivity`) +- deleteRelatedElementsInStore: MISSING (`POST /metadata-expert/related-elements/{}/delete`) +- getMetadataElementByGUID: MISMATCH `get_metadata_element_by_guid` + - PATH + SDK: /{}/metadata-elements/{} + API: /metadata-expert/metadata-elements/{} +- getAnchoredElementsGraph: MISSING (`POST /metadata-expert/metadata-elements/{}/with-anchored-elements`) +- getMetadataElementByUniqueName: MISMATCH `get_metadata_element_by_unique_name` + - PATH + SDK: /{}/metadata-elements/by-unique-name + API: /metadata-expert/metadata-elements/by-unique-name +- getMetadataElementGUIDByUniqueName: MISMATCH `get_metadata_guid_by_unique_name` + - PATH + SDK: /{}/metadata-elements/guid-by-unique-name + API: /metadata-expert/metadata-elements/guid-by-unique-name + - BODY FilterRequestBody != UniqueNameRequestBody +- getClassificationHistory: MISMATCH `get_classification_history` + - PATH + SDK: /{}/metadata-elements/{}/classifications/{}/history + API: /metadata-expert/metadata-elements/{}/classifications/{}/history +- findMetadataElementsWithString: MISMATCH `find_metadata_elements_with_string` + - PATH + SDK: /{}/metadata-elements/by-search-string + API: /metadata-expert/metadata-elements/by-search-string +- findElementsForAnchor: MISMATCH `find_elements_for_anchor` + - PATH + SDK: /{}/metadata-elements/by-search-string/for-anchor/{} + API: /metadata-expert/metadata-elements/by-search-string/for-anchor/{} +- findElementsInAnchorDomain: MISMATCH `find_elements_in_anchor_domain` + - PATH + SDK: /{}/metadata-elements/by-search-string/in-anchor-domain/{} + API: /metadata-expert/metadata-elements/by-search-string/in-anchor-domain/{} +- findElementsInAnchorScope: MISMATCH `find_elements_in_anchor_scope` + - PATH + SDK: /{}/metadata-elements/by-search-string/in-anchor-scope/{} + API: /metadata-expert/metadata-elements/by-search-string/in-anchor-scope/{} +- getAllRelatedMetadataElements: MISSING (`POST /metadata-expert/related-elements/{}/any-type`) +- getRelatedMetadataElements: MISMATCH `get_related_metadata_elements` + - PATH + SDK: /{}/related-elements/{}/type/{} + API: /metadata-expert/related-elements/{}/type/{} +- getAllMetadataElementRelationships: MISMATCH `get_all_metadata_element_relationships` + - PATH + SDK: /{}/metadata-elements/{}/linked-by-any-type/to-elements/{} + API: /metadata-expert/metadata-elements/{}/linked-by-any-type/to-elements/{} +- getMetadataElementRelationships: MISMATCH `get_metadata_element_relationships` + - PATH + SDK: /{}/metadata-elements/{}/linked-by-type/{}/to-elements/{} + API: /metadata-expert/metadata-elements/{}/linked-by-type/{}/to-elements/{} +- findMetadataElements: MISMATCH `find_metadata_elements` + - PATH + SDK: /{}/metadata-elements/by-search-conditions + API: /metadata-expert/metadata-elements/by-search-conditions +- countMetadataElements: MISMATCH `count_metadata_elements` + - PATH + SDK: /{}/metadata-elements/by-search-conditions/count + API: /metadata-expert/metadata-elements/by-search-conditions/count +- findRelationshipsBetweenMetadataElements: MISSING (`POST /metadata-expert/relationships/by-search-conditions`) +- countRelationshipsBetweenMetadataElements: MISSING (`POST /metadata-expert/relationships/by-search-conditions/count`) +- getRelationshipByGUID: MISMATCH `get_relationship_by_guid` + - PATH + SDK: /{}/relationships/by-guid/{} + API: /metadata-expert/relationships/by-guid/{} +- getRelationshipHistory: MISMATCH `get_relationship_history` + - PATH + SDK: /{}/relationships/{}/history + API: /metadata-expert/relationships/{}/history + +### Service: my-profile + +- getMyProfile: MISMATCH `get_my_profile` + - VERB POST != GET +- Get My Profile: MISSING (`POST /my-profile`) +- Add My Profile: MISSING (`POST /my-profile/new`) + +### Service: notification-manager + + +### Service: people-organizer + + +### Service: platform-services + +- Get OMAG Server Platform Origin: MISSING (`GET /platform-services/server-platform/origin`) +- Get Active User List: MISSING (`GET /platform-services/server-platform/security/user-list`) +- Get Contractor User List: MISSING (`GET /platform-services/server-platform/security/user-list`) +- Get all known servers: MISSING (`GET /platform-services/server-platform/servers`) +- Query the status of a specific server: ELSEWHERE -> `server-operations.py` +- Query a connector: MISSING (`GET /platform-services/server-platform/connector-types/org.odpi.openmetadata.metadatasecurity.accessconnector.OpenMetadataAccessSecurityProvider`) +- Shutdown and unregister server from cohorts: MISSING (`DELETE /platform-services/server-platform/servers/{}`) +- Shutdown all active servers: MISSING (`DELETE /platform-services/server-platform/servers/instance`) +- Shutdown and unregister all active servers: MISSING (`DELETE /platform-services/server-platform/servers`) +- Shutdown server platform: MISSING (`DELETE /platform-services/server-platform/instance`) + +### Service: privacy-officer + +- linkPermittedProcessing: MISMATCH `link_permitted_processing` + - PATH + SDK: /{}/data-processing-purposes/{}/permitted-processing/{}/attach + API: /privacy-officer/data-processing-purposes/{}/permitted-processing/{}/attach +- detachPermittedProcessing: MISMATCH `detach_permitted_processing` + - PATH + SDK: /{}/data-processing-purposes/{}/permitted-processing/{}/detach + API: /privacy-officer/data-processing-purposes/{}/permitted-processing/{}/detach +- linkDataProcessingTarget: MISMATCH `link_data_processing_target` + - PATH + SDK: /{}/data-processing-actions/{}/targets/{}/attach + API: /privacy-officer/data-processing-actions/{}/targets/{}/attach +- detachDataProcessingTarget: MISMATCH `detach_data_processing_target` + - PATH + SDK: /{}/data-processing-actions/{}/targets/{}/detach + API: /privacy-officer/data-processing-actions/{}/targets/{}/detach + +### Service: product-catalog + +- find DigitalProductCatalogs: MISSING (`POST /product-catalog/collections/by-search-string`) +- find the open metadata product catalog: MISSING (`POST /product-catalog/collections/by-search-string`) +- find the valid metadata value list digital product: MISSING (`POST /product-catalog/collections/by-search-string`) +- getSolutionBlueprintsByName: ELSEWHERE -> `solution-architect.py` +- getTechnologyTypeDetail: MISSING (`POST /automated-curation/technology-types/by-name`) +- getTechnologyTypeTemplates: MISSING (`POST /automated-curation/technology-types/elements`) +- createElementFromTemplate: MISSING (`POST /automated-curation/catalog-templates/new-element`) +- getGovernanceActionProcessesByName: MISSING (`POST /product-catalog/governance-definitions/by-name`) +- getGovernanceActionProcessGraph: ELSEWHERE -> `governance-officer.py` +- initiateGovernanceActionProcess: MISSING (`POST /automated-curation/governance-action-processes/initiate`) +- findSubscriptions: MISSING (`POST /collection-manager/collections/by-search-string`) +- Get My Profile: MISSING (`POST /my-profile`) +- getCommunitiesByName: ELSEWHERE -> `community-matters.py` +- getNoteLogsByName: MISSING (`POST /feedback-manager/note-logs/by-name`) + +### Service: product-manager + +- updateDigitalProductStatus: MISSING (`POST /product-manager/collections/{}/update`) + +### Service: project-manager + +- createClassifiedProject: MISSING (`POST /project-manager/projects`) +- createCampaign: MISSING (`POST /project-manager/projects`) +- createTaskForProject: MISSING (`POST /project-manager/projects/{}/task`) +- setupProjectDependency: MISSING (`POST /project-manager/projects/{}/project-dependencies/{}/attach`) +- setupProjectHierarchy: MISSING (`POST /project-manager/projects/{}/project-hierarchies/{}/attach`) + +### Service: reference-data + + +### Service: runtime-manager + +- getPlatformsByDeployedImplementationType: MISSING (`POST /runtime-manager/platforms/by-deployed-implementation-type`) +- getPlatformTemplatesByDeployedImplementationType: MISSING (`POST /runtime-manager/platforms/by-deployed-implementation-type`) +- Get Connector Type: MISSING (`GET /runtime-manager/platforms/{}/connector-types/{}`) +- getElementsByCategory: MISSING (`POST /runtime-manager/elements/by-category`) +- getOMAGServerReport: MISSING (`GET /runtime-manager/omag-servers/{}/instance/report`) +- activateWithStoredConfig: MISSING (`POST /runtime-manager/omag-servers/{}/instance`) +- getConfigurationProperties: MISSING (`GET /runtime-manager/integration-daemons/{}/integration-connectors/{}/configuration-properties`) +- updateConfigurationProperties: MISSING (`POST /runtime-manager/integration-daemons/{}/integration-connectors/configuration-properties`) +- updateEndpointNetworkAddress: MISSING (`POST /runtime-manager/integration-daemons/{}/integration-connectors/{}/endpoint-network-address`) +- updateConnectorConnection: MISSING (`POST /runtime-manager/integration-daemons/{}/integration-connectors/{}/connection`) +- refreshConnectors: MISSING (`POST /runtime-manager/integration-daemons/{}/integration-connectors/refresh`) +- restartConnectors: MISSING (`POST /runtime-manager/integration-daemons/{}/integration-connectors/restart`) +- refreshIntegrationGroupConfig: MISSING (`GET /runtime-manager/integration-daemons/{}/integration-groups/{}/refresh-config`) +- refreshConfig: MISSING (`GET /runtime-manager/engine-hosts/{}/governance-engines/{}/refresh-config`) +- addOpenMetadataArchiveFile: MISSING (`POST /runtime-manager/omag-servers/{}/instance/load/open-metadata-archives/file`) +- addOpenMetadataArchiveContent: MISSING (`POST /runtime-manager/omag-servers/{}/instance/load/open-metadata-archives/archive-content`) +- createMetadataRepositoryCohort: MISSING (`POST /runtime-manager/metadata-repository-cohorts`) +- createMetadataRepositoryCohortFromTemplate: MISSING (`POST /runtime-manager/metadata-repository-cohorts/from-template`) +- updateMetadataRepositoryCohort: MISSING (`POST /runtime-manager/metadata-repository-cohorts/{}/update`) +- deleteMetadataRepositoryCohort: MISSING (`POST /runtime-manager/metadata-repository-cohorts/{}/delete`) +- getMetadataRepositoryCohortsByName: MISSING (`POST /runtime-manager/metadata-repository-cohorts/by-name`) +- findMetadataRepositoryCohorts: MISSING (`POST /runtime-manager/metadata-repository-cohorts/by-search-string`) +- getMetadataRepositoryCohortByGUID: MISSING (`POST /runtime-manager/metadata-repository-cohorts/{}/retrieve`) +- linkCohortMember: MISSING (`POST /runtime-manager/metadata-repository-cohorts/{}/cohort-members/{}/attach`) +- detachCohortMember: MISSING (`POST /runtime-manager/metadata-repository-cohorts/{}/cohort-members/{}/detach`) +- connectToCohortGet: MISSING (`GET /runtime-manager/cohort-members/{}/cohorts/{}/connect`) +- disconnectFromCohortGet: MISSING (`GET /runtime-manager/cohort-members/{}/cohorts/{}/disconnect`) +- unregisterFromCohortGet: MISSING (`GET /runtime-manager/cohort-members/{}/cohorts/{}/unregister`) + +### Service: schema-maker + +- deleteSchemaType: MISMATCH `delete_schema_type` + - BODY DeleteElementRequestBody != MetadataSourceRequestBody +- updateSchemaAttribute: MISMATCH `update_schema_attribute` + - PATH + SDK: /schema-maker/schema-attributes/{}/update + API: /schema-maker/schema-attributes/update +- deleteSchemaAttribute: MISMATCH `delete_schema_attribute` + - PATH + SDK: /schema-maker/schema-attributes/{}/delete + API: /schema-maker/schema-attributes/delete + - BODY DeleteElementRequestBody != MetadataSourceRequestBody +- getSchemaAttributeByGUID: MISMATCH `get_schema_attribute_by_guid` + - PATH + SDK: /schema-maker/schema-attributes/{}/retrieve + API: /schema-maker/schema-attributes/{}/retrieve"} + +### Service: security-officer + +- setSecurityAccessControl: MISMATCH `set_security_access_control` + - PATH + SDK: /{}/platforms/{}/security-access-control + API: /security-officer/platforms/{}/security-access-control +- getSecurityAccessControl: MISMATCH `get_security_access_control` + - PATH + SDK: /{}/platforms/{}/security-access-control/{} + API: /security-officer/platforms/{}/security-access-control/{} +- deleteSecurityAccessControl: MISMATCH `delete_security_access_control` + - PATH + SDK: /{}/platforms/{}/security-access-control/{} + API: /security-officer/platforms/{}/security-access-control/{} +- find Security Roles: MISSING (`POST /security-officer/collections/by-search-string`) +- find Security Groups: MISSING (`POST /security-officer/collections/by-search-string`) + +### Service: solution-architect + +- getDesignPatternsByName: MISMATCH `get_design_patterns_by_name` + - PATH + SDK: /solution-architect/design-patterns/by-name/{} + API: /solution-architect/design-patterns/by-name + - BODY SearchStringRequestBody != FilterRequestBody +- createInformationSupplyChain: MISSING (`POST /solution-architect/information-supply-chains`) +- createInformationSupplyChainFromTemplate: MISSING (`POST /solution-architect/information-supply-chains/from-template`) +- updateInformationSupplyChain: MISSING (`POST /solution-architect/information-supply-chains/{}/update`) +- linkPeersInInformationSupplyChain: MISSING (`POST /solution-architect/information-supply-chains/{}/peer-links/{}/attach`) +- unlinkPeerInformationSupplyChains: MISSING (`POST /solution-architect/information-supply-chains/{}/peer-links/{}/detach`) +- deleteInformationSupplyChain: MISSING (`POST /solution-architect/information-supply-chains/{}/delete`) +- findAllInformationSupplyChains - with full request body: MISSING (`POST /solution-architect/information-supply-chains/by-search-string`) +- findInformationSupplyChains - with full request body: MISSING (`POST /solution-architect/information-supply-chains/by-search-string`) +- getInformationSupplyChainsByName: MISSING (`POST /solution-architect/information-supply-chains/by-name`) +- getInformationSupplyChainsByName - with full request body: MISSING (`POST /solution-architect/information-supply-chains/by-name`) +- getInformationSupplyChainByGUID: MISSING (`POST /solution-architect/information-supply-chains/{}/retrieve`) +- getInformationSupplyChainByGUID - with request body: MISSING (`POST /solution-architect/information-supply-chains/{}/retrieve`) +- findAllSolutionBlueprints - with full request body: MISSING (`POST /solution-architect/solution-blueprints/by-search-string`) +- findSolutionBlueprints - with full request body: MISSING (`POST /solution-architect/solution-blueprints/by-search-string`) +- getSolutionBlueprintsByName - with full request body: MISSING (`POST /solution-architect/solution-blueprints/by-name`) +- getSolutionBlueprintByGUID - with request body: MISSING (`POST /solution-architect/solution-blueprints/{}/retrieve`) +- linkSolutionComponentActor: MISSING (`POST /solution-architect/solution-roles/{}/solution-component-actors/{}/attach`) +- detachSolutionComponentActor: MISSING (`POST /solution-architect/solution-roles/{}/solution-component-actors/{}/detach`) +- findAllSolutionRoles - with full request body: MISSING (`POST /solution-architect/solution-roles/by-search-string`) +- findSolutionRoles - with full request body: MISSING (`POST /solution-architect/solution-roles/by-search-string`) +- getSolutionRolesByName: MISMATCH `get_solution_roles_by_name` + - PATH + SDK: /solution-architect/solution-roles/by-name{} + API: /solution-architect/solution-roles/by-name +- getSolutionRolesByName - with full request body: MISSING (`POST /solution-architect/solution-roles/by-name`) +- getSolutionRoleByGUID - with request body: MISSING (`POST /solution-architect/solution-roles/{}/retrieve`) +- detachSubcomponent: MISSING (`POST /solution-architect/solution-components/{}/subcomponents/{}/detach`) +- detachAllSolutionLinkingWire: MISSING (`POST /solution-architect/solution-components/{}/wired-to/{}/detach`) +- detachSolutionLinkingWire: MISMATCH `detach_solution_linking_wire` + - PATH + SDK: /solution-architect/solution-components/{}/wired-to/{}/detach + API: /solution-architect/solution-components/wires/{}/detach +- findAllSolutionComponents - with full request body: MISSING (`POST /solution-architect/solution-components/by-search-string`) +- findSolutionComponents - with full request body: MISSING (`POST /solution-architect/solution-components/by-search-string`) +- getSolutionComponentsByName: MISMATCH `get_solution_components_by_name` + - PATH + SDK: /solution-architect/solution-components/by-name{} + API: /solution-architect/solution-components/by-name +- getSolutionComponentsByName - with full request body: MISSING (`POST /solution-architect/solution-components/by-name`) +- getSolutionComponentByGUID - with request body: MISSING (`POST /solution-architect/solution-components/{}/retrieve`) +- getSolutionComponentImplementations: MISMATCH `get_solution_component_implementations` + - PATH + SDK: /solution-architect/solution-components/{}/implementations{} + API: /solution-architect/solution-components/{}/implementations + +### Service: subject-area + +- linkSubjectAreas: MISSING (`POST /subject-area/collections/{}/collection-hierarchies/{}/attach`) +- detachSubjectAreas: MISSING (`POST /subject-area/collections/{}/collection-hierarchies/{}/detach`) +- findAllSubjectAreas: MISSING (`POST /subject-area/collectionss/by-search-string`) +- findAllSubjectAreas - with full request body: MISSING (`POST /subject-area/collections/by-search-string`) +- findSubjectAreas - with full request body: MISSING (`POST /subject-area/collections/by-search-string`) +- getSubjectAreasByName - with full request body: MISSING (`POST /subject-area/collections/by-name`) +- getSubjectAreaByGUID - with request body: MISSING (`POST /subject-area/collections/{}/retrieve`) + +### Service: template-manager + + +### Service: time-keeper + + +### Service: valid-metadata + +- setUpValidMetadataValue: MISSING (`POST /valid-metadata/setup-value/{}`) +- setUpValidMetadataMapName: MISSING (`POST /valid-metadata/setup-map-name/{}`) +- setUpValidMetadataMapValue: MISSING (`POST /valid-metadata/setup-map-value/{}/{}`) +- getAllTypes: MISSING (`GET /valid-metadata/open-metadata-types`) +- getEntityDefs: MISSING (`GET /valid-metadata/open-metadata-types/entity-defs`) +- getRelationshipDefs: MISSING (`GET /valid-metadata/open-metadata-types/relationship-defs`) +- getClassificationDefs: MISSING (`GET /valid-metadata/open-metadata-types/classification-defs`) +- getAttributeTypes: MISSING (`GET /valid-metadata/open-metadata-types/attribute-defs`) +- getTypeDefByName: MISSING (`GET /valid-metadata/open-metadata-types/name/{}`) +- setUpSpecificationProperty: MISSING (`POST /valid-metadata/elements/{}/specification-properties`) diff --git a/pyegeria/omvs/action_author.py b/pyegeria/omvs/action_author.py index ff4922af..5a023e07 100644 --- a/pyegeria/omvs/action_author.py +++ b/pyegeria/omvs/action_author.py @@ -374,7 +374,7 @@ async def _async_get_governance_action_process( dict | list | str The governance action process. """ - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/action-author/governance-action-processes/{process_guid}/retrieve" + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/governance-officer/governance-definitions/{process_guid}/retrieve" return await self._async_get_guid_request( url, "GovernanceActionProcess", @@ -446,7 +446,7 @@ async def _async_get_governance_action_process_graph( dict | list | str The governance action process graph. """ - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/action-author/governance-action-processes/{process_guid}/graph" + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/governance-officer/governance-action-processes/{process_guid}/graph" return await self._async_get_guid_request( url, "GovernanceActionProcess", diff --git a/pyegeria/omvs/actor_manager.py b/pyegeria/omvs/actor_manager.py index 5aadcadd..2212ac9b 100644 --- a/pyegeria/omvs/actor_manager.py +++ b/pyegeria/omvs/actor_manager.py @@ -578,7 +578,7 @@ async def _async_detach_asset_from_profile(self, asset_guid: str, it_profile_gui "forDuplicateProcessing": false } """ - url = (f"{self.command_root}/assets/{asset_guid}/it_profiles/{it_profile_guid}/detach") + url = (f"{self.command_root}/assets/{asset_guid}/it-profiles/{it_profile_guid}/detach") await self._async_delete_element_request(url, body) logger.debug(f"Detached asset {asset_guid} from it profile {it_profile_guid}") diff --git a/pyegeria/omvs/asset_maker.py b/pyegeria/omvs/asset_maker.py index ece8c0e7..abf44048 100644 --- a/pyegeria/omvs/asset_maker.py +++ b/pyegeria/omvs/asset_maker.py @@ -1651,7 +1651,7 @@ async def _async_detach_catalog_target( "class" : "DeleteRelationshipRequestBody" } """ - url = f"{self.asset_command_root}/integration-connectors/{integration_connector_guid}/catalog-targets/{metadata_element_guid}/delete" + url = f"{self.asset_command_root}/integration-connectors/{integration_connector_guid}/catalog-targets/{metadata_element_guid}/detach" await self._async_delete_relationship_request(url, body) @dynamic_catch @@ -4857,7 +4857,7 @@ async def _async_get_actions_for_requester( ------- list | dict | str """ - url = f"{self.asset_command_root}/actions/by-requester/{metadata_element_guid}" + url = f"{self.asset_command_root}/elements/{metadata_element_guid}/requested/actions" return await self._async_activity_status_request( url, _type="Action", diff --git a/pyegeria/omvs/collection_manager.py b/pyegeria/omvs/collection_manager.py index 69f7ab14..bb7e9e72 100644 --- a/pyegeria/omvs/collection_manager.py +++ b/pyegeria/omvs/collection_manager.py @@ -5410,7 +5410,7 @@ async def _async_detach_agreement_item(self, agreement_guid: str, agreement_item """ url = ( f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections" - f"/agreements" + f"/agreements/" f"{agreement_guid}/agreement-items/{agreement_item_guid}/detach") await self._async_delete_relationship_request(url, body) logger.info(f"Detached agreement item {agreement_item_guid} from {agreement_guid}") diff --git a/pyegeria/omvs/data_discovery.py b/pyegeria/omvs/data_discovery.py index ad2a43a6..9ec8cf4d 100644 --- a/pyegeria/omvs/data_discovery.py +++ b/pyegeria/omvs/data_discovery.py @@ -17,6 +17,7 @@ UpdateElementRequestBody, TemplateRequestBody, FilterRequestBody, + ResultsRequestBody, SearchStringRequestBody, GetRequestBody, NewRelationshipRequestBody, @@ -333,6 +334,479 @@ def update_annotation(self, annotation_guid: str, body: dict | UpdateElementRequ loop = asyncio.get_event_loop() loop.run_until_complete(self._async_update_annotation(annotation_guid, body)) + async def _async_attach_annotation_to_report( + self, + survey_report_guid: str, + annotation_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link an annotation to its survey report. Async version. + + Parameters + ---------- + survey_report_guid : str + The unique identifier of the survey report. + annotation_guid : str + The unique identifier of the annotation. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/survey-reports/{survey_report_guid}/new-annotations/{annotation_guid}/attach" + await self._async_new_relationship_request(url, ["SurveyReportAnnotationRelationship"], body) + + def attach_annotation_to_report( + self, + survey_report_guid: str, + annotation_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link an annotation to its survey report. + + Parameters + ---------- + survey_report_guid : str + The unique identifier of the survey report. + annotation_guid : str + The unique identifier of the annotation. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_attach_annotation_to_report(survey_report_guid, annotation_guid, body) + ) + + async def _async_detach_annotation_from_report( + self, + survey_report_guid: str, + annotation_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach an annotation from its survey report. Async version. + + Parameters + ---------- + survey_report_guid : str + The unique identifier of the survey report. + annotation_guid : str + The unique identifier of the annotation. + body : dict | DeleteRelationshipRequestBody + The properties for the relationship deletion. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/survey-reports/{survey_report_guid}/new-annotations/{annotation_guid}/detach" + await self._async_delete_relationship_request(url, body) + + def detach_annotation_from_report( + self, + survey_report_guid: str, + annotation_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach an annotation from its survey report. + + Parameters + ---------- + survey_report_guid : str + The unique identifier of the survey report. + annotation_guid : str + The unique identifier of the annotation. + body : dict | DeleteRelationshipRequestBody + The properties for the relationship deletion. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_detach_annotation_from_report(survey_report_guid, annotation_guid, body) + ) + + async def _async_link_annotation_to_described_element( + self, + annotation_guid: str, + element_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link an annotation to the element it describes. Async version. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the described element. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/elements/{element_guid}/associated-annotations/{annotation_guid}/attach" + await self._async_new_relationship_request(url, ["AnnotationDescribedElementRelationship"], body) + + def link_annotation_to_described_element( + self, + annotation_guid: str, + element_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link an annotation to the element it describes. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the described element. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_link_annotation_to_described_element(annotation_guid, element_guid, body) + ) + + @dynamic_catch + async def _async_detach_annotation_from_described_element( + self, + annotation_guid: str, + element_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach an annotation from the element it describes. Async version.""" + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/elements/{element_guid}/associated-annotations/{annotation_guid}/detach" + await self._async_delete_relationship_request(url, body) + + def detach_annotation_from_described_element( + self, + annotation_guid: str, + element_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach an annotation from the element it describes.""" + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_detach_annotation_from_described_element(annotation_guid, element_guid, body) + ) + + async def _async_link_annotation_to_its_predecessor( + self, + annotation_guid: str, + predecessor_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link an annotation to its predecessor. Async version. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + predecessor_guid : str + The unique identifier of the predecessor annotation. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{predecessor_guid}/later-annotations/{annotation_guid}/attach" + await self._async_new_relationship_request(url, ["AnnotationPredecessorRelationship"], body) + + def link_annotation_to_its_predecessor( + self, + annotation_guid: str, + predecessor_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link an annotation to its predecessor. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + predecessor_guid : str + The unique identifier of the predecessor annotation. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_link_annotation_to_its_predecessor(annotation_guid, predecessor_guid, body) + ) + + async def _async_detach_annotation_from_its_predecessor( + self, + annotation_guid: str, + predecessor_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach an annotation from its predecessor. Async version. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + predecessor_guid : str + The unique identifier of the predecessor annotation. + body : dict | DeleteRelationshipRequestBody + The properties for the relationship deletion. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{predecessor_guid}/later-annotations/{annotation_guid}/detach" + await self._async_delete_relationship_request(url, body) + + def detach_annotation_from_its_predecessor( + self, + annotation_guid: str, + predecessor_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach an annotation from its predecessor. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + predecessor_guid : str + The unique identifier of the predecessor annotation. + body : dict | DeleteRelationshipRequestBody + The properties for the relationship deletion. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_detach_annotation_from_its_predecessor(annotation_guid, predecessor_guid, body) + ) + + async def _async_link_resource_profile_data( + self, + annotation_guid: str, + element_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link resource profile data to an annotation. Async version. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the resource profile data element. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{annotation_guid}/resource-profile-data-assets/{element_guid}/attach" + await self._async_new_relationship_request(url, ["ResourceProfileDataRelationship"], body) + + def link_resource_profile_data( + self, + annotation_guid: str, + element_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link resource profile data to an annotation. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the resource profile data element. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_link_resource_profile_data(annotation_guid, element_guid, body) + ) + + async def _async_detach_resource_profile_data( + self, + annotation_guid: str, + element_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach resource profile data from an annotation. Async version. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the resource profile data element. + body : dict | DeleteRelationshipRequestBody + The properties for the relationship deletion. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{annotation_guid}/resource-profile-data-assets/{element_guid}/detach" + await self._async_delete_relationship_request(url, body) + + def detach_resource_profile_data( + self, + annotation_guid: str, + element_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach resource profile data from an annotation. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the resource profile data element. + body : dict | DeleteRelationshipRequestBody + The properties for the relationship deletion. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_detach_resource_profile_data(annotation_guid, element_guid, body) + ) + + async def _async_link_annotation_match( + self, + annotation_guid: str, + element_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link an annotation match. Async version. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the matched element. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{annotation_guid}/matched-elements/{element_guid}/attach" + await self._async_new_relationship_request(url, ["AnnotationMatchRelationship"], body) + + def link_annotation_match( + self, + annotation_guid: str, + element_guid: str, + body: dict | NewRelationshipRequestBody, + ) -> None: + """Link an annotation match. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the matched element. + body : dict | NewRelationshipRequestBody + The properties for the relationship. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_link_annotation_match(annotation_guid, element_guid, body) + ) + + async def _async_detach_annotation_match( + self, + annotation_guid: str, + element_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach an annotation match. Async version. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the matched element. + body : dict | DeleteRelationshipRequestBody + The properties for the relationship deletion. + + Returns + ------- + None + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{annotation_guid}/matched-elements/{element_guid}/detach" + await self._async_delete_relationship_request(url, body) + + def detach_annotation_match( + self, + annotation_guid: str, + element_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach an annotation match. + + Parameters + ---------- + annotation_guid : str + The unique identifier of the annotation. + element_guid : str + The unique identifier of the matched element. + body : dict | DeleteRelationshipRequestBody + The properties for the relationship deletion. + + Returns + ------- + None + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_detach_annotation_match(annotation_guid, element_guid, body) + ) + @dynamic_catch async def _async_delete_annotation( self, annotation_guid: str, body: dict | DeleteElementRequestBody @@ -540,6 +1014,340 @@ def get_annotations_by_name( ) ) + @dynamic_catch + async def _async_get_annotations_by_analysis_step( + self, + analysis_step: str, + body: Optional[dict | FilterRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get annotations by analysis step. Async version. + + Notes + ----- + Sample JSON body: + ```json + { + "class" : "FilterRequestBody", + "filter" : "AnalysisStepName", + "startFrom": 0, + "pageSize": 10 + } + ``` + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/by-analysis-step" + return await self._async_get_name_request( + url, + _type="Annotation", + _gen_output=self._generate_annotation_output, + filter_string=analysis_step, + start_from=start_from, + page_size=page_size, + output_format=output_format, + report_spec=report_spec, + body=body, + **kwargs, + ) + + def get_annotations_by_analysis_step( + self, + analysis_step: str, + body: Optional[dict | FilterRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get annotations by analysis step.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete( + self._async_get_annotations_by_analysis_step( + analysis_step, body, start_from, page_size, output_format, report_spec, **kwargs + ) + ) + + @dynamic_catch + async def _async_get_annotations_by_annotation_type( + self, + annotation_type: str, + body: Optional[dict | FilterRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get annotations by annotation type. Async version. + + Notes + ----- + Sample JSON body: + ```json + { + "class" : "FilterRequestBody", + "filter" : "AnnotationTypeName", + "startFrom": 0, + "pageSize": 10 + } + ``` + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/by-annotation-type" + return await self._async_get_name_request( + url, + _type="Annotation", + _gen_output=self._generate_annotation_output, + filter_string=annotation_type, + start_from=start_from, + page_size=page_size, + output_format=output_format, + report_spec=report_spec, + body=body, + **kwargs, + ) + + def get_annotations_by_annotation_type( + self, + annotation_type: str, + body: Optional[dict | FilterRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get annotations by annotation type.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete( + self._async_get_annotations_by_annotation_type( + annotation_type, body, start_from, page_size, output_format, report_spec, **kwargs + ) + ) + + @dynamic_catch + async def _async_get_annotations_for_element( + self, + element_guid: str, + body: Optional[dict | ResultsRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get the annotations associated with an element. Async version. + + Notes + ----- + Sample JSON body: + ```json + { + "class" : "ResultsRequestBody", + "startFrom": 0, + "pageSize": 10 + } + ``` + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/elements/{element_guid}/associated-annotations" + return await self._async_get_results_body_request( + url, + _type="Annotation", + _gen_output=self._generate_annotation_output, + start_from=start_from, + page_size=page_size, + output_format=output_format, + report_spec=report_spec, + body=body, + **kwargs, + ) + + def get_annotations_for_element( + self, + element_guid: str, + body: Optional[dict | ResultsRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get the annotations associated with an element.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete( + self._async_get_annotations_for_element( + element_guid, body, start_from, page_size, output_format, report_spec, **kwargs + ) + ) + + @dynamic_catch + async def _async_get_new_annotations( + self, + survey_report_guid: str, + body: Optional[dict | ResultsRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get the new annotations for a survey report. Async version. + + Notes + ----- + Sample JSON body: + ```json + { + "class" : "ResultsRequestBody", + "startFrom": 0, + "pageSize": 10 + } + ``` + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/survey-reports/{survey_report_guid}/new-annotations" + return await self._async_get_results_body_request( + url, + _type="Annotation", + _gen_output=self._generate_annotation_output, + start_from=start_from, + page_size=page_size, + output_format=output_format, + report_spec=report_spec, + body=body, + **kwargs, + ) + + def get_new_annotations( + self, + survey_report_guid: str, + body: Optional[dict | ResultsRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get the new annotations for a survey report.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete( + self._async_get_new_annotations( + survey_report_guid, body, start_from, page_size, output_format, report_spec, **kwargs + ) + ) + + @dynamic_catch + async def _async_get_annotation_extensions( + self, + annotation_guid: str, + body: Optional[dict | ResultsRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get the annotations that extend an annotation. Async version. + + Notes + ----- + Sample JSON body: + ```json + { + "class" : "ResultsRequestBody", + "startFrom": 0, + "pageSize": 10 + } + ``` + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{annotation_guid}/annotation-extensions" + return await self._async_get_results_body_request( + url, + _type="Annotation", + _gen_output=self._generate_annotation_output, + start_from=start_from, + page_size=page_size, + output_format=output_format, + report_spec=report_spec, + body=body, + **kwargs, + ) + + def get_annotation_extensions( + self, + annotation_guid: str, + body: Optional[dict | ResultsRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get the annotations that extend an annotation.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete( + self._async_get_annotation_extensions( + annotation_guid, body, start_from, page_size, output_format, report_spec, **kwargs + ) + ) + + @dynamic_catch + async def _async_get_previous_annotations( + self, + annotation_guid: str, + body: Optional[dict | ResultsRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get the annotations that precede an annotation. Async version. + + Notes + ----- + Sample JSON body: + ```json + { + "class" : "ResultsRequestBody", + "startFrom": 0, + "pageSize": 10 + } + ``` + """ + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{annotation_guid}/previous-annotations" + return await self._async_get_results_body_request( + url, + _type="Annotation", + _gen_output=self._generate_annotation_output, + start_from=start_from, + page_size=page_size, + output_format=output_format, + report_spec=report_spec, + body=body, + **kwargs, + ) + + def get_previous_annotations( + self, + annotation_guid: str, + body: Optional[dict | ResultsRequestBody] = None, + start_from: int = 0, + page_size: int = 0, + output_format: str = "JSON", + report_spec: str | dict = "Annotations", + **kwargs, + ) -> str | list[dict]: + """Get the annotations that precede an annotation.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete( + self._async_get_previous_annotations( + annotation_guid, body, start_from, page_size, output_format, report_spec, **kwargs + ) + ) + @dynamic_catch async def _async_find_annotations( self, @@ -972,27 +1780,6 @@ def detach_analysis_report_from_asset(self, analysis_report_guid: str, asset_gui loop = asyncio.get_event_loop() loop.run_until_complete(self._async_detach_analysis_report_from_asset(analysis_report_guid, asset_guid, body)) - @dynamic_catch - async def _async_link_annotation_to_matched_element(self, annotation_guid: str, element_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None) -> None: - """Link an annotation to a matched element. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{annotation_guid}/matched-elements/{element_guid}/attach" - await self._async_new_relationship_request(url, ["MatchedElement"], body) - - def link_annotation_to_matched_element(self, annotation_guid: str, element_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None) -> None: - """Link an annotation to a matched element.""" - loop = asyncio.get_event_loop() - loop.run_until_complete(self._async_link_annotation_to_matched_element(annotation_guid, element_guid, body)) - - @dynamic_catch - async def _async_detach_annotation_from_matched_element(self, annotation_guid: str, element_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None: - """Detach an annotation from a matched element. Async version.""" - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/data-discovery/annotations/{annotation_guid}/matched-elements/{element_guid}/detach" - await self._async_delete_relationship_request(url, body) - - def detach_annotation_from_matched_element(self, annotation_guid: str, element_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None: - """Detach an annotation from a matched element.""" - loop = asyncio.get_event_loop() - loop.run_until_complete(self._async_detach_annotation_from_matched_element(annotation_guid, element_guid, body)) @dynamic_catch async def _async_link_request_for_action_target(self, annotation_guid: str, element_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None) -> None: diff --git a/pyegeria/omvs/governance_officer.py b/pyegeria/omvs/governance_officer.py index 5737c659..5c5f84a1 100644 --- a/pyegeria/omvs/governance_officer.py +++ b/pyegeria/omvs/governance_officer.py @@ -2830,7 +2830,7 @@ async def _async_get_governance_action_process( dict | list | str The governance action process. """ - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/governance-officer/governance-action-processes/{guid}/retrieve" + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/governance-officer/governance-definitions/{guid}/retrieve" type = element_type if element_type else "GovernanceActionProcess" return await self._async_get_guid_request( url, diff --git a/pyegeria/omvs/metadata_expert.py b/pyegeria/omvs/metadata_expert.py index 47ae61a3..4a3fe662 100644 --- a/pyegeria/omvs/metadata_expert.py +++ b/pyegeria/omvs/metadata_expert.py @@ -137,6 +137,7 @@ def create_metadata_element(self, body: Optional[dict | NewOpenMetadataElementRe loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_create_metadata_element(body)) + @dynamic_catch async def _async_create_metadata_element_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str: """ @@ -197,6 +198,7 @@ def create_metadata_element_from_template(self, body: Optional[dict | TemplateRe loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_create_metadata_element_from_template(body)) + @dynamic_catch async def _async_update_metadata_element_properties(self, metadata_element_guid: str, body: Optional[dict | UpdatePropertiesRequestBody] = None) -> None: """ @@ -963,12 +965,12 @@ def get_metadata_guid_by_unique_name( property_name: str = "qualifiedName", as_of_time: Optional[str] = None, body: Optional[dict | FilterRequestBody] = None, - **kwargs, + **kwargs ) -> str: """ Retrieve the metadata element GUID using its unique name (typically the qualified name, but it is possible to specify a different property name in the request body as long as it is unique). - If multiple matching instances are found, an exception is thrown. Async version. + If multiple matching instances are found, an exception is thrown. Parameters ---------- @@ -1544,6 +1546,42 @@ def get_element_history( ) return response + @dynamic_catch + async def _async_get_metadata_element_history( + self, + metadata_element_guid: str, + effective_time: Optional[str] = None, + oldest_first: bool = False, + body: Optional[dict] = None, + **kwargs, + ) -> list | str: + """Retrieve the history of a metadata element. Async version.""" + if body is None: + body = { + "class": "HistoryRequestBody", + "effectiveTime": effective_time, + "oldestFirst": oldest_first, + } + url = f"{self.command_root}/metadata-elements/{metadata_element_guid}/history" + return await self._async_get_guid_request( + url, _type="MetadataElement", _gen_output=self._generate_referenceable_output, body=body, **kwargs + ) + + @dynamic_catch + def get_metadata_element_history( + self, + metadata_element_guid: str, + effective_time: Optional[str] = None, + oldest_first: bool = False, + body: Optional[dict] = None, + **kwargs, + ) -> list | str: + """Retrieve the history of a metadata element.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete( + self._async_get_metadata_element_history(metadata_element_guid, effective_time, oldest_first, body, **kwargs) + ) + @dynamic_catch async def _async_get_classification_history( self, @@ -3981,3 +4019,29 @@ def get_relationship_history( return response + + @dynamic_catch + async def _async_get_match_criteria_list(self) -> list: + """Retrieve the list of valid match criteria. Async version.""" + url = f"{self.command_root}/metadata-search/match-criteria-values" + response = await self._async_make_request("GET", url) + return response.json().get("list") + + @dynamic_catch + def get_match_criteria_list(self) -> list: + """Retrieve the list of valid match criteria.""" + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_get_match_criteria_list()) + + @dynamic_catch + async def _async_get_property_comparison_operator_list(self) -> list: + """Retrieve the list of valid property comparison operators. Async version.""" + url = f"{self.command_root}/metadata-search/property-comparison-operator-values" + response = await self._async_make_request("GET", url) + return response.json().get("list") + + @dynamic_catch + 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()) diff --git a/pyegeria/omvs/people_organizer.py b/pyegeria/omvs/people_organizer.py index 8e4bf49c..5adfcc31 100644 --- a/pyegeria/omvs/people_organizer.py +++ b/pyegeria/omvs/people_organizer.py @@ -184,6 +184,47 @@ async def _async_unlink_peer_person( url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/people-organizer/actor-profiles/{person_one_guid}/peer-persons/{person_two_guid}/detach" await self._async_delete_relationship_request(url, body) + def unlink_peer_person( + self, + person_one_guid: str, + person_two_guid: str, + body: dict | DeleteRelationshipRequestBody, + ) -> None: + """Detach a person profile from one of its peers. + + Parameters + ---------- + person_one_guid : str + The unique identifier of the first person profile. + person_two_guid : str + The unique identifier of the second person profile. + body : dict | DeleteRelationshipRequestBody + The request body for the detach operation. + + Returns + ------- + None + + Raises + ------ + PyegeriaException + If there are issues in communications, message format, or Egeria errors. + + Notes + ----- + Sample JSON body: + ```json + { + "class" : "DeleteRelationshipRequestBody", + "deleteMethod": "LOOK_FOR_LINEAGE" + } + ``` + """ + loop = asyncio.get_event_loop() + loop.run_until_complete( + self._async_unlink_peer_person(person_one_guid, person_two_guid, body) + ) + def detach_peer_person( self, person_one_guid: str, diff --git a/pyegeria/omvs/platform_services.py b/pyegeria/omvs/platform_services.py index 875356df..143c6e28 100644 --- a/pyegeria/omvs/platform_services.py +++ b/pyegeria/omvs/platform_services.py @@ -59,63 +59,11 @@ def __init__( + "server-platform" ) - def get_platform_origin(self) -> str: - """Get the version and origin of the platform software - - /open-metadata/platform-services/server-platform/origin - Response from this call is a string not JSON.. - - Parameters - ---------- - - Returns - ------- - String with the platform origin information. Also throws exceptions if no viable server or endpoint errors - - Raises - ------ - PyegeriaInvalidParameterException - If the client passes incorrect parameters on the request — such as bad URLs or invalid values. - PyegeriaAPIException - Raised by the server when an issue arises in processing a valid request. - PyegeriaUnauthorizedException - The principal specified by the `user_id` does not have authorization for the requested action. - """ - - global response - calling_frame = inspect.currentframe().f_back - caller_method = inspect.getframeinfo(calling_frame).function - class_name = __class__.__name__ - - # url = f"{self.platform_url}/open-metadata/platform-services/server-platform/origin" - url = f"{self.platform_url}/api/about" - - local_session = httpx.Client(verify=enable_ssl_check) - response = " " - try: - response = local_session.get(url) - if response.status_code != 200: - # Server returned non-200; raise API exception with response context - raise PyegeriaAPIException(response) - else: - return response.text - except PyegeriaException: - raise - - except ( - httpx.NetworkError, - httpx.ProtocolError, - httpx.HTTPStatusError, - httpx.TimeoutException, - ) as e: - msg = ( - f"Client error in {caller_method} for {class_name} calling {url}: {str(e)}" - ) - raise PyegeriaConnectionException( - context={"caller_method": caller_method, "class": class_name, "url": url}, - additional_info={"message": msg}, - e=e, - ) + # get_platform_origin()/async_get_platform_origin() are inherited from + # BasePlatformClient. They already target + # /open-metadata/platform-services/server-platform/origin and pass + # is_json=False, which this endpoint requires -- it returns plain text, so + # parsing it as JSON raises PyegeriaInvalidParameterException. async def _async_activate_server_stored_config( self, server: Optional[str] = None, timeout: int = 60 @@ -205,7 +153,7 @@ async def _async_activate_server_supplied_config( if server is None: server = self.server_name - url = self.admin_command_root + "/servers/" + server + "/instance/configuration" + url = self.admin_command_root + "/servers/" + server + "/instance" await self._async_make_request("POST", url, config_body, timeout=timeout) def activate_server_supplied_config( @@ -1092,9 +1040,12 @@ def get_security_user_list(self, status: str = None, user_type: str = None) -> l loop = asyncio.get_event_loop() return loop.run_until_complete(self._async_get_security_user_list(status, user_type)) - async def _async_get_registered_services(self, service_category: str) -> list: + async def _async_get_registered_services(self, service_category: Optional[str] = None) -> list: """Retrieve the list of registered services for a category. Async version.""" - url = f"{self.admin_command_root}/registered-services/{service_category}" + if service_category and service_category != "all-services": + url = f"{self.admin_command_root}/registered-services/{service_category}" + else: + url = f"{self.admin_command_root}/registered-services" response = await self._async_make_request("GET", url) return response.json().get("services") diff --git a/pyegeria/omvs/server_operations.py b/pyegeria/omvs/server_operations.py index d0385344..f6fbf1ec 100644 --- a/pyegeria/omvs/server_operations.py +++ b/pyegeria/omvs/server_operations.py @@ -39,8 +39,9 @@ def __init__( token: Optional[str] = None, timeout: int = None): Platform.__init__(self, server_name, platform_url, user_id, user_pwd, token=token) + # No trailing slash - every call site below appends "/servers/...". self.ops_command_root = ( - f"{self.platform_url}/open-metadata/server-operations/" + f"{self.platform_url}/open-metadata/server-operations" ) async def _async_get_active_configuration(self, server: str = None) -> dict | str: @@ -221,7 +222,7 @@ async def _async_get_active_server_status(self, server: str = None) -> dict: if server is None: server = self.server_name - url = self.ops_command_root + "servers/" + server + "/instance/status" + url = self.ops_command_root + "/servers/" + server + "/instance/status" response = await self._async_make_request("GET", url) return response diff --git a/pyegeria/omvs/solution_architect.py b/pyegeria/omvs/solution_architect.py index 57fca51a..99698400 100644 --- a/pyegeria/omvs/solution_architect.py +++ b/pyegeria/omvs/solution_architect.py @@ -133,7 +133,8 @@ async def _async_create_design_pattern(self, body: dict | NewElementRequestBody) def create_design_pattern(self, body: dict | NewElementRequestBody): """Create a design pattern.""" - return asyncio.run(self._async_create_design_pattern(body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_create_design_pattern(body)) async def _async_create_design_pattern_from_template(self, body: dict | TemplateRequestBody): url = f"{self.solution_architect_command_root}/design-patterns/from-template" @@ -142,7 +143,8 @@ async def _async_create_design_pattern_from_template(self, body: dict | Template def create_design_pattern_from_template(self, body: dict | TemplateRequestBody): """Create a design pattern from template.""" - return asyncio.run(self._async_create_design_pattern_from_template(body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_create_design_pattern_from_template(body)) async def _async_update_design_pattern(self, guid: str, body: dict | UpdateElementRequestBody): validate_guid(guid) @@ -151,7 +153,8 @@ async def _async_update_design_pattern(self, guid: str, body: dict | UpdateEleme def update_design_pattern(self, guid: str, body: dict | UpdateElementRequestBody): """Update a design pattern.""" - return asyncio.run(self._async_update_design_pattern(guid, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_update_design_pattern(guid, body)) async def _async_link_nested_design_patterns(self, parent_guid: str, child_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None): validate_guid(parent_guid) @@ -161,7 +164,8 @@ async def _async_link_nested_design_patterns(self, parent_guid: str, child_guid: def link_nested_design_patterns(self, parent_guid: str, child_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None): """Link nested design patterns.""" - return asyncio.run(self._async_link_nested_design_patterns(parent_guid, child_guid, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_link_nested_design_patterns(parent_guid, child_guid, body)) async def _async_detach_nested_design_patterns(self, parent_guid: str, child_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None): validate_guid(parent_guid) @@ -171,7 +175,8 @@ async def _async_detach_nested_design_patterns(self, parent_guid: str, child_gui def detach_nested_design_patterns(self, parent_guid: str, child_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None): """Detach nested design patterns.""" - return asyncio.run(self._async_detach_nested_design_patterns(parent_guid, child_guid, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_detach_nested_design_patterns(parent_guid, child_guid, body)) async def _async_link_specialized_design_patterns(self, general_guid: str, special_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None): validate_guid(general_guid) @@ -181,7 +186,8 @@ async def _async_link_specialized_design_patterns(self, general_guid: str, speci def link_specialized_design_patterns(self, general_guid: str, special_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None): """Link specialized design patterns.""" - return asyncio.run(self._async_link_specialized_design_patterns(general_guid, special_guid, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_link_specialized_design_patterns(general_guid, special_guid, body)) async def _async_detach_specialized_design_patterns(self, general_guid: str, special_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None): validate_guid(general_guid) @@ -191,7 +197,8 @@ async def _async_detach_specialized_design_patterns(self, general_guid: str, spe def detach_specialized_design_patterns(self, general_guid: str, special_guid: str, body: Optional[dict | DeleteRelationshipRequestBody] = None): """Detach specialized design patterns.""" - return asyncio.run(self._async_detach_specialized_design_patterns(general_guid, special_guid, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_detach_specialized_design_patterns(general_guid, special_guid, body)) async def _async_link_related_design_patterns(self, guid1: str, guid2: str, body: Optional[dict | NewRelationshipRequestBody] = None): validate_guid(guid1) @@ -201,7 +208,8 @@ async def _async_link_related_design_patterns(self, guid1: str, guid2: str, body def link_related_design_patterns(self, guid1: str, guid2: str, body: Optional[dict | NewRelationshipRequestBody] = None): """Link related design patterns.""" - return asyncio.run(self._async_link_related_design_patterns(guid1, guid2, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_link_related_design_patterns(guid1, guid2, body)) async def _async_detach_related_design_patterns(self, guid1: str, guid2: str, body: Optional[dict | DeleteRelationshipRequestBody] = None): validate_guid(guid1) @@ -211,7 +219,8 @@ async def _async_detach_related_design_patterns(self, guid1: str, guid2: str, bo def detach_related_design_patterns(self, guid1: str, guid2: str, body: Optional[dict | DeleteRelationshipRequestBody] = None): """Detach related design patterns.""" - return asyncio.run(self._async_detach_related_design_patterns(guid1, guid2, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_detach_related_design_patterns(guid1, guid2, body)) async def _async_delete_design_pattern(self, guid: str, body: Optional[dict | DeleteElementRequestBody] = None): validate_guid(guid) @@ -220,7 +229,8 @@ async def _async_delete_design_pattern(self, guid: str, body: Optional[dict | De def delete_design_pattern(self, guid: str, body: Optional[dict | DeleteElementRequestBody] = None): """Delete a design pattern.""" - return asyncio.run(self._async_delete_design_pattern(guid, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_delete_design_pattern(guid, body)) async def _async_find_design_patterns(self, search_string: str = "*", body: Optional[dict | SearchStringRequestBody] = None, starts_with: bool = True, ends_with: bool = False, ignore_case: bool = False, @@ -250,7 +260,8 @@ def find_design_patterns(self, search_string: str = "*", body: Optional[dict | S start_from: int = 0, page_size: int = 100, graph_query_depth: int = 3, output_format: str = "JSON", report_spec: str | dict = "Design-Pattern-DrE", **kwargs): """Find design patterns.""" - return asyncio.run(self._async_find_design_patterns(search_string, body, starts_with, ends_with, + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_find_design_patterns(search_string, body, starts_with, ends_with, ignore_case, start_from, page_size, graph_query_depth, output_format, report_spec, **kwargs)) @@ -279,7 +290,8 @@ def get_design_patterns_by_name(self, name: Optional[str] = None, body: Optional start_from: int = 0, page_size: int = max_paging_size, graph_query_depth: int = 3, output_format: str = "JSON", report_spec: str | dict = "Design-Pattern-DrE", **kwargs): """Get design patterns by name.""" - return asyncio.run(self._async_get_design_patterns_by_name(name=name, body=body, start_from=start_from, page_size=page_size, + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_get_design_patterns_by_name(name=name, body=body, start_from=start_from, page_size=page_size, graph_query_depth=graph_query_depth, output_format=output_format, report_spec=report_spec, **kwargs)) @@ -303,7 +315,8 @@ async def _async_get_design_pattern_by_guid(self, guid: str = None, body: Option def get_design_pattern_by_guid(self, guid: str = None, body: Optional[dict | GetRequestBody] = None, graph_query_depth: int = 3, output_format: str = "JSON", report_spec: str | dict = "Design-Pattern-DrE", **kwargs): """Get a design pattern by GUID.""" - return asyncio.run(self._async_get_design_pattern_by_guid(guid=guid, body=body, graph_query_depth=graph_query_depth, output_format=output_format, report_spec=report_spec, **kwargs)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_get_design_pattern_by_guid(guid=guid, body=body, graph_query_depth=graph_query_depth, output_format=output_format, report_spec=report_spec, **kwargs)) async def _async_update_solution_blueprint_status(self, guid: str, body: dict | UpdateElementRequestBody): validate_guid(guid) @@ -312,7 +325,8 @@ async def _async_update_solution_blueprint_status(self, guid: str, body: dict | def update_solution_blueprint_status(self, guid: str, body: dict | UpdateElementRequestBody): """Update the status of a solution blueprint.""" - return asyncio.run(self._async_update_solution_blueprint_status(guid, body)) + loop = asyncio.get_event_loop() + return loop.run_until_complete(self._async_update_solution_blueprint_status(guid, body)) def _get_supply_chain_rel_elements_dict(self, el_struct: dict)-> dict | str: @@ -2233,6 +2247,7 @@ def get_info_supply_chain_by_guid(self, guid: str = None, body: dict = None, add # Blueprints # + @dynamic_catch async def _async_create_solution_blueprint(self, body: dict | NewElementRequestBody) -> str: """ Create a solution blueprint. To set a lifecycle status @@ -6056,6 +6071,7 @@ def delete_solution_role(self, guid: str, body: Optional[dict | DeleteElementReq loop = asyncio.get_event_loop() loop.run_until_complete(self._async_delete_solution_role(guid, body, cascade_delete)) + async def _async_find_solution_roles( self, search_string: str = "*", diff --git a/pyegeria/omvs/subject_area.py b/pyegeria/omvs/subject_area.py index 4e25fa4d..09578e78 100644 --- a/pyegeria/omvs/subject_area.py +++ b/pyegeria/omvs/subject_area.py @@ -443,7 +443,7 @@ async def _async_link_subject_area_hierarchy( } ``` """ - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/subject-area/subject-areas/{parent_subject_area_guid}/subject-area-hierarchies/{nested_subject_area_guid}/attach" + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/subject-area/collections/{parent_subject_area_guid}/collection-hierarchies/{nested_subject_area_guid}/attach" await self._async_new_relationship_request(url, ["SubjectAreaHierarchyProperties"], body) def link_subject_area_hierarchy( @@ -527,7 +527,7 @@ async def _async_detach_subject_area_hierarchy( } ``` """ - url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/subject-area/subject-areas/{parent_subject_area_guid}/subject-area-hierarchies/{nested_subject_area_guid}/detach" + url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/subject-area/collections/{parent_subject_area_guid}/collection-hierarchies/{nested_subject_area_guid}/detach" await self._async_delete_relationship_request(url, body) def detach_subject_area_hierarchy( diff --git a/pyegeria/omvs/time_keeper.py b/pyegeria/omvs/time_keeper.py index 2a5c3559..c9a6f8f2 100644 --- a/pyegeria/omvs/time_keeper.py +++ b/pyegeria/omvs/time_keeper.py @@ -1156,7 +1156,7 @@ async def _async_link_context_event_timeline_effect( ``` """ url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/time-keeper/elements/{timeline_affected_element_guid}/context-events-describing-timeline-effects/{context_event_guid}/attach" - await self._async_new_relationship_request(url, ['ContextEventTimelineEffect'],body) + await self._async_new_relationship_request(url, ["ContextEventForTimelineEffectsProperties"], body) def link_context_event_timeline_effect( self, diff --git a/scripts/omvs_audit.py b/scripts/omvs_audit.py new file mode 100644 index 00000000..450f1bc3 --- /dev/null +++ b/scripts/omvs_audit.py @@ -0,0 +1,539 @@ +#!/usr/bin/env python3 +"""Audit pyegeria's OMVS clients against the Egeria .http collections. + +The .http files under ``pyegeria/http clients/`` are the ground truth for REST +URLs, verbs, and request-body classes (see CLAUDE.md). This script reconciles +every ``_async_*`` method in ``pyegeria/omvs/*.py`` against them and reports: + + * MISSING - a documented request with no SDK method + * VERB MISMATCH - SDK uses a different HTTP verb than the API + * PATH MISMATCH - SDK builds a different URL path than the API + * BODY MISMATCH - SDK sends a different request-body class than the API + * ELSEWHERE - method exists, but in a different OMVS module + * LINT - malformed URLs (``//``, missing separators) in the SDK + +Usage +----- + python scripts/omvs_audit.py [--report PATH] [--service NAME] [--quiet] + +Exit status is 1 when any confirmed defect (verb/path/body mismatch or lint) +is found, so this can gate CI. + +Note: ``pyegeria/http clients/`` is gitignored. It must be present locally or +the audit cannot run - the script fails loudly rather than reporting a clean +sheet against zero inputs. +""" + +from __future__ import annotations + +import argparse +import ast +import glob +import os +import re +import sys +from collections import defaultdict +from dataclasses import dataclass, field + +HTTP_DIR = "pyegeria/http clients" +OMVS_DIR = "pyegeria/omvs" +SERVER_CLIENT = "pyegeria/core/_server_client.py" + +# Services documented in .http files that intentionally have no OMVS client. +SKIP_SERVICES = {"data-officer", "devops-pipeline", "multi-language"} + +# .http @name -> SDK method name, where the SDK deliberately diverges. +NAME_OVERRIDES = { + "get_omag__server__platform__origin": "get_platform_origin", + "get_omag__server__platform__organization": "get_platform_organization", + "get__platform__security__connection": "get_security_connection", + "set__platform__security__connection": "set_security_connection", + "delete__platform__security__connection": "delete_security_connection", + "get__user__list": "get_security_user_list", + "get_all_services": "get_registered_all_services", + "get_governance_services": "get_registered_governance_services", + "get_common_services": "get_registered_common_services", + "query_if_a_specific_server_is_known": "is_server_known", + "get_all_known_servers": "get_known_server_list", + "get_all_active_servers": "get_active_server_list", + "query_the_status_of_a_specific_server": "get_active_server_status", + "start_server_-_using_stored_configuration": "activate_server_stored_config", + "start_server_-_using_supplied_configuration": "activate_server_supplied_config", + "move_glossary_term": "move_term", + "delete_glossary_term": "delete_term", + "get_glossary_terms_by_name": "get_terms_by_name", + "get_glossary_term_by_guid": "get_term_by_guid", + "get_glossary_term_relationship_statuses": "get_glossary_term_rel_statuses", + "create_glossary_term_from_template": "create_term_copy", + "setup_term_relationship": "add_relationship_between_terms", + "update_term_relationship": "update_relationship_between_terms", + "clear_term_relationship": "remove_relationship_between_terms", + "set_term_as_abstract_concept": "add_is_abstract_concept", + "clear_term_as_abstract_concept": "remove_is_abstract_concept", + "set_term_as_prime_word": "set_is_prime_word", + "clear_term_as_prime_word": "clear_is_prime_word", + "set_term_as_modifier": "set_is_modifier", + "clear_term_as_modifier": "clear_is_modifier", + "set_term_as_class_word": "set_is_class_word", + "clear_term_as_class_word": "clear_is_class_word", + "set_term_as_data_value": "add_is_data_value", + "clear_term_as_data_value": "remove_is_data_value", + "set_term_as_activity": "add_activity_description", + "clear_term_as_activity": "remove_activity_description", + "set_term_as_context": "add_is_context_definition", + "clear_term_as_context": "remove_is_context_definition", + "get_metadata_element_guid_by_unique_name": "get_metadata_guid_by_unique_name", + "get_governance_action_process_by_guid": "get_governance_action_process", + "get_governance_action_processes_by_name": "get_governance_action_process_by_name", + "link_peer_definitions": "link_peer_definition", + "detach_peer_definitions": "detach_peer_definition", + "get_actions_for_requester": "get_actions_for_requester", +} + +# Transparent wrappers around a URL expression, e.g. str(HttpUrl(f"...")). +URL_WRAPPERS = {"str", "HttpUrl", "AnyUrl", "quote", "urljoin"} + +# Internal helpers that resolve a name/GUID before the real call. They issue +# their own request, so they must not be mistaken for the method's own verb. +LOOKUP_HELPER_RE = re.compile(r"get_guid__|__async_get_guid") + +# Known-duplicate endpoints: same verb+path by design, differing only by body. +EXPECTED_DUPLICATE_PATHS = { + "POST platform-services/server-platform/servers/{}/instance", +} + + +# -------------------------------------------------------------------------- +# helpers +# -------------------------------------------------------------------------- + +def to_snake(name: str) -> str: + name = name.replace(" ", "_") + name = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower() + + +def canon_path(path: str) -> str: + """Normalise a URL path for comparison. + + Collapses every path parameter to ``{}`` and strips the host/prefix so both + sides are rooted at the OMVS service segment. Deliberately does NOT strip + trailing verbs (``/attach`` vs ``/detach``) - that distinction is exactly + the class of bug this audit exists to catch. + """ + path = path.split("?", 1)[0] + # Strip copy/paste junk seen on some .http lines. Deliberately excludes '}' + # and ']' so a path ending in a parameter ("/{{guid}}") stays intact. + path = path.rstrip('"\'`),;') + path = re.sub(r"\{\{[^}]+\}\}", "{}", path) # {{guid}} -> {} + path = re.sub(r"\{[^}]*\}", "{}", path) # {guid} -> {} + path = re.sub(r"\{\}(?=\{\})", "", path) # collapse adjacent params + m = re.search(r"/api/open-metadata/(.*)", path) + if m: + path = m.group(1) + else: + m = re.search(r"/open-metadata/(.*)", path) + if m: + path = m.group(1) + return "/" + path.strip("/") + + +def lint_url(raw: str) -> list[str]: + """Structural problems detectable without ground truth.""" + problems = [] + body = re.sub(r"^https?://", "", raw) + if "//" in body: + problems.append("double slash in path") + + # A path parameter must follow a separator: ".../agreements{guid}" is the + # concatenation bug that "/agreements" + "{guid}/..." produces. Only flag + # placeholders that hold an identifier - a trailing "{query_params}" or a + # dynamic segment like "by-{name}" is legitimate. + for m in re.finditer(r"(?<=[A-Za-z0-9])\{(\w+)\}", raw): + var = m.group(1).lower() + if re.search(r"query|param|filter|suffix|string", var): + continue + if raw[m.start() - 1] == "-": # "by-{name}" style segment + continue + if re.search(r"guid|_id$|^id$", var): + problems.append(f"missing '/' before interpolated '{m.group(1)}'") + + literal = re.sub(r"\{\w*\}", "", body.split("?", 1)[0]) + if "_" in literal: + problems.append("underscore in path segment (Egeria uses hyphens)") + return problems + + +# -------------------------------------------------------------------------- +# ground truth: .http collections +# -------------------------------------------------------------------------- + +@dataclass +class HttpRequest: + name: str + verb: str + path: str + body_class: str | None + + +def parse_http_file(filepath: str) -> dict[str, HttpRequest]: + requests: dict[str, HttpRequest] = {} + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + for block in content.split("###"): + nm = re.search(r"#\s*@name\s+([^(\n\r]+)", block) + if not nm: + continue + um = re.search(r"^(GET|POST|PUT|DELETE|PATCH)\s+(\S+)", block, re.M) + if not um: + continue + bm = re.search(r'"class"\s*:\s*"([^"]+)"', block) + name = nm.group(1).strip() + # First definition wins; later ones are usually worked examples of the + # same endpoint with concrete GUIDs substituted in. + requests.setdefault(name, HttpRequest( + name=name, + verb=um.group(1), + path=canon_path(um.group(2)), + body_class=bm.group(1) if bm else None, + )) + return requests + + +# -------------------------------------------------------------------------- +# subject: pyegeria OMVS clients +# -------------------------------------------------------------------------- + +def discover_helper_verbs(path: str) -> dict[str, str]: + """Map ``_async_*_request`` helpers to the verb they actually issue. + + Derived from the source rather than hardcoded, so the audit stays correct + when helpers change. (Historically every helper is POST - a hardcoded + table in an earlier version wrongly mapped the get_* helpers to GET, which + is how a batch of GET-vs-POST defects went undetected.) + """ + verbs: dict[str, str] = {} + if not os.path.exists(path): + return verbs + tree = ast.parse(open(path, encoding="utf-8").read()) + for fn in ast.walk(tree): + if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + found = set() + for node in ast.walk(fn): + if (isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "_async_make_request" + and node.args + and isinstance(node.args[0], ast.Constant) + and isinstance(node.args[0].value, str)): + found.add(node.args[0].value.replace("POST-DATA", "POST").upper()) + if len(found) == 1: + verbs[fn.name] = found.pop() + return verbs + + +@dataclass +class PyMethod: + name: str + verb: str | None + path: str | None + raw_url: str | None + body_class: str | None + lint: list[str] = field(default_factory=list) + + +def _flatten(node: ast.AST, roots: dict[str, str]) -> str: + """Render a URL expression to a literal path with ``{}`` for each variable. + + Handles the three shapes used across the OMVS clients: f-strings, ``+`` + concatenation, and plain constants - including nesting of all three. + Regex cannot do this reliably (multi-line f-strings silently yielded an + empty path in an earlier version, masking every mismatch in the module). + """ + if isinstance(node, ast.Constant): + return str(node.value) + if isinstance(node, ast.JoinedStr): + return "".join(_flatten(v, roots) for v in node.values) + if isinstance(node, ast.FormattedValue): + return _flatten(node.value, roots) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): + return _flatten(node.left, roots) + _flatten(node.right, roots) + if isinstance(node, ast.Name): + return "{%s}" % node.id + if isinstance(node, ast.Attribute): + if isinstance(node.value, ast.Name) and node.value.id == "self": + if node.attr == "platform_url": + return "" + if node.attr in roots: + return roots[node.attr] + return "{%s}" % node.attr + # Unwrap URL-normalising wrappers, e.g. str(HttpUrl(f"...")). + if isinstance(node, ast.Call): + fname = (node.func.id if isinstance(node.func, ast.Name) + else getattr(node.func, "attr", None)) + if fname in URL_WRAPPERS and node.args: + return _flatten(node.args[0], roots) + # e.g. name.lower() - name the receiver so lint can classify it + if isinstance(node.func, ast.Attribute): + return _flatten(node.func.value, roots) + return "{expr}" + return "{expr}" + + +def resolve_roots(tree: ast.AST) -> dict[str, str]: + """Resolve ``self._command_root``-style URL prefixes from __init__.""" + roots: dict[str, str] = {} + for node in ast.walk(tree): + if not isinstance(node, (ast.Assign, ast.AnnAssign)): + continue + targets = node.targets if isinstance(node, ast.Assign) else [node.target] + for t in targets: + if (isinstance(t, ast.Attribute) + and isinstance(t.value, ast.Name) and t.value.id == "self" + and node.value is not None + and re.search(r"(command_root|command_base|base_path|command_url)$", t.attr)): + roots[t.attr] = _flatten(node.value, roots) + return roots + + +def parse_py_file(filepath: str, helper_verbs: dict[str, str]) -> dict[str, PyMethod]: + content = open(filepath, encoding="utf-8").read() + try: + tree = ast.parse(content) + except SyntaxError as exc: # pragma: no cover + print(f" ! skipping {filepath}: {exc}", file=sys.stderr) + return {} + + roots = resolve_roots(tree) + methods: dict[str, PyMethod] = {} + sync_names: list[str] = [] + + for fn in ast.walk(tree): + if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if not fn.name.startswith("_async_"): + if not fn.name.startswith("_"): + sync_names.append(fn.name) + continue + + raw_url = path = body_class = None + direct_verb = helper_verb = None + lint: list[str] = [] + + for node in ast.walk(fn): + # url = ... + if (raw_url is None and isinstance(node, ast.Assign) + and any(isinstance(t, ast.Name) and t.id == "url" for t in node.targets)): + raw_url = _flatten(node.value, roots) + lint = lint_url(raw_url) + path = canon_path(raw_url) + + # verb: a direct _async_make_request wins over any helper, because + # some methods first call a GUID-resolution helper (which issues its + # own unrelated request) before making the real call. + if isinstance(node, ast.Call): + fname = (node.func.attr if isinstance(node.func, ast.Attribute) + else getattr(node.func, "id", None)) + if fname == "_async_make_request" and node.args: + a0 = node.args[0] + if direct_verb is None and isinstance(a0, ast.Constant) and isinstance(a0.value, str): + direct_verb = a0.value.replace("POST-DATA", "POST").upper() + elif (helper_verb is None and fname in helper_verbs + and not LOOKUP_HELPER_RE.search(fname or "")): + helper_verb = helper_verbs[fname] + + # request-body model referenced anywhere in the method + if body_class is None: + ident = (node.id if isinstance(node, ast.Name) + else node.attr if isinstance(node, ast.Attribute) else None) + if ident and ident.endswith("RequestBody"): + body_class = ident + + methods[fn.name] = PyMethod( + name=fn.name, verb=direct_verb or helper_verb, path=path, + raw_url=raw_url, body_class=body_class, lint=lint, + ) + + for sm in sync_names: + methods.setdefault(sm, PyMethod(sm, None, None, None, None)) + + return methods + + +# -------------------------------------------------------------------------- +# audit +# -------------------------------------------------------------------------- + +def audit(service_filter: str | None, report_path: str, quiet: bool, + http_dir: str = HTTP_DIR) -> int: + if not os.path.isdir(http_dir): + sys.exit( + f"ERROR: '{http_dir}' not found.\n" + "This directory is gitignored and holds the audit's ground truth.\n" + "It is absent in fresh worktrees - point --http-dir (or " + "PYEGERIA_HTTP_DIR) at a checkout that has it." + ) + + http_files = sorted(glob.glob(f"{http_dir}/Egeria-api-*.http")) + extra = f"{http_dir}/Egeria-platform-services.http" + if os.path.exists(extra): + http_files.append(extra) + if not http_files: + sys.exit(f"ERROR: no .http collections found under '{http_dir}'.") + + helper_verbs = discover_helper_verbs(SERVER_CLIENT) + + http_by_service = {} + for hf in http_files: + svc = (os.path.basename(hf) + .replace("Egeria-api-", "").replace("Egeria-", "").replace(".http", "")) + http_by_service[svc] = parse_http_file(hf) + + py_by_service = {} + for pf in sorted(glob.glob(f"{OMVS_DIR}/*.py")): + svc = os.path.basename(pf).replace(".py", "").replace("_omvs", "").replace("_", "-") + py_by_service[svc] = parse_py_file(pf, helper_verbs) + + flat = {m: (svc, d) for svc, ms in py_by_service.items() for m, d in ms.items()} + + counts = defaultdict(int) + lines: list[str] = [] + defects = 0 + + # ---- lint pass ------------------------------------------------------- + lint_rows = [] + for svc, ms in py_by_service.items(): + if service_filter and service_filter != svc: + continue + for m, d in ms.items(): + for problem in d.lint: + lint_rows.append(f"- `{svc}.py` `{m}`: {problem}\n `{d.raw_url}`") + if lint_rows: + lines.append("## URL Lint (malformed SDK URLs)\n") + lines.extend(lint_rows) + lines.append("") + defects += len(lint_rows) + counts["lint"] = len(lint_rows) + + # ---- duplicate pass -------------------------------------------------- + dupes = defaultdict(list) + for svc, ms in py_by_service.items(): + for m, d in ms.items(): + if d.path and d.verb: + dupes[f"{d.verb} {d.path}"].append((svc, m)) + dupe_rows = [] + for key, occ in sorted(dupes.items()): + if len(occ) > 1 and key not in EXPECTED_DUPLICATE_PATHS: + dupe_rows.append(f"- `{key}`") + dupe_rows += [f" - `{s}.py`: `{m}`" for s, m in occ] + if dupe_rows: + lines.append("## Duplicate endpoints (same verb + path)\n") + lines.append("_Review only - cross-service overlap is often intentional._\n") + lines.extend(dupe_rows) + lines.append("") + + # ---- coverage pass --------------------------------------------------- + for svc, requests in sorted(http_by_service.items()): + if svc in SKIP_SERVICES: + continue + if service_filter and service_filter != svc: + continue + + lines.append(f"\n### Service: {svc}\n") + py_methods = py_by_service.get(svc, {}) + + for name, req in requests.items(): + if name == "Token": + continue + snake = to_snake(name) + cands = [snake, f"_async_{snake}"] + for prefix in ("glossary_", "asset_", "location_", "project_", + "collection_", "actor_"): + if snake.startswith(prefix): + stem = snake[len(prefix):] + cands += [stem, f"_async_{stem}"] + if snake in NAME_OVERRIDES: + ov = NAME_OVERRIDES[snake] + cands += [ov, f"_async_{ov}"] + + match = next((c for c in cands if c in py_methods), None) + + if not match: + elsewhere = next((c for c in cands if c in flat), None) + if elsewhere: + counts["elsewhere"] += 1 + lines.append(f"- {name}: ELSEWHERE -> `{flat[elsewhere][0]}.py`") + else: + counts["missing"] += 1 + lines.append(f"- {name}: MISSING (`{req.verb} {req.path}`)") + continue + + check = match if match.startswith("_async_") else f"_async_{match}" + d = py_methods.get(check) or py_methods[match] + + issues = [] + if d.verb and d.verb != req.verb: + issues.append(f"VERB {d.verb} != {req.verb}") + if d.path and d.path != req.path: + issues.append(f"PATH\n SDK: {d.path}\n API: {req.path}") + if (d.body_class and req.body_class + and d.body_class != req.body_class + and "RequestBody" in req.body_class): + issues.append(f"BODY {d.body_class} != {req.body_class}") + + if issues: + counts["mismatch"] += 1 + defects += 1 + lines.append(f"- {name}: MISMATCH `{match}`") + lines += [f" - {i}" for i in issues] + else: + counts["ok"] += 1 + if not quiet: + lines.append(f"- {name}: OK") + + header = [ + "# OMVS Audit Report", + "", + f"Ground truth: `{HTTP_DIR}` ({len(http_files)} collections)", + f"Subject: `{OMVS_DIR}` ({len(py_by_service)} modules)", + "", + "| Result | Count |", + "|---|---|", + f"| OK | {counts['ok']} |", + f"| Mismatch (verb/path/body) | {counts['mismatch']} |", + f"| Missing | {counts['missing']} |", + f"| Found in another module | {counts['elsewhere']} |", + f"| URL lint | {counts['lint']} |", + "", + ] + + with open(report_path, "w", encoding="utf-8") as f: + f.write("\n".join(header + lines) + "\n") + + print(f"Audit complete -> {report_path}") + print(f" OK={counts['ok']} mismatch={counts['mismatch']} " + f"missing={counts['missing']} elsewhere={counts['elsewhere']} " + f"lint={counts['lint']}") + return 1 if defects else 0 + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--report", default="omvs_audit_report.md", help="output path") + ap.add_argument("--service", help="audit a single service, e.g. location-arena") + ap.add_argument("--quiet", action="store_true", help="omit OK rows from the report") + ap.add_argument( + "--http-dir", + default=os.environ.get("PYEGERIA_HTTP_DIR", HTTP_DIR), + help="path to the .http ground-truth collections (gitignored; " + "defaults to $PYEGERIA_HTTP_DIR or the in-repo location)", + ) + args = ap.parse_args() + return audit(args.service, args.report, args.quiet, args.http_dir) + + +if __name__ == "__main__": + sys.exit(main()) From 17314fc0ee35a8fbf2d772ba290e4ad1fec5467b Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 15:39:29 +0100 Subject: [PATCH 2/3] fix(scripts): omvs_audit -- detect service roots by value, not by attribute name resolve_roots() only recognised attributes whose name ended in command_root, command_base, base_path or command_url. connection_maker.py assigns its root to self.base_url, so nothing resolved it: every URL flattened to "/{}/..." and all 34 of its endpoints were reported as path mismatches. Detect roots by value instead -- any self. assigned a string containing "/open-metadata/" is a service root -- keeping the name heuristic as a fallback. This is self-maintaining as new clients are added. Verified by set-comparing connection-maker's 34 endpoints against Egeria-api-connection-maker.http independently of the name-matching logic: 34/34 exact, no difference in either direction. The module was already correct; the finding was entirely a tool artifact. Audit now reports 598 OK / 61 mismatch (was 557 / 102). Not changed: connection_maker, privacy_officer and security_officer build URLs from self.server_name while 31 other clients use self.view_server. Since the OMVS clients set view_server = server_name, and server_name itself defaults to the configured view server, both forms produce identical URLs -- a cosmetic inconsistency, not a defect. Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 174 ++---------------------------------------- scripts/omvs_audit.py | 21 +++-- 2 files changed, 21 insertions(+), 174 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 171acf23..7c6cfe3f 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 557 | -| Mismatch (verb/path/body) | 102 | +| OK | 598 | +| Mismatch (verb/path/body) | 61 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -88,12 +88,12 @@ _Review only - cross-service overlap is often intentional._ - `POST /runtime-manager/software-servers/by-deployed-implementation-type` - `runtime-manager.py`: `_async_get_servers_by_dep_impl_type` - `runtime-manager.py`: `_async_get_server_templates_by_dep_impl_type` +- `POST /security-officer/collections/by-search-string` + - `security-officer.py`: `_async_find_security_roles` + - `security-officer.py`: `_async_find_security_groups` - `POST /solution-architect/solution-blueprints/{}/update` - `solution-architect.py`: `_async_update_solution_blueprint_status` - `solution-architect.py`: `_async_update_solution_blueprint` -- `POST /{}/collections/by-search-string` - - `security-officer.py`: `_async_find_security_roles` - - `security-officer.py`: `_async_find_security_groups` - `POST /{}/governance-definitions` - `governance-officer.py`: `_async_create_governance_definition` - `governance-officer.py`: `_async_create_data_lens` @@ -259,142 +259,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: connection-maker -- createConnection: MISMATCH `create_connection` - - PATH - SDK: /{}/connections - API: /connection-maker/connections -- createConnectionFromTemplate: MISMATCH `create_connection_from_template` - - PATH - SDK: /{}/connections/from-template - API: /connection-maker/connections/from-template -- updateConnection: MISMATCH `update_connection` - - PATH - SDK: /{}/connections/{}/update - API: /connection-maker/connections/{}/update -- linkConnectionConnectorType: MISMATCH `link_connection_connector_type` - - PATH - SDK: /{}/connections/{}/connector-types/{}/attach - API: /connection-maker/connections/{}/connector-types/{}/attach -- detachConnectionConnectorType: MISMATCH `detach_connection_connector_type` - - PATH - SDK: /{}/connections/{}/connector-types/{}/detach - API: /connection-maker/connections/{}/connector-types/{}/detach -- linkConnectionEndpoint: MISMATCH `link_connection_endpoint` - - PATH - SDK: /{}/connections/{}/endpoints/{}/attach - API: /connection-maker/connections/{}/endpoints/{}/attach -- detachConnectionEndpoint: MISMATCH `detach_connection_endpoint` - - PATH - SDK: /{}/connections/{}/endpoints/{}/detach - API: /connection-maker/connections/{}/endpoints/{}/detach -- linkEmbeddedConnection: MISMATCH `link_embedded_connection` - - PATH - SDK: /{}/connections/{}/embedded-connections/{}/attach - API: /connection-maker/connections/{}/embedded-connections/{}/attach -- detachEmbeddedConnection: MISMATCH `detach_embedded_connection` - - PATH - SDK: /{}/connections/{}/embedded-connections/{}/detach - API: /connection-maker/connections/{}/embedded-connections/{}/detach -- linkAssetToConnection: MISMATCH `link_asset_to_connection` - - PATH - SDK: /{}/assets/{}/connections/{}/attach - API: /connection-maker/assets/{}/connections/{}/attach -- detachAssetFromConnection: MISMATCH `detach_asset_from_connection` - - PATH - SDK: /{}/assets/{}/connections/{}/detach - API: /connection-maker/assets/{}/connections/{}/detach -- linkEndpointToITAsset: MISMATCH `link_endpoint_to_it_asset` - - PATH - SDK: /{}/assets/{}/endpoints/{}/attach - API: /connection-maker/assets/{}/endpoints/{}/attach -- detachEndpointFromITAsset: MISMATCH `detach_endpoint_from_it_asset` - - PATH - SDK: /{}/assets/{}/endpoints/{}/detach - API: /connection-maker/assets/{}/endpoints/{}/detach -- deleteConnection: MISMATCH `delete_connection` - - PATH - SDK: /{}/connections/{}/delete - API: /connection-maker/connections/{}/delete -- getConnectionsByName: MISMATCH `get_connections_by_name` - - PATH - SDK: /{}/connections/by-name - API: /connection-maker/connections/by-name -- findConnections: MISMATCH `find_connections` - - PATH - SDK: /{}/connections/by-search-string - API: /connection-maker/connections/by-search-string -- getConnectionByGUID: MISMATCH `get_connection_by_guid` - - PATH - SDK: /{}/connections/{}/retrieve - API: /connection-maker/connections/{}/retrieve -- createConnectorType: MISMATCH `create_connector_type` - - PATH - SDK: /{}/connector-types - API: /connection-maker/connector-types -- createConnectorTypeFromTemplate: MISMATCH `create_connector_type_from_template` - - PATH - SDK: /{}/connector-types/from-template - API: /connection-maker/connector-types/from-template -- updateConnectorType: MISMATCH `update_connector_type` - - PATH - SDK: /{}/connector-types/{}/update - API: /connection-maker/connector-types/{}/update -- deleteConnectorType: MISMATCH `delete_connector_type` - - PATH - SDK: /{}/connector-types/{}/delete - API: /connection-maker/connector-types/{}/delete -- getConnectorTypesByName: MISMATCH `get_connector_types_by_name` - - PATH - SDK: /{}/connector-types/by-name - API: /connection-maker/connector-types/by-name -- getConnectorTypesByConnectorProviderClassName: MISMATCH `get_connector_types_by_connector_provider_class_name` - - PATH - SDK: /{}/connector-types/by-connector-provider-class-name - API: /connection-maker/connector-types/by-connector-provider-class-name -- findConnectorTypes: MISMATCH `find_connector_types` - - PATH - SDK: /{}/connector-types/by-search-string - API: /connection-maker/connector-types/by-search-string -- getConnectorTypeByGUID: MISMATCH `get_connector_type_by_guid` - - PATH - SDK: /{}/connector-types/{}/retrieve - API: /connection-maker/connector-types/{}/retrieve -- createEndpoint: MISMATCH `create_endpoint` - - PATH - SDK: /{}/endpoints - API: /connection-maker/endpoints -- createEndpointFromTemplate: MISMATCH `create_endpoint_from_template` - - PATH - SDK: /{}/endpoints/from-template - API: /connection-maker/endpoints/from-template -- updateEndpoint: MISMATCH `update_endpoint` - - PATH - SDK: /{}/endpoints/{}/update - API: /connection-maker/endpoints/{}/update -- deleteEndpoint: MISMATCH `delete_endpoint` - - PATH - SDK: /{}/endpoints/{}/delete - API: /connection-maker/endpoints/{}/delete -- getEndpointsByName: MISMATCH `get_endpoints_by_name` - - PATH - SDK: /{}/endpoints/by-name - API: /connection-maker/endpoints/by-name -- getEndpointsByNetworkAddress: MISMATCH `get_endpoints_by_network_address` - - PATH - SDK: /{}/endpoints/by-network-address - API: /connection-maker/endpoints/by-network-address -- getEndpointsForAsset: MISMATCH `get_endpoints_for_asset` - - PATH - SDK: /{}/assets/{}/endpoints/retrieve - API: /connection-maker/assets/{}/endpoints/retrieve -- findEndpoints: MISMATCH `find_endpoints` - - PATH - SDK: /{}/endpoints/by-search-string - API: /connection-maker/endpoints/by-search-string -- getEndpointByGUID: MISMATCH `get_endpoint_by_guid` - - PATH - SDK: /{}/endpoints/{}/retrieve - API: /connection-maker/endpoints/{}/retrieve ### Service: data-designer @@ -717,22 +581,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: privacy-officer -- linkPermittedProcessing: MISMATCH `link_permitted_processing` - - PATH - SDK: /{}/data-processing-purposes/{}/permitted-processing/{}/attach - API: /privacy-officer/data-processing-purposes/{}/permitted-processing/{}/attach -- detachPermittedProcessing: MISMATCH `detach_permitted_processing` - - PATH - SDK: /{}/data-processing-purposes/{}/permitted-processing/{}/detach - API: /privacy-officer/data-processing-purposes/{}/permitted-processing/{}/detach -- linkDataProcessingTarget: MISMATCH `link_data_processing_target` - - PATH - SDK: /{}/data-processing-actions/{}/targets/{}/attach - API: /privacy-officer/data-processing-actions/{}/targets/{}/attach -- detachDataProcessingTarget: MISMATCH `detach_data_processing_target` - - PATH - SDK: /{}/data-processing-actions/{}/targets/{}/detach - API: /privacy-officer/data-processing-actions/{}/targets/{}/detach ### Service: product-catalog @@ -817,18 +665,6 @@ _Review only - cross-service overlap is often intentional._ ### Service: security-officer -- setSecurityAccessControl: MISMATCH `set_security_access_control` - - PATH - SDK: /{}/platforms/{}/security-access-control - API: /security-officer/platforms/{}/security-access-control -- getSecurityAccessControl: MISMATCH `get_security_access_control` - - PATH - SDK: /{}/platforms/{}/security-access-control/{} - API: /security-officer/platforms/{}/security-access-control/{} -- deleteSecurityAccessControl: MISMATCH `delete_security_access_control` - - PATH - SDK: /{}/platforms/{}/security-access-control/{} - API: /security-officer/platforms/{}/security-access-control/{} - find Security Roles: MISSING (`POST /security-officer/collections/by-search-string`) - find Security Groups: MISSING (`POST /security-officer/collections/by-search-string`) diff --git a/scripts/omvs_audit.py b/scripts/omvs_audit.py index 450f1bc3..2714edfe 100644 --- a/scripts/omvs_audit.py +++ b/scripts/omvs_audit.py @@ -283,18 +283,29 @@ def _flatten(node: ast.AST, roots: dict[str, str]) -> str: def resolve_roots(tree: ast.AST) -> dict[str, str]: - """Resolve ``self._command_root``-style URL prefixes from __init__.""" + """Resolve the service-root URL prefixes assigned in __init__. + + Detected by *value*, not by name: any ``self.`` assigned a string + containing ``/open-metadata/`` is a service root. Name-matching missed + ``self.base_url`` in connection_maker.py, which made all 34 of its + endpoints report a bogus path mismatch. + """ roots: dict[str, str] = {} for node in ast.walk(tree): if not isinstance(node, (ast.Assign, ast.AnnAssign)): continue targets = node.targets if isinstance(node, ast.Assign) else [node.target] for t in targets: - if (isinstance(t, ast.Attribute) + if not (isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name) and t.value.id == "self" - and node.value is not None - and re.search(r"(command_root|command_base|base_path|command_url)$", t.attr)): - roots[t.attr] = _flatten(node.value, roots) + and node.value is not None): + continue + if t.attr == "platform_url": + continue + value = _flatten(node.value, roots) + if "/open-metadata/" in value or re.search( + r"(command_root|command_base|base_path|command_url)$", t.attr): + roots[t.attr] = value return roots From 0bdb03b4a04d1983f2d5ceeffbfbbfb7f58ab1a8 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 18 Aug 2026 15:49:57 +0100 Subject: [PATCH 3/3] fix(pyegeria): governance-officer audit -- resolve url_marker, compare body as a set governance-officer reported 18 mismatches; 17 were tool artifacts. Tooling (scripts/omvs_audit.py): - Resolve bare service markers. governance_officer.py interpolates self.url_marker = "governance-officer" straight into its URLs. Only full-URL roots were resolved, so every path flattened to "/{}/..." and 16 endpoints reported a bogus mismatch. Now resolves constant string attributes whose name looks like a URL marker and whose value looks like a path segment, so identity/credential attributes are never substituted into a path. - Derive the request-body class from the helper actually called, not from the first *RequestBody identifier in the method. The old heuristic reported "FilterRequestBody" for a method that in fact sends GetRequestBody. - Compare bodies as a set, not a single name. Several helpers accept a union (e.g. dict | UpdateElementRequestBody | UpdateClassificationRequestBody); picking one name out of a union invented two more false mismatches. Endpoint fix: - governance_officer.detach_design_from_implementation annotated its body as DeleteElementRequestBody while calling _async_delete_relationship_request, whose validator accepts only DeleteRelationshipRequestBody or dict. A caller following the annotation and passing a DeleteElementRequestBody object hits the validator's else branch, which returns None -- so the guarded call is skipped and the detach silently never happens. Corrected to DeleteRelationshipRequestBody (matches the .http ground truth). Audit now reports 603 OK / 56 mismatch (was 598 / 45 -- the count rose because accurate body detection surfaced real findings the old heuristic masked). governance-officer has one finding left, deliberately not changed: getGovernanceActionProcessGraph sends GetRequestBody where the .http documents ResultsRequestBody. GetRequestBody is a strict subset -- it lacks the paging, sequencing and anchor fields -- so switching helpers is a behaviour change that needs verification against a live server first. Signed-off-by: Dan Wolfson --- omvs_audit_report.md | 131 +++++++++------------------- pyegeria/omvs/governance_officer.py | 4 +- scripts/omvs_audit.py | 85 ++++++++++++++---- 3 files changed, 113 insertions(+), 107 deletions(-) diff --git a/omvs_audit_report.md b/omvs_audit_report.md index 7c6cfe3f..5505a8ff 100644 --- a/omvs_audit_report.md +++ b/omvs_audit_report.md @@ -5,8 +5,8 @@ Subject: `pyegeria/omvs` (43 modules) | Result | Count | |---|---| -| OK | 598 | -| Mismatch (verb/path/body) | 61 | +| OK | 603 | +| Mismatch (verb/path/body) | 56 | | Missing | 288 | | Found in another module | 13 | | URL lint | 0 | @@ -52,8 +52,15 @@ _Review only - cross-service overlap is often intentional._ - `POST /glossary-manager/glossaries/terms` - `glossary-manager.py`: `_async_create_glossary_term` - `glossary-manager.py`: `_async_create_question` +- `POST /governance-officer/governance-action-processes/{}/graph` + - `action-author.py`: `_async_get_governance_action_process_graph` + - `governance-officer.py`: `_async_get_governance_action_process_graph` +- `POST /governance-officer/governance-definitions` + - `governance-officer.py`: `_async_create_governance_definition` + - `governance-officer.py`: `_async_create_data_lens` - `POST /governance-officer/governance-definitions/{}/retrieve` - `action-author.py`: `_async_get_governance_action_process` + - `governance-officer.py`: `_async_get_governance_definition_by_guid` - `governance-officer.py`: `_async_get_governance_action_process` - `POST /metadata-expert/related-elements` - `collection-manager.py`: `_async_link_saved_query_to_results_set` @@ -94,9 +101,6 @@ _Review only - cross-service overlap is often intentional._ - `POST /solution-architect/solution-blueprints/{}/update` - `solution-architect.py`: `_async_update_solution_blueprint_status` - `solution-architect.py`: `_async_update_solution_blueprint` -- `POST /{}/governance-definitions` - - `governance-officer.py`: `_async_create_governance_definition` - - `governance-officer.py`: `_async_create_data_lens` ### Service: action-author @@ -104,11 +108,15 @@ _Review only - cross-service overlap is often intentional._ ### Service: actor-manager +- detachAssetFromProfile: MISMATCH `detach_asset_from_profile` + - BODY sends DeleteElementRequestBody != DeleteRelationshipRequestBody - find All ContributionRecords: MISSING (`POST /actor-manager/contribution-records/by-search-string`) - updateActorRole: MISMATCH `update_actor_role` - PATH SDK: /actor-manager/actor-roles/{}/update API: /actor-manager/actor-roles/update +- detachPersonRoleFromProfile: MISMATCH `detach_person_role_from_profile` + - BODY sends DeleteElementRequestBody != DeleteRelationshipRequestBody - Detach a team role from a team profile.: MISSING (`POST /actor-manager/actor-roles/{}/team-role-appointments/{}/detach`) - linkITProfileRoleToProfile: MISSING (`POST /actor-manager/actor-roles/{}/it-profile-role-appointments/{}/attach`) - detachITProfileRoleFromProfile: MISSING (`POST /actor-manager/actor-roles/{}/it-profile-role-appointments/{}/detach`) @@ -116,7 +124,7 @@ _Review only - cross-service overlap is often intentional._ - PATH SDK: /actor-manager/actor-roles/{}/delete API: /actor-manager/actor-roles/delete - - BODY DeleteElementRequestBody != DeleteRelationshipRequestBody + - BODY sends DeleteElementRequestBody != DeleteRelationshipRequestBody - getActorRoleByGUID: MISMATCH `get_actor_role_by_guid` - PATH SDK: /actor-manager/actor-roles/{}/retrieve @@ -131,6 +139,12 @@ _Review only - cross-service overlap is often intentional._ SDK: /actor-manager/user-identities/{}/security-group-membership/reclassify API: /actor-manager/user-identities/{}/security-group-memberships/reclassify - removeAllSecurityGroupMembership: MISSING (`POST /actor-manager/user-identities/{}/security-group-memberships/declassify`) +- createContactDetailsFromTemplate: MISMATCH `create_contact_details_from_template` + - BODY sends NewElementRequestBody != TemplateRequestBody +- createPerspectiveFromTemplate: MISMATCH `create_perspective_from_template` + - BODY sends NewElementRequestBody != TemplateRequestBody +- createSkillFromTemplate: MISMATCH `create_skill_from_template` + - BODY sends NewElementRequestBody != TemplateRequestBody ### Service: asset-catalog @@ -180,7 +194,11 @@ _Review only - cross-service overlap is often intentional._ - addOwnership: MISSING (`POST /classification-explorer/elements/{}/ownership`) - clearOwnership: MISSING (`POST /classification-explorer/elements/{}/ownership/remove`) - clearDigitalResourceOrigin: MISSING (`POST /classification-explorer/elements/{}/digital-resource-origin/remove`) +- clearKnownDuplicateClassification: MISMATCH `clear_known_duplicate_classification` + - BODY sends DeleteRelationshipRequestBody != DeleteClassificationRequestBody - setupPeerDuplicates: MISSING (`POST /classification-explorer/related-elements/{}/peer-duplicate/{}/attach`) +- clearConsolidatedDuplicateClassification: MISMATCH `clear_consolidated_duplicate_classification` + - BODY sends DeleteRelationshipRequestBody != DeleteClassificationRequestBody - clearSemanticAssignment: MISSING (`POST /classification-explorer/elements/{}/semantic-assignment/terms/{}/detach`) - getSemanticAssignees: MISMATCH `get_semantic_assignees` - PATH @@ -288,6 +306,8 @@ _Review only - cross-service overlap is often intentional._ ### Service: data-discovery +- createAnnotationFromTemplate: MISMATCH `create_annotation_from_template` + - BODY sends NewElementRequestBody != TemplateRequestBody ### Service: data-engineer @@ -300,8 +320,6 @@ _Review only - cross-service overlap is often intentional._ - linkCitedDocumentReference: MISSING (`POST /external-links/elements/{}/cited-document-references/{}/attach`) - detachCitedDocumentReference: MISSING (`POST /external-links/elements/{}/cited-document-references/{}/detach`) -- deleteExternalReference: MISMATCH `delete_external_reference` - - BODY DeleteRelationshipRequestBody != DeleteElementRequestBody ### Service: feedback-manager @@ -348,65 +366,29 @@ _Review only - cross-service overlap is often intentional._ - getTermRelationshipTypeNames: MISSING (`GET /glossary-manager/glossaries/terms/relationships/type-names`) - clearTermAsAbstractConcept: MISMATCH `remove_is_abstract_concept` - - BODY DeleteClassificationRequestBody != DeleteElementRequestBody + - BODY sends DeleteClassificationRequestBody != DeleteElementRequestBody - clearTermAsActivity: MISMATCH `remove_activity_description` - - BODY DeleteClassificationRequestBody != DeleteRelationshipRequestBody + - BODY sends DeleteClassificationRequestBody != DeleteRelationshipRequestBody - clearTermAsContext: MISMATCH `remove_is_context_definition` - - BODY DeleteClassificationRequestBody != DeleteRelationshipRequestBody + - BODY sends DeleteClassificationRequestBody != DeleteRelationshipRequestBody ### Service: governance-officer - getValidMetadataValues - domainIdentifier values: MISSING (`GET /valid-metadata/get-valid-metadata-values/domainIdentifier`) -- createGovernanceDefinition: MISMATCH `create_governance_definition` - - PATH - SDK: /{}/governance-definitions - API: /governance-officer/governance-definitions - createRegulation: MISSING (`POST /governance-officer/governance-definitions`) -- addRegulatorToRegulation: MISMATCH `add_regulator_to_regulation` - - PATH - SDK: /{}/regulations/{}/regulators/organizations/{}/attach - API: /governance-officer/regulations/{}/regulators/organizations/{}/attach - removeRegulatorFromRegulation: MISSING (`POST /governance-officer/regulations/{}/regulators/organizations/{}/detach`) - createGovernanceControl: MISSING (`POST /governance-officer/governance-definitions`) -- createDataLens: MISMATCH `create_data_lens` - - PATH - SDK: /{}/governance-definitions - API: /governance-officer/governance-definitions - createSecurityAccessControl: MISSING (`POST /governance-officer/governance-definitions`) - createNamingStandardRule: MISSING (`POST /governance-officer/governance-definitions`) - createCertificationType: MISSING (`POST /governance-officer/governance-definitions`) - createLicenseType: MISSING (`POST /governance-officer/governance-definitions`) -- createGovernanceDefinitionFromTemplate: MISMATCH `create_governance_definition_from_template` - - PATH - SDK: /{}/governance-definitions/from-template - API: /governance-officer/governance-definitions/from-template -- updateGovernanceDefinition: MISMATCH `update_governance_definition` - - PATH - SDK: /{}/governance-definitions/{}/update - API: /governance-officer/governance-definitions/{}/update - updateGovernanceDefinitionStatus: MISSING (`POST /governance-officer/governance-definitions/{}/update`) -- detachPeerDefinitions: MISMATCH `detach_peer_definitions` - - PATH - SDK: /{}/governance-definitions/{}/peer-definitions/{}/{}/detach - API: /governance-officer/governance-definitions/{}/peer-definitions/{}/{}/detach - attachSupportingDefinition: MISSING (`POST /governance-officer/governance-definitions/{}/supporting-definitions/{}/{}/attach`) - detachSupportingDefinition: MISSING (`POST /governance-officer/governance-definitions/{}/supporting-definitions/{}/{}/detach`) - findAllGovernanceDefinitions: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) - findAllGovernanceDefinitions - with full request body: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) -- findGovernanceDefinitions: MISMATCH `find_governance_definitions` - - PATH - SDK: /{}/governance-definitions/by-search-string - API: /governance-officer/governance-definitions/by-search-string - findGovernanceDefinitions - with full request body: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) -- getGovernanceDefinitionsByName: MISMATCH `get_governance_definitions_by_name` - - PATH - SDK: /{}/governance-definitions/by-name - API: /governance-officer/governance-definitions/by-name - getGovernanceDefinitionsByName - with full request body: MISSING (`POST /governance-officer/governance-definitions/by-name`) -- getGovernanceDefinitionByGUID: MISMATCH `get_governance_definition_by_guid` - - PATH - SDK: /{}/governance-definitions/{}/retrieve - API: /governance-officer/governance-definitions/{}/retrieve - getGovernanceDefinitionByGUID - with request body: MISSING (`POST /governance-officer/governance-definitions/{}/retrieve`) - getAllGovernanceActionTypes: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) - findGovernanceActionTypes: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) @@ -416,49 +398,13 @@ _Review only - cross-service overlap is often intentional._ - getAllGovernanceActionProcesses: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) - getGovernanceActionProcessesByName: MISSING (`POST /governance-officer/governance-definitions/by-name`) - getGovernanceActionProcessGraph: MISMATCH `get_governance_action_process_graph` - - PATH - SDK: /{}/governance-action-processes/{}/graph - API: /governance-officer/governance-action-processes/{}/graph - - BODY FilterRequestBody != ResultsRequestBody + - BODY sends GetRequestBody != ResultsRequestBody - addGovernanceDefinitionToElement: MISSING (`POST /governance-officer/elements/{}/governed-by/definition/{}/attach`) - removeGovernanceDefinitionFromElement: MISSING (`POST /governance-officer/elements/{}/governed-by/definition/{}/detach`) -- linkDesignToImplementation: MISMATCH `link_design_to_implementation` - - PATH - SDK: /{}/designs/{}/implementations/{}/attach - API: /governance-officer/designs/{}/implementations/{}/attach -- detachDesignFromImplementation: MISMATCH `detach_design_from_implementation` - - PATH - SDK: /{}/designs/{}/implementations/{}/detach - API: /governance-officer/designs/{}/implementations/{}/detach - - BODY DeleteElementRequestBody != DeleteRelationshipRequestBody -- linkImplementationResource: MISMATCH `link_implementation_resource` - - PATH - SDK: /{}/designs/{}/implementation-resources/{}/attach - API: /governance-officer/designs/{}/implementation-resources/{}/attach -- detachImplementationResource: MISMATCH `detach_implementation_resource` - - PATH - SDK: /{}/designs/{}/implementation-resources/{}/detach - API: /governance-officer/designs/{}/implementation-resources/{}/detach - linkApprovedPurpose: MISSING (`POST /governance-officer/elements/{}/approved-purposes/{}/attach`) - detachApprovedPurpose: MISSING (`POST /governance-officer/elements/{}/approved-purposes/{}/detach`) -- linkGovernanceResults: MISMATCH `link_governance_results` - - PATH - SDK: /{}/governance-metrics/{}/measurements/{}/attach - API: /governance-officer/governance-metrics/{}/measurements/{}/attach -- detachGovernanceResults: MISMATCH `detach_governance_results` - - PATH - SDK: /{}/governance-metrics/{}/measurements/{}/detach - API: /governance-officer/governance-metrics/{}/measurements/{}/detach -- licenseElement: MISMATCH `license_element` - - PATH - SDK: /{}/elements/{}/license-types/{}/license - API: /governance-officer/elements/{}/license-types/{}/license - updateLicense: ELSEWHERE -> `classification-explorer.py` - unlicenseElement: ELSEWHERE -> `classification-explorer.py` -- certifyElement: MISMATCH `certify_element` - - PATH - SDK: /{}/elements/{}/certification-types/{}/certify - API: /governance-officer/elements/{}/certification-types/{}/certify - updateCertification: ELSEWHERE -> `classification-explorer.py` - decertifyElement: ELSEWHERE -> `classification-explorer.py` @@ -500,7 +446,9 @@ _Review only - cross-service overlap is often intentional._ - PATH SDK: /{}/metadata-elements/guid-by-unique-name API: /metadata-expert/metadata-elements/guid-by-unique-name - - BODY FilterRequestBody != UniqueNameRequestBody + - BODY sends FilterRequestBody != UniqueNameRequestBody +- getMetadataElementHistory: MISMATCH `get_metadata_element_history` + - BODY sends GetRequestBody != HistoryRequestBody - getClassificationHistory: MISMATCH `get_classification_history` - PATH SDK: /{}/metadata-elements/{}/classifications/{}/history @@ -608,6 +556,8 @@ _Review only - cross-service overlap is often intentional._ - createClassifiedProject: MISSING (`POST /project-manager/projects`) - createCampaign: MISSING (`POST /project-manager/projects`) - createTaskForProject: MISSING (`POST /project-manager/projects/{}/task`) +- clearProjectClassification: MISMATCH `clear_project_classification` + - BODY sends NewClassificationRequestBody != DeleteClassificationRequestBody - setupProjectDependency: MISSING (`POST /project-manager/projects/{}/project-dependencies/{}/attach`) - setupProjectHierarchy: MISSING (`POST /project-manager/projects/{}/project-hierarchies/{}/attach`) @@ -648,7 +598,7 @@ _Review only - cross-service overlap is often intentional._ ### Service: schema-maker - deleteSchemaType: MISMATCH `delete_schema_type` - - BODY DeleteElementRequestBody != MetadataSourceRequestBody + - BODY sends DeleteElementRequestBody != MetadataSourceRequestBody - updateSchemaAttribute: MISMATCH `update_schema_attribute` - PATH SDK: /schema-maker/schema-attributes/{}/update @@ -657,7 +607,7 @@ _Review only - cross-service overlap is often intentional._ - PATH SDK: /schema-maker/schema-attributes/{}/delete API: /schema-maker/schema-attributes/delete - - BODY DeleteElementRequestBody != MetadataSourceRequestBody + - BODY sends DeleteElementRequestBody != MetadataSourceRequestBody - getSchemaAttributeByGUID: MISMATCH `get_schema_attribute_by_guid` - PATH SDK: /schema-maker/schema-attributes/{}/retrieve @@ -674,7 +624,6 @@ _Review only - cross-service overlap is often intentional._ - PATH SDK: /solution-architect/design-patterns/by-name/{} API: /solution-architect/design-patterns/by-name - - BODY SearchStringRequestBody != FilterRequestBody - createInformationSupplyChain: MISSING (`POST /solution-architect/information-supply-chains`) - createInformationSupplyChainFromTemplate: MISSING (`POST /solution-architect/information-supply-chains/from-template`) - updateInformationSupplyChain: MISSING (`POST /solution-architect/information-supply-chains/{}/update`) @@ -722,6 +671,8 @@ _Review only - cross-service overlap is often intentional._ ### Service: subject-area +- createSubjectAreaFromTemplate: MISMATCH `create_subject_area_from_template` + - BODY sends NewElementRequestBody != TemplateRequestBody - linkSubjectAreas: MISSING (`POST /subject-area/collections/{}/collection-hierarchies/{}/attach`) - detachSubjectAreas: MISSING (`POST /subject-area/collections/{}/collection-hierarchies/{}/detach`) - findAllSubjectAreas: MISSING (`POST /subject-area/collectionss/by-search-string`) @@ -735,6 +686,8 @@ _Review only - cross-service overlap is often intentional._ ### Service: time-keeper +- createContextEventFromTemplate: MISMATCH `create_context_event_from_template` + - BODY sends NewElementRequestBody != TemplateRequestBody ### Service: valid-metadata @@ -748,3 +701,5 @@ _Review only - cross-service overlap is often intentional._ - getAttributeTypes: MISSING (`GET /valid-metadata/open-metadata-types/attribute-defs`) - getTypeDefByName: MISSING (`GET /valid-metadata/open-metadata-types/name/{}`) - setUpSpecificationProperty: MISSING (`POST /valid-metadata/elements/{}/specification-properties`) +- linkSpecificationProperty: MISMATCH `link_specification_property` + - BODY sends NewElementRequestBody != NewRelationshipRequestBody diff --git a/pyegeria/omvs/governance_officer.py b/pyegeria/omvs/governance_officer.py index 5c5f84a1..fd785d8e 100644 --- a/pyegeria/omvs/governance_officer.py +++ b/pyegeria/omvs/governance_officer.py @@ -2992,7 +2992,7 @@ def link_design_to_implementation(self, design_desc_guid: str, implementation_gu @dynamic_catch async def _async_detach_design_from_implementation(self, design_desc_guid: str, implementation_guid: str, - body: Optional[dict | DeleteElementRequestBody] = None) -> None: + body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None: """ Detach a governance definition from its implementation. Async Version. Parameters @@ -3039,7 +3039,7 @@ async def _async_detach_design_from_implementation(self, design_desc_guid: str, @dynamic_catch def detach_design_from_implementation(self, design_desc_guid: str, implementation_guid: str, - body: Optional[dict | DeleteElementRequestBody] = None) -> None: + body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None: """ Detach a governance definition from its implementation. Request body is optional. Parameters diff --git a/scripts/omvs_audit.py b/scripts/omvs_audit.py index 2714edfe..3115a37c 100644 --- a/scripts/omvs_audit.py +++ b/scripts/omvs_audit.py @@ -90,6 +90,12 @@ "get_actions_for_requester": "get_actions_for_requester", } +# Attributes holding a bare service marker that gets interpolated into a URL +# (e.g. self.url_marker = "governance-officer"), and the shape their value +# must have to be treated as a literal path segment. +URL_SEGMENT_ATTR_RE = re.compile(r"(url_marker|marker|url_fragment|service_name)$") +URL_SEGMENT_RE = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") + # Transparent wrappers around a URL expression, e.g. str(HttpUrl(f"...")). URL_WRAPPERS = {"str", "HttpUrl", "AnyUrl", "quote", "urljoin"} @@ -234,13 +240,39 @@ def discover_helper_verbs(path: str) -> dict[str, str]: return verbs +def discover_helper_bodies(path: str) -> dict[str, frozenset[str]]: + """Map each ``_async_*_request`` helper to the body models it accepts. + + A method that delegates to a helper sends *that helper's* model, whatever + its own ``body`` annotation claims - so this is what must be compared + against the ``"class"`` in the .http file. + + The value is a *set*, because several helpers accept a union (e.g. + ``dict | UpdateElementRequestBody | UpdateClassificationRequestBody``). + Picking a single name out of a union produced false mismatches. + """ + bodies: dict[str, frozenset[str]] = {} + if not os.path.exists(path): + return bodies + tree = ast.parse(open(path, encoding="utf-8").read()) + for fn in ast.walk(tree): + if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + for a in list(fn.args.args) + list(fn.args.kwonlyargs): + if a.arg == "body" and a.annotation is not None: + names = re.findall(r"\b(\w+RequestBody)\b", ast.unparse(a.annotation)) + if names: + bodies[fn.name] = frozenset(names) + return bodies + + @dataclass class PyMethod: name: str verb: str | None path: str | None raw_url: str | None - body_class: str | None + body_class: frozenset[str] | None lint: list[str] = field(default_factory=list) @@ -306,10 +338,20 @@ def resolve_roots(tree: ast.AST) -> dict[str, str]: if "/open-metadata/" in value or re.search( r"(command_root|command_base|base_path|command_url)$", t.attr): roots[t.attr] = value + # A bare service marker interpolated straight into a URL, e.g. + # self.url_marker = "governance-officer". Restricted to constants + # that look like a URL path segment so identity/credential + # attributes are never substituted into a path. + elif (URL_SEGMENT_ATTR_RE.search(t.attr) + and isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + and URL_SEGMENT_RE.fullmatch(node.value.value)): + roots[t.attr] = node.value.value return roots -def parse_py_file(filepath: str, helper_verbs: dict[str, str]) -> dict[str, PyMethod]: +def parse_py_file(filepath: str, helper_verbs: dict[str, str], + helper_bodies: dict[str, str] | None = None) -> dict[str, PyMethod]: content = open(filepath, encoding="utf-8").read() try: tree = ast.parse(content) @@ -330,8 +372,17 @@ def parse_py_file(filepath: str, helper_verbs: dict[str, str]) -> dict[str, PyMe continue raw_url = path = body_class = None - direct_verb = helper_verb = None + direct_verb = helper_verb = helper_body = None lint: list[str] = [] + helper_bodies = helper_bodies or {} + + # What the method's own signature claims - the fallback when it does + # not delegate to a helper. + for a in list(fn.args.args) + list(fn.args.kwonlyargs): + if a.arg == "body" and a.annotation is not None: + names = re.findall(r"\b(\w+RequestBody)\b", ast.unparse(a.annotation)) + if names: + body_class = frozenset(names) for node in ast.walk(fn): # url = ... @@ -351,20 +402,16 @@ def parse_py_file(filepath: str, helper_verbs: dict[str, str]) -> dict[str, PyMe a0 = node.args[0] if direct_verb is None and isinstance(a0, ast.Constant) and isinstance(a0.value, str): direct_verb = a0.value.replace("POST-DATA", "POST").upper() - elif (helper_verb is None and fname in helper_verbs + elif (fname in helper_verbs and not LOOKUP_HELPER_RE.search(fname or "")): - helper_verb = helper_verbs[fname] - - # request-body model referenced anywhere in the method - if body_class is None: - ident = (node.id if isinstance(node, ast.Name) - else node.attr if isinstance(node, ast.Attribute) else None) - if ident and ident.endswith("RequestBody"): - body_class = ident + if helper_verb is None: + helper_verb = helper_verbs[fname] + if helper_body is None and fname in helper_bodies: + helper_body = helper_bodies[fname] methods[fn.name] = PyMethod( name=fn.name, verb=direct_verb or helper_verb, path=path, - raw_url=raw_url, body_class=body_class, lint=lint, + raw_url=raw_url, body_class=helper_body or body_class, lint=lint, ) for sm in sync_names: @@ -395,6 +442,7 @@ def audit(service_filter: str | None, report_path: str, quiet: bool, sys.exit(f"ERROR: no .http collections found under '{http_dir}'.") helper_verbs = discover_helper_verbs(SERVER_CLIENT) + helper_bodies = discover_helper_bodies(SERVER_CLIENT) http_by_service = {} for hf in http_files: @@ -405,7 +453,7 @@ def audit(service_filter: str | None, report_path: str, quiet: bool, py_by_service = {} for pf in sorted(glob.glob(f"{OMVS_DIR}/*.py")): svc = os.path.basename(pf).replace(".py", "").replace("_omvs", "").replace("_", "-") - py_by_service[svc] = parse_py_file(pf, helper_verbs) + py_by_service[svc] = parse_py_file(pf, helper_verbs, helper_bodies) flat = {m: (svc, d) for svc, ms in py_by_service.items() for m, d in ms.items()} @@ -489,10 +537,13 @@ def audit(service_filter: str | None, report_path: str, quiet: bool, issues.append(f"VERB {d.verb} != {req.verb}") if d.path and d.path != req.path: issues.append(f"PATH\n SDK: {d.path}\n API: {req.path}") + # Mismatch only when the documented class is not among those the + # SDK path accepts (several helpers accept a union). if (d.body_class and req.body_class - and d.body_class != req.body_class - and "RequestBody" in req.body_class): - issues.append(f"BODY {d.body_class} != {req.body_class}") + and "RequestBody" in req.body_class + and req.body_class not in d.body_class): + issues.append( + f"BODY sends {'|'.join(sorted(d.body_class))} != {req.body_class}") if issues: counts["mismatch"] += 1