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
69 changes: 69 additions & 0 deletions PYEGERIA_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,75 @@ on an Egeria Server capability that doesn't exist yet — but the pyegeria/
Dr.Egeria-side work each will need once that capability ships is written
into the entry now, so it isn't rediscovered from scratch later.

### ISSUE-122: `AssetMaker.get_catalog_targets` / `get_catalog_target` send `metadataElementTypeName="CatalogTarget"` (a relationship type) — server rejects with OMAG-COMMON-400-019, surfaced as SERVER_ERROR_500

**Status: fixed on branch `fix/issue-122-catalog-target-type` (2026-10-04), pending PR/merge**
(logged 2026-10-04 by the Resource Explorer design session; found read-only while
checking which catalog targets the PostgreSQL cataloguers hold).

**Fix + live verification (2026-10-04):** both methods now pass
`filter_results_by_type=False`; `_async_get_guid_request` gained that flag
(default `True`, mirroring `_async_get_results_body_request`).
`CatalogTargetProperties` gained `connectionName`, `metadataCollectionQualifiedName`,
`permittedSynchronization`, `deleteMethod`. Verified live with one throwaway Asset +
one CatalogTarget on the JDBC cataloguer (both removed afterwards; confirmed
not-found by GUID): list JSON/DICT/MD and single JSON work. **Second defect found
by that run:** `get_catalog_target` DICT/MD output was an all-blank record, because
the endpoint returns a *relationship* (`relationshipGUID`, `elementAtEnd1/2`) and the
element formatter has nothing to read. Added `_generate_catalog_target_output` for
the single get and verified it live in a second throwaway run (also removed,
not-found by GUID): the relationship's two ends are element stubs
(`guid`/`uniqueName`/`type`, no `properties`), so names come from `uniqueName`.
The daemon's log during the ~1s windows was not inspected.

Original report follows.

Both methods pass `_type="CatalogTarget"` into the generic results/guid request
helpers, which put it in the request body as `metadataElementTypeName`
(`pyegeria/omvs/asset_maker.py:1371` in `_async_get_catalog_target`, `:1480` in
`_async_get_catalog_targets`, present at 6.1.27). `CatalogTarget` is the
*relationship* type between an integration connector and its target, not an
element type, so the view server answers:

```
OMAG-COMMON-400-019 ... CatalogTarget ... is not a sub-type of OpenMetadataRoot
```

which pyegeria surfaces as `SERVER_ERROR_500`. The call therefore always fails
as shipped; nobody can list a connector's catalog targets through this client.

**How to trigger:**
```python
am = AssetMaker("qs-view-server", "https://localhost:9443", user, pw)
am.create_egeria_bearer_token(user, pw)
am.get_catalog_targets("70dcd0b7-9f06-48ad-ad44-ae4d7a7762aa") # JDBC cataloguer
# -> PyegeriaException SERVER_ERROR_500 wrapping OMAG-COMMON-400-019
```

**Working alternative (confirmed live 2026-10-04):** supply a body with no
element type, so the helper does not inject `CatalogTarget`:
```python
am.get_catalog_targets(
"70dcd0b7-9f06-48ad-ad44-ae4d7a7762aa",
body={"class": "ResultsRequestBody", "graphQueryDepth": 0},
)
# -> "No elements found" (zero targets on this platform)
```
The same applies to `get_catalog_target(relationship_guid)`.

**Likely fix (not made):** drop the `_type="CatalogTarget"` argument in both
methods (or pass the element type the caller wants, default none), since the
endpoint already scopes to catalog targets by URL. Not verified: the
per-target response shape of a non-empty result (no targets exist on the dev
platform to read), and whether `_generate_referenceable_output` handles the
relationship-plus-element shape the server returns.

Related: Resource Explorer's `evidence/CATALOGUE-LEVER-FINDINGS.md` §4–5
(trellis repo), which also notes `CatalogTargetProperties` (`asset_maker.py:46-51`)
lacks `deleteMethod`, `permittedSynchronization`, `connectionName` and
`metadataCollectionQualifiedName`, which the Java relationship accepts.


---

### ISSUE-114: `get_guid_for_name`'s miss-sentinel string ("No elements found") is truthy and repeatedly fools callers' existence checks — a caller guideline, not a candidate fix here
Expand Down
7 changes: 6 additions & 1 deletion pyegeria/core/_server_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6638,9 +6638,14 @@ async def _async_get_guid_request(self, url: str, _type: str, _gen_output: Calla
body: Optional[dict | GetRequestBody | ResultsRequestBody] = None,
max_mermaid_node_count=10,
body_model: type[GetRequestBody | ResultsRequestBody] = GetRequestBody,
filter_results_by_type: bool = True,
**kwargs) -> Any:
"""Retrieve an element by GUID.

Set `filter_results_by_type=False` when `_type` is only a rendering hint (or names a
relationship type, which the server rejects as a metadataElementTypeName), so it is
not sent in the default request body.

`body_model` selects the request-body class to send. It defaults to
GetRequestBody, which is what nearly every endpoint reached through
this helper documents. A few endpoints document ResultsRequestBody
Expand All @@ -6659,7 +6664,7 @@ async def _async_get_guid_request(self, url: str, _type: str, _gen_output: Calla
_type = _type.replace(" ", "")
body = {
"class": body_model.__name__,
"metadataElementTypeName": _type,
"metadataElementTypeName": _type if filter_results_by_type else None,
"includeOnlyRelationships": include_only_relationships,
"skipRelationships": skip_relationships,
"graphQueryDepth": graph_query_depth,
Expand Down
63 changes: 62 additions & 1 deletion pyegeria/omvs/asset_maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ class CatalogTargetProperties(ReferenceableProperties):
"""Properties for Catalog Target relationships"""
catalogTargetName: str | None = None
metadataSourceQualifiedName: str | None = None
connectionName: str | None = None
metadataCollectionQualifiedName: str | None = None
# One of BOTH_DIRECTIONS, FROM_THIRD_PARTY, TO_THIRD_PARTY (Egeria PermittedSynchronization)
permittedSynchronization: str | None = None
# One of LOOK_FOR_LINEAGE, ARCHIVE, SOFT_DELETE, PURGE (see DeleteMethod)
deleteMethod: str | None = None
templates: dict | None = None
configurationProperties: dict | None = None

Expand Down Expand Up @@ -1369,7 +1375,8 @@ async def _async_get_catalog_target(
return await self._async_get_guid_request(
url,
_type="CatalogTarget",
_gen_output=self._generate_referenceable_output,
_gen_output=self._generate_catalog_target_output,
filter_results_by_type=False, # CatalogTarget is a relationship type, not an element type
graph_query_depth=graph_query_depth,
output_format=output_format,
report_spec=report_spec,
Expand Down Expand Up @@ -1479,6 +1486,7 @@ async def _async_get_catalog_targets(
url,
_type="CatalogTarget",
_gen_output=self._generate_referenceable_output,
filter_results_by_type=False, # CatalogTarget is a relationship type, not an element type
graph_query_depth=graph_query_depth,
start_from=start_from,
page_size=page_size,
Expand Down Expand Up @@ -6799,6 +6807,59 @@ def get_integration_groups(
)
)

def _generate_catalog_target_output(
self,
elements: list | dict,
filter_string: Optional[str] = None,
element_type_name: Optional[str] = None,
output_format: str = "DICT",
report_spec: dict | str | None = None,
**kwargs
):
"""Format a single CatalogTarget *relationship* (ISSUE-122).

get_catalog_target returns a relationship (relationshipGUID / relationshipProperties /
elementAtEnd1 / elementAtEnd2), not an element, so the element formatters return an
all-blank record for it. `report_spec` is not applied: no spec describes a relationship.
"""
rel = elements[0] if isinstance(elements, list) and elements else elements
if not isinstance(rel, dict):
return rel

def _end(n: int) -> dict:
# The ends are element stubs (guid / uniqueName / type), not full elements --
# confirmed live 2026-10-04; there is no `properties` block to read names from.
end = rel.get(f"elementAtEnd{n}") or {}
return {
"guid": rel.get(f"elementGUIDAtEnd{n}") or end.get("guid", ""),
"type": (end.get("type") or {}).get("typeName", ""),
"name": end.get("uniqueName", ""),
}

end1, end2 = _end(1), _end(2)
props = (rel.get("relationshipProperties") or {}).get("propertiesAsStrings") or {}
row = {
"GUID": rel.get("relationshipGUID", ""),
"Type Name": (rel.get("relationshipType") or rel.get("type") or {}).get("typeName", "CatalogTarget"),
"Catalog Target Name": props.get("catalogTargetName", ""),
"Integration Connector GUID": end1["guid"],
"Integration Connector": end1["name"],
"Target Element GUID": end2["guid"],
"Target Element": end2["name"],
"Target Element Type": end2["type"],
"Relationship Properties": props,
}
if output_format.upper() == "DICT":
return [row]
lines = ["## Catalog Target", ""]
for k, v in row.items():
if k == "Relationship Properties":
lines += [f"### {k}", "| Parameter Name | Parameter Value |", "| :--- | :--- |"]
lines += [f"| {pk} | {pv} |" for pk, pv in v.items()]
else:
lines += [f"### {k}", str(v), ""]
return "\n".join(lines) + "\n"

def _generate_referenceable_output(
self,
elements: list | dict,
Expand Down
9 changes: 8 additions & 1 deletion tests/micro-tests/my_profile/test_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,14 @@ async def test_lists_own_subscriptions(self, mock_egeria):
self._subscription("sub-1", "Sales Feed", "garygeeke", "PROPOSED"),
self._subscription("sub-2", "Not Mine", "erinoverview"),
]
app = ScreenTestHostApp(ViewSubscriptionsScreen)
def screen_for_garygeeke():
# The screen takes its user from settings (EGERIA_USER); pin it so the
# "own subscriptions" filter doesn't depend on the developer's environment.
screen = ViewSubscriptionsScreen()
screen.user_name = "garygeeke"
return screen

app = ScreenTestHostApp(screen_for_garygeeke)
async with app.run_test() as pilot:
await pilot.pause()
assert client.find_collections.call_args.kwargs["metadata_element_type_name"] == "DigitalSubscription"
Expand Down
110 changes: 110 additions & 0 deletions tests/micro-tests/test_asset_maker_catalog_targets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
ISSUE-122: AssetMaker.get_catalog_target(s) must not send
metadataElementTypeName="CatalogTarget" -- that is a relationship type, and the
server rejects it (OMAG-COMMON-400-019).
"""
import json

import pytest

from pyegeria.omvs.asset_maker import AssetMaker, CatalogTargetProperties


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

def json(self):
return self._payload


def _client_capturing(bodies: list, payload: dict, monkeypatch) -> AssetMaker:
monkeypatch.setattr(AssetMaker, "check_connection", lambda self: "test")
client = AssetMaker("view-server", "https://localhost:1", "user", "pwd")

async def fake_make_request(method, url, body=None, *args, **kwargs):
bodies.append(json.loads(body) if isinstance(body, str) else body)
return _Response(payload)

client._async_make_request = fake_make_request
return client


@pytest.mark.asyncio
async def test_get_catalog_targets_sends_no_element_type(monkeypatch):
bodies: list = []
client = _client_capturing(bodies, {"elements": []}, monkeypatch)

await client._async_get_catalog_targets("conn-guid", output_format="JSON")

assert "metadataElementTypeName" not in bodies[0]


@pytest.mark.asyncio
async def test_get_catalog_target_sends_no_element_type(monkeypatch):
bodies: list = []
client = _client_capturing(bodies, {"element": {}}, monkeypatch)

await client._async_get_catalog_target("rel-guid", output_format="JSON")

assert "metadataElementTypeName" not in bodies[0]


@pytest.mark.asyncio
@pytest.mark.parametrize("flag, expected", [(True, "Widget"), (False, None)])
async def test_guid_request_filter_flag(flag, expected, monkeypatch):
bodies: list = []
client = _client_capturing(bodies, {"element": {}}, monkeypatch)

await client._async_get_guid_request(
"https://localhost:1/x", "Widget", lambda **kw: kw, output_format="JSON",
filter_results_by_type=flag)

assert bodies[0].get("metadataElementTypeName") == expected


def test_catalog_target_properties_keeps_relationship_fields():
props = CatalogTargetProperties(
qualifiedName="q", displayName="d", catalogTargetName="t", connectionName="c", metadataCollectionQualifiedName="m",
permittedSynchronization="TO_THIRD_PARTY", deleteMethod="SOFT_DELETE")
dumped = props.model_dump(exclude_none=True, by_alias=True)
for key in ("connectionName", "metadataCollectionQualifiedName",
"permittedSynchronization", "deleteMethod"):
assert key in dumped


# Trimmed from a live get_catalog_target response (2026-10-04): a relationship, not an element;
# its ends are element stubs (guid / uniqueName / type), with no `properties` block.
_REL = {
"relationshipGUID": "rel-1",
"relationshipType": {"typeName": "CatalogTarget"},
"relationshipProperties": {"propertiesAsStrings": {"catalogTargetName": "tgt"}},
"elementGUIDAtEnd1": "conn-1",
"elementAtEnd1": {"guid": "conn-1", "type": {"typeName": "IntegrationConnector"},
"uniqueName": "JDBC Cataloguer"},
"elementGUIDAtEnd2": "asset-1",
"elementAtEnd2": {"guid": "asset-1", "type": {"typeName": "Asset"}, "uniqueName": "q-asset"},
}


@pytest.mark.asyncio
async def test_get_catalog_target_dict_is_not_blank(monkeypatch):
client = _client_capturing([], {"element": _REL}, monkeypatch)

rows = await client._async_get_catalog_target("rel-1", output_format="DICT")

assert rows[0]["GUID"] == "rel-1"
assert rows[0]["Catalog Target Name"] == "tgt"
assert rows[0]["Integration Connector"] == "JDBC Cataloguer"
assert rows[0]["Target Element GUID"] == "asset-1"
assert rows[0]["Target Element"] == "q-asset"


@pytest.mark.asyncio
async def test_get_catalog_target_md_is_not_blank(monkeypatch):
client = _client_capturing([], {"element": _REL}, monkeypatch)

md = await client._async_get_catalog_target("rel-1", output_format="MD")

assert "rel-1" in md and "tgt" in md and "JDBC Cataloguer" in md
assert "NO DISPLAY NAME" not in md
Loading