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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions md_processing/md_processing_utils/common_md_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,9 +605,11 @@ def set_gov_prop_body(object_type: str, qualified_name: str, attributes: dict)->
prop_bod["implications"] = attributes.get('Implications', {}).get('value', [])
prop_bod["outcomes"] = attributes.get('Outcomes', {}).get('value', [])
prop_bod["results"] = attributes.get('Results', {}).get('value', []) or []
prop_bod["entitlements"] = attributes.get('Entitlements', {}).get('value', [])
prop_bod["obligations"] = attributes.get('Obligations', {}).get('value', [])
prop_bod["restrictions"] = attributes.get('Restrictions', {}).get('value', [])
# ISSUE-157: Egeria types these as Map<String, String> (TermsAndConditionsProperties and its
# subtypes, e.g. ServiceLevelObjectiveProperties); a [] default made the server refuse the body.
prop_bod["entitlements"] = attributes.get('Entitlements', {}).get('value', None)
prop_bod["obligations"] = attributes.get('Obligations', {}).get('value', None)
prop_bod["restrictions"] = attributes.get('Restrictions', {}).get('value', None)
prop_bod["usage"] = attributes.get('Usage', {}).get('value', None)
prop_bod["effectiveFrom"] = attributes.get('Effective From', {}).get('value', None)
prop_bod["effectiveTo"] = attributes.get('Effective To', {}).get('value', None)
Expand Down
55 changes: 36 additions & 19 deletions pyegeria/omvs/automated_curation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3975,6 +3975,25 @@ def get_catalog_target(
)
return response

@staticmethod
def _catalog_target_properties(catalog_target_name: str, connection_name: Optional[str],
metadata_src_qual_name: Optional[str], config_properties: Optional[dict],
template_properties: Optional[dict], permitted_sync: Optional[str],
delete_method: Optional[str]) -> dict:
"""Build the CatalogTargetProperties for add/update_catalog_target, omitting unset fields.
`template_properties` maps to Egeria's `templates` (template name -> template GUID)."""
props = {
"class": "CatalogTargetProperties",
"catalogTargetName": catalog_target_name,
"connectionName": connection_name,
"metadataSourceQualifiedName": metadata_src_qual_name,
"configurationProperties": config_properties,
"templates": template_properties,
"permittedSynchronization": permitted_sync,
"deleteMethod": delete_method,
}
return {k: v for k, v in props.items() if v is not None}

async def _async_add_catalog_target(
self,
integ_connector_guid: str,
Expand Down Expand Up @@ -4032,19 +4051,18 @@ async def _async_add_catalog_target(

url = f"{self.ref_curation_command_base}/integration-connectors/{integ_connector_guid}/catalog-targets/{metadata_element_guid}"

# ISSUE-156: Egeria takes NewRelationshipRequestBody with CatalogTargetProperties
# (AssetMakerResource.addCatalogTarget); there is no CatalogTargetRequestBody class.
body = {
"class": "CatalogTargetRequestBody",
"catalogTargetName": catalog_target_name,
"connectionName": connection_name,
"metadataSourceQualifiedName": metadata_src_qual_name,
"configurationProperties": config_properties,
"templateProperties": template_properties,
"permittedSynchronization": permitted_sync,
"deleteMethod": delete_method,
"class": "NewRelationshipRequestBody",
"properties": self._catalog_target_properties(
catalog_target_name, connection_name, metadata_src_qual_name,
config_properties, template_properties, permitted_sync, delete_method,
),
}

response = await self._async_make_request("POST", url, body)
return response.json().get("guid", "No Guid returned")
guid = await self._async_new_relationship_request(url, ["CatalogTargetProperties"], body)
return guid or "No Guid returned"

def add_catalog_target(
self,
Expand Down Expand Up @@ -4155,18 +4173,17 @@ async def _async_update_catalog_target(

url = f"{self.ref_curation_command_base}/catalog-targets/{relationship_guid}/update"

# ISSUE-156: Egeria takes UpdateRelationshipRequestBody with CatalogTargetProperties
# (AssetMakerResource.updateCatalogTarget).
body = {
"class": "CatalogTargetRequestBody",
"catalogTargetName": catalog_target_name,
"connectionName": connection_name,
"metadataSourceQualifiedName": metadata_src_qual_name,
"configurationProperties": config_properties,
"templateProperties": template_properties,
"permittedSynchronization": permitted_sync,
"deleteMethod": delete_method,
"class": "UpdateRelationshipRequestBody",
"properties": self._catalog_target_properties(
catalog_target_name, connection_name, metadata_src_qual_name,
config_properties, template_properties, permitted_sync, delete_method,
),
}

await self._async_make_request("POST", url, body)
await self._async_update_relationship_request(url, ["CatalogTargetProperties"], body)

def update_catalog_target(
self,
Expand Down
101 changes: 101 additions & 0 deletions tests/micro-tests/test_issue_156_157_catalog_target_slo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
ISSUE-156: AutomatedCuration.add/update_catalog_target posted a flat
"CatalogTargetRequestBody", a class Egeria does not have; the endpoints take
New/UpdateRelationshipRequestBody with CatalogTargetProperties.
ISSUE-157: set_gov_prop_body defaulted entitlements/obligations/restrictions
to [], which Egeria cannot read into its Map<String, String> fields, so
Create Service Level Objective was refused.
No live server: the HTTP call is captured and the body asserted.
"""
import json

import pytest

from pyegeria.omvs.automated_curation import AutomatedCuration
from md_processing.md_processing_utils.common_md_utils import set_gov_prop_body

CONNECTOR = "11111111-1111-1111-1111-111111111111"
ELEMENT = "22222222-2222-2222-2222-222222222222"
REL = "33333333-3333-3333-3333-333333333333"


class _Response:
def __init__(self, payload):
self._payload = payload

def json(self):
return self._payload


def _client(calls: list, monkeypatch) -> AutomatedCuration:
monkeypatch.setattr(AutomatedCuration, "check_connection", lambda self: "test")
client = AutomatedCuration("view-server", "https://localhost:1", "user", "pwd")

async def fake_make_request(method, url, payload_=None, *args, **kwargs):
calls.append({"method": method, "url": url,
"body": json.loads(payload_) if isinstance(payload_, str) else payload_})
return _Response({"class": "GUIDResponse", "guid": "rel-guid"})

client._async_make_request = fake_make_request
return client


@pytest.mark.asyncio
async def test_add_catalog_target_sends_new_relationship_request_body(monkeypatch):
calls = []
client = _client(calls, monkeypatch)
guid = await client._async_add_catalog_target(
CONNECTOR, ELEMENT, "target-1", connection_name="conn",
config_properties={"a": "b"}, template_properties={"tmpl": "tmpl-guid"})

assert guid == "rel-guid"
call = calls[0]
assert call["method"] == "POST"
assert call["url"].endswith(
f"/automated-curation/integration-connectors/{CONNECTOR}/catalog-targets/{ELEMENT}")
body = call["body"]
assert body["class"] == "NewRelationshipRequestBody"
assert "catalogTargetName" not in body # no flat properties at the top level
assert body["properties"] == {
"class": "CatalogTargetProperties",
"catalogTargetName": "target-1",
"connectionName": "conn",
"configurationProperties": {"a": "b"},
"templates": {"tmpl": "tmpl-guid"},
"permittedSynchronization": "BOTH_DIRECTIONS",
"deleteMethod": "ARCHIVE",
}


@pytest.mark.asyncio
async def test_update_catalog_target_sends_update_relationship_request_body(monkeypatch):
calls = []
client = _client(calls, monkeypatch)
await client._async_update_catalog_target(REL, "target-2", permitted_sync="FROM_THIRD_PARTY")

call = calls[0]
assert call["url"].endswith(f"/automated-curation/catalog-targets/{REL}/update")
body = call["body"]
assert body["class"] == "UpdateRelationshipRequestBody"
assert body["properties"]["class"] == "CatalogTargetProperties"
assert body["properties"]["catalogTargetName"] == "target-2"
assert body["properties"]["permittedSynchronization"] == "FROM_THIRD_PARTY"
assert "connectionName" not in body["properties"]


def _attrs(**values):
return {k: {"value": v} for k, v in values.items()}


def test_service_level_objective_body_has_no_list_valued_map_fields():
body = set_gov_prop_body("Service Level Objective", "SLO::pyeg-test",
_attrs(**{"Display Name": "Within an hour"}))
for field in ("entitlements", "obligations", "restrictions"):
assert body.get(field) is None, f"{field} must not default to a list"


def test_terms_and_conditions_maps_are_still_passed_through():
body = set_gov_prop_body("Terms And Conditions", "TC::pyeg-test",
_attrs(**{"Display Name": "T&C", "Entitlements": {"read": "yes"}}))
assert body["entitlements"] == {"read": "yes"}
assert body.get("obligations") is None
Loading