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..5505a8ff --- /dev/null +++ b/omvs_audit_report.md @@ -0,0 +1,705 @@ +# OMVS Audit Report + +Ground truth: `pyegeria/http clients` (40 collections) +Subject: `pyegeria/omvs` (43 modules) + +| Result | Count | +|---|---| +| OK | 603 | +| Mismatch (verb/path/body) | 56 | +| 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-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` + - `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 /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` + + +### Service: action-author + + +### 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`) +- deleteActorRole: MISMATCH `delete_actor_role` + - PATH + SDK: /actor-manager/actor-roles/{}/delete + API: /actor-manager/actor-roles/delete + - BODY sends 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`) +- 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 + + +### 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`) +- 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 + 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 + + +### 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 + +- createAnnotationFromTemplate: MISMATCH `create_annotation_from_template` + - BODY sends NewElementRequestBody != TemplateRequestBody + +### 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`) + +### 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 sends DeleteClassificationRequestBody != DeleteElementRequestBody +- clearTermAsActivity: MISMATCH `remove_activity_description` + - BODY sends DeleteClassificationRequestBody != DeleteRelationshipRequestBody +- clearTermAsContext: MISMATCH `remove_is_context_definition` + - BODY sends DeleteClassificationRequestBody != DeleteRelationshipRequestBody + +### Service: governance-officer + +- getValidMetadataValues - domainIdentifier values: MISSING (`GET /valid-metadata/get-valid-metadata-values/domainIdentifier`) +- createRegulation: MISSING (`POST /governance-officer/governance-definitions`) +- removeRegulatorFromRegulation: MISSING (`POST /governance-officer/regulations/{}/regulators/organizations/{}/detach`) +- createGovernanceControl: MISSING (`POST /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`) +- updateGovernanceDefinitionStatus: MISSING (`POST /governance-officer/governance-definitions/{}/update`) +- 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 - with full request body: MISSING (`POST /governance-officer/governance-definitions/by-search-string`) +- getGovernanceDefinitionsByName - with full request body: MISSING (`POST /governance-officer/governance-definitions/by-name`) +- 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` + - BODY sends GetRequestBody != ResultsRequestBody +- addGovernanceDefinitionToElement: MISSING (`POST /governance-officer/elements/{}/governed-by/definition/{}/attach`) +- removeGovernanceDefinitionFromElement: MISSING (`POST /governance-officer/elements/{}/governed-by/definition/{}/detach`) +- linkApprovedPurpose: MISSING (`POST /governance-officer/elements/{}/approved-purposes/{}/attach`) +- detachApprovedPurpose: MISSING (`POST /governance-officer/elements/{}/approved-purposes/{}/detach`) +- updateLicense: ELSEWHERE -> `classification-explorer.py` +- unlicenseElement: ELSEWHERE -> `classification-explorer.py` +- 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 sends FilterRequestBody != UniqueNameRequestBody +- getMetadataElementHistory: MISMATCH `get_metadata_element_history` + - BODY sends GetRequestBody != HistoryRequestBody +- 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 + + +### 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`) +- 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`) + +### 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 sends 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 sends 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 + +- 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 +- 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 + +- 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`) +- 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 + +- createContextEventFromTemplate: MISMATCH `create_context_event_from_template` + - BODY sends NewElementRequestBody != TemplateRequestBody + +### 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`) +- linkSpecificationProperty: MISMATCH `link_specification_property` + - BODY sends NewElementRequestBody != NewRelationshipRequestBody diff --git a/pyegeria/omvs/action_author.py b/pyegeria/omvs/action_author.py index aa4a3e28..c407d7df 100644 --- a/pyegeria/omvs/action_author.py +++ b/pyegeria/omvs/action_author.py @@ -379,7 +379,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", @@ -451,7 +451,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 5c77aab5..fb21437d 100644 --- a/pyegeria/omvs/asset_maker.py +++ b/pyegeria/omvs/asset_maker.py @@ -1650,7 +1650,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 @@ -4856,7 +4856,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 5aa2bce6..8d762069 100644 --- a/pyegeria/omvs/collection_manager.py +++ b/pyegeria/omvs/collection_manager.py @@ -5423,7 +5423,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..fd785d8e 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, @@ -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/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 3d534f1f..dcba4066 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 @@ -6063,6 +6078,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..3115a37c --- /dev/null +++ b/scripts/omvs_audit.py @@ -0,0 +1,601 @@ +#!/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", +} + +# 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"} + +# 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 + + +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: frozenset[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 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 not (isinstance(t, ast.Attribute) + and isinstance(t.value, ast.Name) and t.value.id == "self" + 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 + # 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], + helper_bodies: dict[str, str] | None = None) -> 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 = 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 = ... + 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 (fname in helper_verbs + and not LOOKUP_HELPER_RE.search(fname or "")): + 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=helper_body or 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) + helper_bodies = discover_helper_bodies(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, helper_bodies) + + 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}") + # 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 "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 + 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())