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
30 changes: 30 additions & 0 deletions PYEGERIA_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,36 @@ changed beyond that one status correction and this note.

---

### ISSUE-115: `MyProfile.get_my_resources` always failed — its rendering hint `"Resource"` was sent as `metadataElementTypeName`

**Layer:** Pyegeria · **Status:** fixed 2026-10-02 · **Found:** 2026-10-02
(while looking for a profile-scoped way to list a user's collections for
the My Profile app's bookmarks).

**What:** every call failed with a 400 —
`OMAG-COMMON-400-018 The type name Resource passed on method getEntityTypeGUID
of service Open Metadata Store Services is not recognized`. `_async_get_my_resources`
passed `_type="Resource"` to `_async_get_results_body_request`, whose default
`filter_results_by_type=True` copies `_type` into the default request body as
`metadataElementTypeName`. `Resource` is not an Egeria type. The ground truth
(`Egeria-api-my-profile.http`, `getMyResources` →
`POST .../my-profile/assigned-resources`) sends a plain `ResultsRequestBody`
with no type filter, and the resources linked to a profile through
`ResourceList` can be of any type (Collections, for example).

**Why it went unnoticed:** `tests/functional-tests/test_my_profile.py::test_get_my_resources`
catches `PyegeriaException` and only prints "failed as expected or due to
env", so the 400 never failed the test.

**Fixed:** pass `filter_results_by_type=False`, keeping `"Resource"` as a
rendering hint only — the same fix as `get_collection_members`
(`collection_manager.py`). Verified live: Gary Geeke's profile now returns its
`ResourceList`-linked `Bookmarks::garygeeke` collection in JSON, DICT and LIST
formats. Covered by `tests/micro-tests/test_my_profile_get_my_resources_body.py`,
which captures the outgoing body (fails without the fix).

---

### ISSUE-109: `Link Product Dependency` was missing `ISC Qualified Name` — an earlier fix wrongly confirmed it as "correctly absent" against a stale local `.http` copy

**Layer:** Pyegeria · **Status:** fixed 2026-09-21 · **Found:** 2026-09-20
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ contributors who need more depth than the top-level `README.md`/`CLAUDE.md`/

| File | Covers |
|---|---|
| `developer_guide_pyegeria_textual.md` | Developer Guide for Python programmers new to Egeria and Textual — step-by-step examples (`examples/developer_guide/`), then My Profile, Dr.Egeria and hey_egeria as case studies. Screenshots in `images/developer-guide/`. |
| `dr_egeria_manual.md` | The Dr.Egeria user manual — command reference, markdown authoring conventions, attribute styles. |
| `my_profile_app_manual.md` | The My Profile App user manual — configuration, dashboard overview, keyboard shortcuts. |
| `My-Egeria-Doc.md` | In-depth architecture, workflow flows, and sequence diagrams for My Profile / MyEgeria. |
Expand Down
1,028 changes: 1,028 additions & 0 deletions docs/developer_guide_pyegeria_textual.md

Large diffs are not rendered by default.

235 changes: 235 additions & 0 deletions docs/images/developer-guide/my_profile_bookmark_prefilled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
234 changes: 234 additions & 0 deletions docs/images/developer-guide/my_profile_main.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
232 changes: 232 additions & 0 deletions docs/images/developer-guide/my_profile_shop_for_data.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
237 changes: 237 additions & 0 deletions docs/images/developer-guide/my_profile_splash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
230 changes: 230 additions & 0 deletions docs/images/developer-guide/step2_glossary_browser.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
242 changes: 242 additions & 0 deletions docs/images/developer-guide/step3_add_comment.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions docs/my_profile_app_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The **My Profile App** is a Textual-based terminal user interface (TUI) applicat
- [Communities and Roles](#communities-and-roles)
- [Actions and Work Items](#actions-and-work-items)
- [Catalog and Shopping for Data](#catalog-and-shopping-for-data)
- [Bookmarks](#bookmarks)
- [Technology Type Explorer](#technology-type-explorer)
6. [Keyboard Shortcuts](#keyboard-shortcuts)
7. [Troubleshooting](#troubleshooting)
Expand Down Expand Up @@ -113,6 +114,14 @@ The Actions table provides a real-time view of your responsibilities. You can tr

The **Shop for Data** function allows you to browse registered catalogs, dictionaries, and glossaries. You can explore the metadata hierarchy and find assets relevant to your work.

### Bookmarks

Bookmarks let you keep a personal list of elements you want to come back to. They are stored in Egeria as members of a private collection, `Bookmarks::<your user id>`. The collection is attached to your profile through a `ResourceList` relationship with resource use "Bookmarks", and is created automatically the first time you add a bookmark.

- **Open bookmarks** with `Ctrl+B` (or **User Bookmarks** in Other Functions). If a row is highlighted in a table on the main screen or the Shop for Data screen, its GUID is picked up for you. Tables without a GUID column, such as the glossary table, have the GUID looked up from the row's qualified name.
- **Add a bookmark** with `Ctrl+N`. The GUID box is pre-filled with the highlighted row's GUID, and you can edit it or paste another one. Then press **Add New Bookmark**.
- **Delete a bookmark** by highlighting it in the bookmarks table and pressing `Ctrl+R`. The GUID box is pre-filled with that bookmark's GUID. Then press **Delete Bookmark**. This removes the element from your bookmarks collection; the element itself is not deleted.

### Technology Type Explorer

The **Technology Types** explorer provides a deep dive into the technical metadata registered in Egeria. You can browse technology types, templates, and associated processes, helping you understand the technical landscape of your metadata environment.
Expand All @@ -125,6 +134,8 @@ The application supports the following global keyboard shortcuts:
| --- | --- |
| `q` | Quit the application |
| `r` | Refresh data from the Egeria server |
| `Ctrl+B` | Open bookmarks, picking up the highlighted row's GUID (main and Shop for Data screens) |
| `Ctrl+N` / `Ctrl+R` | Add / delete a bookmark (on the bookmarks screen) |
| `Ctrl+C` | Force quit (standard terminal shortcut) |

## Troubleshooting
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ examples/<script>.py` after adjusting connection settings (or via a local
| `GeoSpatial Products Example.py` | Digital product catalog example. |
| `format_sets_save_load_example.py`, `output_formats_example.py` | Saving/loading custom report `FormatSet`s and generating output in different formats. |
| `extract_attribute_test.py` | Small standalone parser/extraction check. |
| `developer_guide/` | The step-by-step pyegeria + Textual examples from `docs/developer_guide_pyegeria_textual.md` (own `README.md`). |

Subdirectories:
- `doc_samples/` — worked examples referenced from `docs/output-formats-and-report-specs.md`.
Expand Down
32 changes: 32 additions & 0 deletions examples/developer_guide/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!-- SPDX-License-Identifier: CC-BY-4.0 -->
<!-- Copyright Contributors to the ODPi Egeria project. -->

# examples/developer_guide

Runnable examples for the
[Developer Guide: Building Egeria Applications in Python with pyegeria and Textual](../../docs/developer_guide_pyegeria_textual.md).
Each step builds on the previous one.

| File | Demonstrates |
|---|---|
| `common.py` | Reading connection settings from pyegeria's configuration; a glossary query that returns GUIDs. |
| `step1_hello_pyegeria.py` | pyegeria only: connect, log in, list glossaries, handle `PyegeriaException`. |
| `step2_glossary_browser.py` | A Textual app: `DataTable`, bindings, CSS, and a thread worker that loads data from Egeria. |
| `step3_glossary_comments.py` | Acting on the selected row: a `ModalScreen` pre-filled with the row's GUID, returning a result to a callback that adds a comment in Egeria. **Writes to Egeria.** |
| `capture_screenshots.py` | Regenerates the guide's screenshots in `docs/images/developer-guide/` by driving these examples and the My Profile app headlessly. Read-only. |

All of them need a running Egeria. The
[Egeria Workspaces quick start](https://egeria-project.org/egeria-workspaces/quick-start/overview/)
provides one with the defaults these examples assume (`https://localhost:9443`,
`qs-view-server`, user `garygeeke`). Override them with a `.env` file or environment
variables, as described in the guide.

```bash
python examples/developer_guide/step1_hello_pyegeria.py
python examples/developer_guide/step2_glossary_browser.py
python examples/developer_guide/step3_glossary_comments.py
python examples/developer_guide/capture_screenshots.py
```

The scripts import `common.py` from their own folder, and Python puts the script's folder on
the import path, so they can be run from any working directory.
100 changes: 100 additions & 0 deletions examples/developer_guide/capture_screenshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
PDX-License-Identifier: Apache-2.0
Copyright Contributors to the ODPi Egeria project.

Regenerates the Developer Guide screenshots (docs/images/developer-guide/*.svg) by driving
the example apps and the My Profile app headlessly with Textual's test Pilot.
Needs a running Egeria (e.g. the egeria-workspaces quick start). Nothing is written to
Egeria: the comment and bookmark dialogs are captured before they are submitted.

Run: python examples/developer_guide/capture_screenshots.py
"""

import asyncio
import sys
from pathlib import Path

from textual.app import App
from textual.pilot import Pilot
from textual.widgets import DataTable, Input

HERE = Path(__file__).resolve().parent
REPO_ROOT = HERE.parents[1]
IMAGES = REPO_ROOT / "docs" / "images" / "developer-guide"
MY_PROFILE_DIR = REPO_ROOT / "my_egeria" / "my_egeria" / "DemoCode" / "My_Profile"
SIZE = (150, 42)

sys.path.append(str(MY_PROFILE_DIR))


async def wait_for_table(pilot: Pilot, table: DataTable, timeout: float = 60.0) -> None:
"""Wait until a DataTable has finished loading and has rows."""
for _ in range(int(timeout / 0.5)):
await pilot.pause(0.5)
if not table.loading and table.row_count:
return


def save(app: App, name: str) -> None:
app.save_screenshot(f"{name}.svg", path=str(IMAGES))
print(f"saved {IMAGES / name}.svg")


async def capture_examples() -> None:
from step2_glossary_browser import GlossaryBrowserApp
from step3_glossary_comments import GlossaryCommentsApp

app = GlossaryBrowserApp()
async with app.run_test(size=SIZE) as pilot:
await wait_for_table(pilot, app.query_one(DataTable))
await pilot.pause(4) # let the "Loaded n glossaries" toast clear
save(app, "step2_glossary_browser")

app = GlossaryCommentsApp()
async with app.run_test(size=SIZE) as pilot:
await wait_for_table(pilot, app.query_one(DataTable))
await pilot.pause(4)
await pilot.press("down", "down", "ctrl+a")
await pilot.pause()
app.screen.query_one("#comment", Input).value = "Is this glossary still maintained?"
await pilot.pause()
save(app, "step3_add_comment")


async def capture_my_profile() -> None:
from my_profile_app import MyProfileApp

app = MyProfileApp()
async with app.run_test(size=SIZE) as pilot:
await pilot.pause(1)
save(app, "my_profile_splash")

await pilot.click("#continue")
await wait_for_table(pilot, app.get_screen("main").query_one("#roles_table", DataTable))
await pilot.pause(1)
save(app, "my_profile_main")

await app.handle_shop_for_data_option()
await pilot.pause()
for table_id in ("#glossary_table", "#data_dictionary_table"):
await wait_for_table(pilot, app.screen.query_one(table_id, DataTable))
await pilot.pause(1)
save(app, "my_profile_shop_for_data")

# Bookmark the highlighted glossary: Ctrl+B opens bookmarks, Ctrl+N shows the pre-filled GUID
app.screen.query_one("#glossary_table", DataTable).focus()
await pilot.press("down", "ctrl+b")
await pilot.pause(2)
await pilot.press("ctrl+n")
await pilot.pause()
save(app, "my_profile_bookmark_prefilled")


async def main() -> None:
IMAGES.mkdir(parents=True, exist_ok=True)
await capture_examples()
await capture_my_profile()


if __name__ == "__main__":
asyncio.run(main())
59 changes: 59 additions & 0 deletions examples/developer_guide/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
PDX-License-Identifier: Apache-2.0
Copyright Contributors to the ODPi Egeria project.

Shared helpers for the Developer Guide examples: connection settings and a glossary query.
"""

from dataclasses import dataclass

from pyegeria import EgeriaTech, load_app_config, settings


@dataclass
class Connection:
view_server: str
platform_url: str
user_name: str
user_password: str


def connection_settings() -> Connection:
"""Read connection details from pyegeria's configuration (.env, config.json or OS environment),
falling back to the Egeria quick-start defaults for anything that isn't set."""
load_app_config()
env = settings.Environment
user = settings.User_Profile
return Connection(
view_server=env.egeria_view_server or "qs-view-server",
platform_url=env.egeria_platform_url or "https://localhost:9443",
user_name=user.user_name or "garygeeke",
user_password=user.user_pwd or "secret",
)


def fetch_glossaries(conn: Connection, search_string: str = "*") -> list[dict]:
"""Return one dict per glossary with its display name, qualified name, description and GUID.

find_glossaries(output_format="JSON") returns the raw Egeria elements: each has an
"elementHeader" (holding the GUID) and a "properties" dict. A str is returned instead
of a list when nothing matches.
"""
client = EgeriaTech(conn.view_server, conn.platform_url, conn.user_name, conn.user_password)
try:
client.create_egeria_bearer_token(conn.user_name, conn.user_password)
elements = client.find_glossaries(search_string, output_format="JSON", graph_query_depth=0)
finally:
client.close_session()

if not isinstance(elements, list):
return []
return [
{
"Display Name": element["properties"].get("displayName", ""),
"Qualified Name": element["properties"].get("qualifiedName", ""),
"Description": element["properties"].get("description", ""),
"GUID": element["elementHeader"]["guid"],
}
for element in elements
]
32 changes: 32 additions & 0 deletions examples/developer_guide/step1_hello_pyegeria.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
PDX-License-Identifier: Apache-2.0
Copyright Contributors to the ODPi Egeria project.

Developer Guide step 1: connect to Egeria with pyegeria and list the glossaries. No Textual yet.

Run: python examples/developer_guide/step1_hello_pyegeria.py
"""

from pyegeria import PyegeriaException, print_basic_exception

from common import connection_settings, fetch_glossaries


def main() -> None:
conn = connection_settings()
print(f"Connecting to {conn.view_server} at {conn.platform_url} as {conn.user_name}")
try:
glossaries = fetch_glossaries(conn)
except PyegeriaException as e:
print_basic_exception(e)
return

if not glossaries:
print("No glossaries found")
return
for glossary in glossaries:
print(f"{glossary['Display Name']:<55} {glossary['GUID']}")


if __name__ == "__main__":
main()
79 changes: 79 additions & 0 deletions examples/developer_guide/step2_glossary_browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
PDX-License-Identifier: Apache-2.0
Copyright Contributors to the ODPi Egeria project.

Developer Guide step 2: a Textual app that shows Egeria glossaries in a DataTable.
The Egeria call runs in a thread worker so the UI stays responsive while it loads.

Run: python examples/developer_guide/step2_glossary_browser.py
"""

from pyegeria import PyegeriaException
from textual import work
from textual.app import App, ComposeResult
from textual.widgets import DataTable, Footer, Header, Static

from common import connection_settings, fetch_glossaries


class GlossaryBrowserApp(App):
"""List the glossaries known to Egeria."""

TITLE = "Egeria"
SUB_TITLE = "Glossary Browser"

CSS = """
#heading { padding: 1 2; text-style: bold; }
DataTable { height: 1fr; }
"""

BINDINGS = [
("q", "quit", "Quit"),
("r", "refresh", "Refresh"),
]

def __init__(self) -> None:
super().__init__()
self.conn = connection_settings()

def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield Static(f"Glossaries visible to {self.conn.user_name}", id="heading")
yield DataTable(id="glossary_table", cursor_type="row", zebra_stripes=True)
yield Footer()

def on_mount(self) -> None:
table = self.query_one("#glossary_table", DataTable)
table.add_columns("Display Name", "Qualified Name", "GUID")
self.action_refresh()

def action_refresh(self) -> None:
"""Bound to "r": clear the table and reload it from Egeria."""
table = self.query_one("#glossary_table", DataTable)
table.clear()
table.loading = True
self.load_glossaries()

@work(thread=True, exclusive=True)
def load_glossaries(self) -> None:
"""Runs in a background thread. Widgets must only be updated on the UI thread,
so the results are handed back with call_from_thread."""
try:
glossaries = fetch_glossaries(self.conn)
except PyegeriaException as e:
self.call_from_thread(self.notify, f"Egeria request failed: {e}", severity="error")
glossaries = []
self.call_from_thread(self.show_glossaries, glossaries)

def show_glossaries(self, glossaries: list[dict]) -> None:
table = self.query_one("#glossary_table", DataTable)
for glossary in glossaries:
# The GUID doubles as the row key, so it can be looked up directly later
table.add_row(glossary["Display Name"], glossary["Qualified Name"], glossary["GUID"],
key=glossary["GUID"])
table.loading = False
self.notify(f"Loaded {len(glossaries)} glossaries")


if __name__ == "__main__":
GlossaryBrowserApp().run()
Loading
Loading