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
112 changes: 112 additions & 0 deletions pyegeria/omvs/asset_maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2480,6 +2480,118 @@ def detach_data_set_content(
self._async_detach_data_set_content(data_set_guid, data_content_asset_guid, body)
)

# DataSetContent is a multi-link relationship: the same data set and content may be linked more than once
# (for example, once for each information supply chain). The two methods below therefore address ONE
# relationship by its own unique identifier, rather than by the two linked elements.

@dynamic_catch
async def _async_update_data_set_content(
self,
data_set_content_guid: str,
body: dict | UpdateRelationshipRequestBody | None = None,
) -> None:
"""Update the properties of one DataSetContent relationship. Async version.

Parameters
----------
data_set_content_guid: str
Unique identifier of the DataSetContent relationship (not of the data set or the content).
body: dict | UpdateRelationshipRequestBody, optional
The properties to update.

Returns
-------
None

Raises
------
PyegeriaException
One of the pyegeria exceptions will be raised if there are issues in communications, message format, or
Egeria errors.

Notes
-----
Sample body:
{
"class" : "UpdateRelationshipRequestBody",
"mergeUpdate": true,
"properties": {
"class": "DataSetContentProperties",
"queryId": "add id that is used on overall formula",
"query": "add query",
"queryType": "add query type",
"iscQualifiedName": "add qualified name"
}
}
"""
url = f"{self.asset_command_root}/data-set-content/{data_set_content_guid}/update"
await self._async_update_relationship_request(url, ["DataSetContentProperties"], body)

@dynamic_catch
def update_data_set_content(
self,
data_set_content_guid: str,
body: dict | UpdateRelationshipRequestBody | None = None,
) -> None:
"""Update the properties of one DataSetContent relationship.

Parameters
----------
data_set_content_guid: str
Unique identifier of the DataSetContent relationship (not of the data set or the content).
body: dict | UpdateRelationshipRequestBody, optional
The properties to update.
"""
loop = asyncio.get_event_loop()
loop.run_until_complete(self._async_update_data_set_content(data_set_content_guid, body))

@dynamic_catch
async def _async_detach_data_set_content_relationship(
self,
data_set_content_guid: str,
body: dict | DeleteRelationshipRequestBody | None = None,
) -> None:
"""Remove one DataSetContent relationship, leaving any others between the same elements in place.
Async version.

Parameters
----------
data_set_content_guid: str
Unique identifier of the DataSetContent relationship (not of the data set or the content).
body: dict | DeleteRelationshipRequestBody, optional
Additional parameters for the delete operation.

Returns
-------
None

Raises
------
PyegeriaException
One of the pyegeria exceptions will be raised if there are issues in communications, message format, or
Egeria errors.
"""
url = f"{self.asset_command_root}/data-set-content/{data_set_content_guid}/detach"
await self._async_delete_relationship_request(url, body)

@dynamic_catch
def detach_data_set_content_relationship(
self,
data_set_content_guid: str,
body: dict | DeleteRelationshipRequestBody | None = None,
) -> None:
"""Remove one DataSetContent relationship, leaving any others between the same elements in place.

Parameters
----------
data_set_content_guid: str
Unique identifier of the DataSetContent relationship (not of the data set or the content).
body: dict | DeleteRelationshipRequestBody, optional
Additional parameters for the delete operation.
"""
loop = asyncio.get_event_loop()
loop.run_until_complete(self._async_detach_data_set_content_relationship(data_set_content_guid, body))

#
# Report Methods
#
Expand Down
41 changes: 41 additions & 0 deletions pyegeria/omvs/connection_maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""

from pyegeria.core._server_client import ServerClient
from pyegeria.core.utils import dynamic_catch
from pyegeria.models.models import (
NewElementRequestBody,
UpdateElementRequestBody,
Expand Down Expand Up @@ -1011,6 +1012,46 @@ def detach_asset_from_connection(self, asset_guid: str, connection_guid: str, bo
loop = asyncio.get_event_loop()
return loop.run_until_complete(self._async_detach_asset_from_connection(asset_guid, connection_guid, body))

# Generalisation of link_asset_to_connection / detach_asset_from_connection: any element that has a digital
# resource (not only an asset) can be attached to the connection that accesses that resource.

@dynamic_catch
async def _async_link_resource_to_connection(self, element_guid: str, connection_guid: str, body: Union[NewRelationshipRequestBody, dict] = None):
"""Attach an element to the connection that accesses its digital resource. Async version.

Sample body:
{
"class" : "NewRelationshipRequestBody",
"properties": {
"class": "ResourceConnectionProperties",
"label": "add label here",
"description": "add description here",
"effectiveFrom": "2024-01-01T00:00:00.000+00:00",
"effectiveTo": "2024-12-31T23:59:59.999+00:00"
}
}
"""
url = f"{self.base_url}/elements/{element_guid}/connections/{connection_guid}/attach"
await self._async_new_relationship_request(url, ["ResourceConnectionProperties"], body)

@dynamic_catch
def link_resource_to_connection(self, element_guid: str, connection_guid: str, body: Union[NewRelationshipRequestBody, dict] = None):
"""Attach an element to the connection that accesses its digital resource."""
loop = asyncio.get_event_loop()
return loop.run_until_complete(self._async_link_resource_to_connection(element_guid, connection_guid, body))

@dynamic_catch
async def _async_detach_resource_from_connection(self, element_guid: str, connection_guid: str, body: Union[DeleteRelationshipRequestBody, dict] = None):
"""Detach an element from the connection that accesses its digital resource. Async version."""
url = f"{self.base_url}/elements/{element_guid}/connections/{connection_guid}/detach"
await self._async_delete_relationship_request(url, body)

@dynamic_catch
def detach_resource_from_connection(self, element_guid: str, connection_guid: str, body: Union[DeleteRelationshipRequestBody, dict] = None):
"""Detach an element from the connection that accesses its digital resource."""
loop = asyncio.get_event_loop()
return loop.run_until_complete(self._async_detach_resource_from_connection(element_guid, connection_guid, body))

async def _async_link_endpoint_to_it_asset(self, asset_guid: str, endpoint_guid: str, body: Union[NewRelationshipRequestBody, dict] = None):
url = f"{self.base_url}/assets/{asset_guid}/endpoints/{endpoint_guid}/attach"
await self._async_new_relationship_request(url, ["ITAssetEndpoint"], body)
Expand Down
136 changes: 136 additions & 0 deletions tests/micro-tests/test_audit_gap_small_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""
Audit-gap methods added from the refreshed Egeria .http collections:
asset-maker: update_data_set_content, detach_data_set_content_relationship
connection-maker: link_resource_to_connection, detach_resource_from_connection
No live server: the HTTP call is captured and the verb, URL and body asserted.
"""
import json

import pytest

from pyegeria.omvs.asset_maker import AssetMaker
from pyegeria.omvs.connection_maker import ConnectionMaker


class _Response:
def json(self):
return {"class": "VoidResponse", "relatedHTTPCode": 200}


def _capture(client, calls: list):
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()

client._async_make_request = fake_make_request
return client


def _asset_maker(calls, monkeypatch):
monkeypatch.setattr(AssetMaker, "check_connection", lambda self: "test")
return _capture(AssetMaker("view-server", "https://localhost:1", "user", "pwd"), calls)


def _connection_maker(calls, monkeypatch):
monkeypatch.setattr(ConnectionMaker, "check_connection", lambda self: "test")
return _capture(ConnectionMaker("view-server", "https://localhost:1", "user", "pwd"), calls)


ASSET_ROOT = "/servers/view-server/api/open-metadata/asset-maker"
CONN_ROOT = "/servers/view-server/api/open-metadata/connection-maker"


@pytest.mark.asyncio
async def test_update_data_set_content_addresses_the_relationship_by_its_own_guid(monkeypatch):
calls: list = []
client = _asset_maker(calls, monkeypatch)
props = {"class": "DataSetContentProperties", "queryId": "q1", "query": "select 1",
"queryType": "SQL", "iscQualifiedName": "isc::one"}

await client._async_update_data_set_content(
"rel-guid", {"class": "UpdateRelationshipRequestBody", "mergeUpdate": True, "properties": props})

assert calls[0]["method"] == "POST"
assert calls[0]["url"].endswith(f"{ASSET_ROOT}/data-set-content/rel-guid/update")
assert calls[0]["body"]["class"] == "UpdateRelationshipRequestBody"
assert calls[0]["body"]["mergeUpdate"] is True
# none of the DataSetContent properties is dropped on the way out
assert calls[0]["body"]["properties"] == props


@pytest.mark.asyncio
async def test_detach_data_set_content_relationship_with_body(monkeypatch):
calls: list = []
client = _asset_maker(calls, monkeypatch)

await client._async_detach_data_set_content_relationship(
"rel-guid", {"class": "DeleteRelationshipRequestBody", "externalSourceGUID": "src"})

assert calls[0]["method"] == "POST"
assert calls[0]["url"].endswith(f"{ASSET_ROOT}/data-set-content/rel-guid/detach")
assert calls[0]["body"]["class"] == "DeleteRelationshipRequestBody"
assert calls[0]["body"]["externalSourceGUID"] == "src"


@pytest.mark.asyncio
async def test_detach_data_set_content_relationship_without_body(monkeypatch):
calls: list = []
client = _asset_maker(calls, monkeypatch)

await client._async_detach_data_set_content_relationship("rel-guid")

assert calls[0]["url"].endswith(f"{ASSET_ROOT}/data-set-content/rel-guid/detach")
assert calls[0]["body"] is None


@pytest.mark.asyncio
async def test_new_methods_do_not_reuse_the_two_element_paths(monkeypatch):
# the older attach/detach address the two linked elements; the new ones must not
calls: list = []
client = _asset_maker(calls, monkeypatch)

await client._async_detach_data_set_content_relationship("rel-guid")

assert "/data-sets/" not in calls[0]["url"]


@pytest.mark.asyncio
async def test_link_resource_to_connection(monkeypatch):
calls: list = []
client = _connection_maker(calls, monkeypatch)
props = {"class": "ResourceConnectionProperties", "label": "primary", "description": "d"}

await client._async_link_resource_to_connection(
"element-guid", "connection-guid", {"class": "NewRelationshipRequestBody", "properties": props})

assert calls[0]["method"] == "POST"
assert calls[0]["url"].endswith(f"{CONN_ROOT}/elements/element-guid/connections/connection-guid/attach")
assert calls[0]["body"]["class"] == "NewRelationshipRequestBody"
assert calls[0]["body"]["properties"] == props


@pytest.mark.asyncio
async def test_detach_resource_from_connection_sends_delete_options(monkeypatch):
calls: list = []
client = _connection_maker(calls, monkeypatch)

await client._async_detach_resource_from_connection(
"element-guid", "connection-guid",
{"class": "DeleteRelationshipRequestBody", "deleteMethod": "SOFT_DELETE"})

assert calls[0]["method"] == "POST"
assert calls[0]["url"].endswith(f"{CONN_ROOT}/elements/element-guid/connections/connection-guid/detach")
assert calls[0]["body"]["deleteMethod"] == "SOFT_DELETE"


@pytest.mark.asyncio
async def test_link_resource_is_distinct_from_the_asset_only_path(monkeypatch):
calls: list = []
client = _connection_maker(calls, monkeypatch)

await client._async_link_resource_to_connection("e", "c", {"class": "NewRelationshipRequestBody"})
await client._async_link_asset_to_connection("a", "c", {"class": "NewRelationshipRequestBody"})

assert "/elements/e/connections/c/attach" in calls[0]["url"]
assert "/assets/a/connections/c/attach" in calls[1]["url"]
Loading