From 4b35912d96d52f453c878d5e5dc3174640b8ec35 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:53:07 -0500 Subject: [PATCH 01/19] feat(my_egeria): add experimental my_network script Co-authored-by: Junie Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../DemoCode/Experimental Code/my_network.py | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py new file mode 100644 index 00000000..f775b700 --- /dev/null +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py @@ -0,0 +1,177 @@ +""" + PDX-License-Identifier: Apache-2.0 + Copyright Contributors to the ODPi Egeria project. + + This file provides the main entry point and core lifecycle for the My Profile Textual App. +""" + +import sys +from pathlib import Path +from typing import Any +import asyncio + +from textual import on +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import ScrollableContainer +from textual.widgets import Header, Footer, Static, Placeholder, Input, DataTable + +# Add the project root to sys.path to allow running this script from any directory +root_path = Path(__file__).resolve().parents[4] +if str(root_path) not in sys.path: + sys.path.append(str(root_path)) + +current_dir = Path(__file__).resolve().parent +if str(current_dir) not in sys.path: + sys.path.append(str(current_dir)) + +from pyegeria import ( + load_app_config, + settings, + MyProfile, + PyegeriaException, + print_basic_exception, + exec_report_spec, Egeria, +) + +class MyNetworkApp(App): + """My Network App. + + Finds user ids based on a search parameter to allow the current user to add the user(s) + found to their private "peer network" in their profile in Egeria. + """ + + BINDINGS = [ + ("q", "quit", "Quit"), + ("r", "refresh", "Refresh Data"), + Binding("ctrl+f", "feedback", "Feedback", priority=True), + Binding("f3", "view_feedback_log", "Feedback Log", priority=True), + ] + + # CSS_PATH = "my_profile.tcss" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.title = "My_Network" + self.sub_title = "Egeria network of contacts for the current user" + self.description = "Display the user's network of (Egeria) contacts." + load_app_config() + app_config = settings.Environment + self.log(f"Application Config: {app_config}") + app_user = settings.User_Profile + self.log(f"User Profile: {app_user}") + self.user_name = app_user.user_name or "garygeeke" + self.user_password = app_user.user_pwd or "secret" + self.view_server = app_config.egeria_view_server or "qs-view-server" + self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" + self.log(f"Platform URL: {self.platform_url}") + self.log(f"View Server: {self.view_server}") + self.log(f"User: {self.user_name}") + self.log(f"User PWD: {self.user_password}") + self.users_found: DataTable = DataTable(id="users_found") + self.my_network: DataTable = DataTable(id="my_network") + + def on_mount(self) -> None: + """Perform actions when the application is mounted.""" + self.my_network.add_column("Nickname", key="nickname") + self.my_network.add_column("User ID", key="user_id") + self.my_network.add_column("GUID", key="guid") + self.my_network.add_column("Qualified Name", key="qualified_name") + self.my_network.add_column("Notes", key="notes") + self.my_network.cursor_type="row" + self.my_network.zebra_stripes=True + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + yield Static(self.description) + yield ScrollableContainer( + Placeholder(label="Placeholder for contacts list", id="users_network_placeholder"), + id="users_network_container") + yield ScrollableContainer( + Static("Search for user, enter the (partial) name to search for and press return:"), + Input(placeholder="Name", id="user_search_input"), + id="user_input_container") + yield Footer() + + @on(Input.Submitted, "#user_search_input") + def handle_input_submitted(self, event: Input.Submitted) -> None: + """Handle the submission of the user search input.""" + self.log(f"User search input submitted: {event.value}") + user_found = self.search_users(str(event.value)) + self.users_found.clear(columns=True) + self.users_found.add_columns("Id", "GUID", "Qualified Name") + self.users_found.cursor_type = "row" + self.users_found.zebra_stripes=True + self.notify(f"Found {len(user_found)} users with name: {event.value}", severity="information", timeout=20) + if isinstance(user_found, list): + for user in user_found: + self.users_found.add_row(user["User ID"], user["GUID"], user["Qualified Name"]) + self.query_one("#users_network_container", ScrollableContainer).mount(self.users_found, + Static("Please select a row to add to your network")) + elif isinstance(user_found, dict): + self.users_found.add_row(user_found["User ID"], user_found["GUID"], user_found["Qualified Name"]) + self.query_one("#users_network_container", ScrollableContainer).mount(self.users_found, + Static( + "Please select a row to add to your network")) + else: + self.log(f"Unexpected response type: {type(user_found)}") + self.notify(f"Unexpected response type: {type(user_found)}") + self.users_found.refresh() + self.query_one("#user_input_container", ScrollableContainer).remove_children() + + def search_users(self, user_name: str) -> Any: + self.log(f"Searching for users with name: {user_name}") + try: + eclient=Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) + token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) + user_found=eclient.find_user_identities( + search_string=user_name, + output_format="DICT", + report_spec="User-Identities", + query_depth=0, + page_size=10, + ) + self.log(f"Found {user_found} users with name: {user_name}") + except PyegeriaException as e: + self.log(f"Error searching for users: {e}") + user_found="Error" + self.notify(f"Error searching for users: {e}", severity="error", timeout=20) + finally: + if eclient: + eclient.close_session() + return user_found + + @on(DataTable.RowSelected, "#users_found") + def on_users_found_row_selected(self, event: DataTable.RowSelected) -> None: + self.log(f"User selected: {event.row_key}") + self.row_selected = event.row_key + row_data = self.users_found.get_row(self.row_selected) + self.log(f"Selected row data: {row_data}") + self.selected_user_id = row_data[0] + self.selected_GUID = row_data[1] + self.selected_qualified_name = row_data[2] + self.log(f"Selected user ID: {self.selected_user_id}") + self.log(f"Selected GUID: {self.selected_GUID}") + self.log(f"Selected qualified name: {self.selected_qualified_name}") + self.query_one("#user_input_container", ScrollableContainer).remove_children() + self.query_one("#users_input_container", ScrollableContainer).mount( + Static("Selected ID for new network contact is:"), + Static(f"{self.selected_user_id, " ", self.selected_qualified_name}"), + Static("Please provide a NickName to Store this contact under:"), + Input(placeholder="Nickname", id="nickname_input"), + ) + + @on(Input.Submitted, "#nickname_input") + def handle_nickname_input(self, event: Input.Submitted): + """Handle the submission of the user nickname input.""" + self.log(f"User nickname input submitted: {event.value}") + self.nickname = event.value + self.my_network.add_row(self.nickname, self.selected_user_id, self.selected_GUID, self.selected_qualified_name) + +def main() -> None: + """Entry point for the my_profile console script.""" + MyNetworkApp().run() + + +if __name__ == "__main__": + main() \ No newline at end of file From 3b62625283647ba8f82e05330ad0b78a41c02592 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:24:08 -0500 Subject: [PATCH 02/19] wip(my_egeria): my_network loads profile and existing peers, links new peers - Load the user's profile and existing Peer relationships on mount - Search actor profiles (case-insensitive) instead of user identities - Add description input and a Link Peer button to create the relationship Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../DemoCode/Experimental Code/my_network.py | 126 ++++++++++++++++-- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py index f775b700..5de0c0ac 100644 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py @@ -6,6 +6,7 @@ """ import sys +from datetime import datetime, timezone from pathlib import Path from typing import Any import asyncio @@ -14,7 +15,7 @@ from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import ScrollableContainer -from textual.widgets import Header, Footer, Static, Placeholder, Input, DataTable +from textual.widgets import Header, Footer, Static, Placeholder, Input, DataTable, Button # Add the project root to sys.path to allow running this script from any directory root_path = Path(__file__).resolve().parents[4] @@ -60,7 +61,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.log(f"Application Config: {app_config}") app_user = settings.User_Profile self.log(f"User Profile: {app_user}") - self.user_name = app_user.user_name or "garygeeke" + self.user_name = app_user.user_name or "tessatube" self.user_password = app_user.user_pwd or "secret" self.view_server = app_config.egeria_view_server or "qs-view-server" self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" @@ -70,6 +71,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.log(f"User PWD: {self.user_password}") self.users_found: DataTable = DataTable(id="users_found") self.my_network: DataTable = DataTable(id="my_network") + self.nickname = "" + self.description = "" def on_mount(self) -> None: """Perform actions when the application is mounted.""" @@ -80,6 +83,59 @@ def on_mount(self) -> None: self.my_network.add_column("Notes", key="notes") self.my_network.cursor_type="row" self.my_network.zebra_stripes=True + try: + eclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) + token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) + self.my_profile=eclient.get_my_profile(output_format="DICT") + self.log(f"User profile retrieved successfully, {self.my_profile}") + except PyegeriaException as e: + self.log(f"Error: {e}") + self.notify(f"Error, unable to retrieve user profile: {e}", severity="error", timeout=20) + self.exit(400) + finally: + if eclient: + eclient.close_session() + if self.my_profile: + if isinstance(self.my_profile, list): + self.user_GUID = self.my_profile[0].get("GUID") or None + else: + self.notify(f"User profile is not a list: {self.my_profile}", severity="error", timeout=20) + self.exit(400) + else: + self.notify(f"User profile not found: {self.user_name}", severity="error", timeout=20) + self.exit(400) + + # Get existing peer relationship list + + try: + eclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) + token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) + peers = eclient.get_relationships( + relationship_type="Peer", + # source_guid=self.user_GUID, + output_format="JSON", + page_size=30) + self.log(f"Peers: {peers}") + except PyegeriaException as e: + self.log(f"Error creating peer relationship: {e}") + self.notify(f"Error creating peer relationship: {e}", timeout=15, severity="error") + self.exit(400) + finally: + if eclient: + eclient.close_session() + for peer in peers: + self.log(f"Peer: {peer}") + self.my_network.add_row(peer.get("Label"), + peer.get("Display Name"), + peer.get("GUID"), + peer.get("Qualified Name"), + peer.get("Description")) + continue + self.my_network.refresh() + container=self.query_one("#users_network_container", ScrollableContainer) + container.remove_children() + container.mount(self.my_network) + return def compose(self) -> ComposeResult: yield Header(show_clock=True) @@ -90,7 +146,7 @@ def compose(self) -> ComposeResult: yield ScrollableContainer( Static("Search for user, enter the (partial) name to search for and press return:"), Input(placeholder="Name", id="user_search_input"), - id="user_input_container") + id="users_input_container") yield Footer() @on(Input.Submitted, "#user_search_input") @@ -103,9 +159,16 @@ def handle_input_submitted(self, event: Input.Submitted) -> None: self.users_found.cursor_type = "row" self.users_found.zebra_stripes=True self.notify(f"Found {len(user_found)} users with name: {event.value}", severity="information", timeout=20) + if len(user_found) == 0: + self.notify(f"No users found with name: {event.value}", severity="warning", timeout=20) + return + if isinstance(user_found, str): + self.notify(f"Error searching for users: {user_found}", severity="error", timeout=20) + return if isinstance(user_found, list): for user in user_found: - self.users_found.add_row(user["User ID"], user["GUID"], user["Qualified Name"]) + self.users_found.add_row(user["Display Name"], user["GUID"], user["Qualified Name"]) + self.query_one("#users_network_container", ScrollableContainer).remove_children() self.query_one("#users_network_container", ScrollableContainer).mount(self.users_found, Static("Please select a row to add to your network")) elif isinstance(user_found, dict): @@ -117,15 +180,16 @@ def handle_input_submitted(self, event: Input.Submitted) -> None: self.log(f"Unexpected response type: {type(user_found)}") self.notify(f"Unexpected response type: {type(user_found)}") self.users_found.refresh() - self.query_one("#user_input_container", ScrollableContainer).remove_children() + self.query_one("#users_input_container", ScrollableContainer).remove_children() def search_users(self, user_name: str) -> Any: self.log(f"Searching for users with name: {user_name}") try: eclient=Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) - user_found=eclient.find_user_identities( + user_found=eclient.find_actor_profiles( search_string=user_name, + ignore_case=True, output_format="DICT", report_spec="User-Identities", query_depth=0, @@ -147,26 +211,70 @@ def on_users_found_row_selected(self, event: DataTable.RowSelected) -> None: self.row_selected = event.row_key row_data = self.users_found.get_row(self.row_selected) self.log(f"Selected row data: {row_data}") + self.query_one("#users_input_container", ScrollableContainer).remove_children() self.selected_user_id = row_data[0] self.selected_GUID = row_data[1] self.selected_qualified_name = row_data[2] self.log(f"Selected user ID: {self.selected_user_id}") self.log(f"Selected GUID: {self.selected_GUID}") self.log(f"Selected qualified name: {self.selected_qualified_name}") - self.query_one("#user_input_container", ScrollableContainer).remove_children() self.query_one("#users_input_container", ScrollableContainer).mount( Static("Selected ID for new network contact is:"), Static(f"{self.selected_user_id, " ", self.selected_qualified_name}"), Static("Please provide a NickName to Store this contact under:"), Input(placeholder="Nickname", id="nickname_input"), + Static("Optionally provide a description/comment about this relationship"), + Input(placeholder="Description", id="description_input"), + Button("Link Peer", id="link_peer_button", variant="primary") ) - @on(Input.Submitted, "#nickname_input") - def handle_nickname_input(self, event: Input.Submitted): + @on(Input.Changed, "#nickname_input") + def handle_nickname_input(self, event: Input.Changed): """Handle the submission of the user nickname input.""" self.log(f"User nickname input submitted: {event.value}") self.nickname = event.value + + @on(Input.Changed, "#description_input") + def handle_description_input(self, event: Input.Changed): + """Handle the submission of the user description input.""" + self.log(f"User description input submitted: {event.value}") + self.description = event.value + + @on(Button.Pressed, "#link_peer_button") + def handle_link_peer_button(self, event: Button.Pressed): + """Handle the submission of the link peer button. + Since there is no validity check and both NickName and Description are optional + we link the peer person without further upfront validation """ self.my_network.add_row(self.nickname, self.selected_user_id, self.selected_GUID, self.selected_qualified_name) + #add link to peer + self.log(f"Owner GUID: {self.user_GUID} Peer selected GUID: {self.selected_GUID}") + now = datetime.now(timezone.utc).isoformat(timespec="milliseconds") + body = { + "class": "NewRelationshipRequestBody", + "properties": { + "class": "PeerProperties", + "peer_relationship_name": self.nickname, + "description": self.description, + "effective_from" : None, + "effective_to": None + } + } + self.log(f"Linking peer person: body {body} Owner GUID: {self.user_GUID} and Peer selected GUID: {self.selected_GUID}") + try: + eclient = Egeria(self.view_server, + self.platform_url, + self.user_name, + self.user_password) + token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) + eclient.link_peer_person(self.user_GUID, + self.selected_GUID, + body + ) + except PyegeriaException as e: + self.log(f"Error linking peer person: {e}") + self.notify(f"Error linking peer person: {e}") + finally: + return def main() -> None: """Entry point for the my_profile console script.""" From 357c96063cdc4d333a5efd5ae4db1535da67ab8d Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:32:03 -0500 Subject: [PATCH 03/19] tidying up the project files, moved a number of folders to deprecated, added custom report format for listing peers Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- config/config.json | 5 +- .../DemoCode/Experimental Code/my_network.py | 117 +++++++++++++++++- .../DemoCode/Journals_Demo/__init__.py | 7 -- my_egeria/my_egeria/README.md | 10 +- .../con_services/egeria_connection.py | 4 +- my_egeria/my_egeria/error_popup_app.py | 2 +- my_egeria/my_egeria/my_egeria_app.py | 42 +++---- .../add_governance_definition.py | 4 +- .../delete_governance_definition.py | 4 +- .../governance_officer_browser.py | 4 +- .../GovernanceOfficer/marketplace_tree.py | 1 - .../ProductManager/product_manager_browser.py | 4 +- .../ProjectManager/ProjectManagerBrowser.py | 2 +- my_egeria/my_egeria/screens/base_screen.py | 4 +- .../screens/collections/add_collection.py | 4 +- .../screens/collections/collection_browser.py | 2 +- .../screens/collections/collection_details.py | 2 +- .../collections/collection_members_screen.py | 2 +- .../screens/collections/delete_collection.py | 2 +- .../screens/glossary/glossary_browser.py | 6 +- .../screens/glossary/glossary_browser_old.py | 4 +- .../screens/glossary/glossary_list_screen.py | 6 +- .../screens/glossary/term_details.py | 3 +- .../screens/glossary/term_list_screen.py | 3 +- .../my_egeria/screens/governance_screen.py | 4 +- my_egeria/my_egeria/screens/login_screen.py | 2 +- my_egeria/my_egeria/screens/main_menu.py | 4 +- .../screens/platform/platform_services.py | 4 +- .../screens/platform/platforms_screen.py | 4 +- my_egeria/my_egeria/screens/project_screen.py | 4 +- .../my_egeria/screens/subject_area_screen.py | 4 +- my_egeria/my_egeria/services/base_service.py | 4 +- .../my_egeria/services/collection_service.py | 2 +- .../my_egeria/services/glossary_service.py | 2 +- .../services/governance_officer_service.py | 4 +- .../services/product_manager_service.py | 5 +- .../services/project_manager_service.py | 3 +- .../services/subject_area_service.py | 2 +- .../my_egeria/tests/test_login_screen.py | 4 - .../my_egeria/tests/test_startup_check.py | 2 +- .../tests/test_term_details_screen.py | 1 - my_report_specs/my_profile_report_specs.json | 47 +++++++ 42 files changed, 240 insertions(+), 106 deletions(-) delete mode 100644 my_egeria/my_egeria/DemoCode/Journals_Demo/__init__.py create mode 100644 my_report_specs/my_profile_report_specs.json diff --git a/config/config.json b/config/config.json index dac20f26..6d3e25ad 100644 --- a/config/config.json +++ b/config/config.json @@ -51,5 +51,8 @@ "Egeria Home Glossary Name": "Egeria-Markdown", "Egeria Local Qualifier": "myLocal", "Egeria Usage Level": "Basic" - } + }, + "Pyegeria Report Spec Modules": [ + "~/my_report_specs/my_profile_report_specs.json" + ] } \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py index 5de0c0ac..8a20343d 100644 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py @@ -71,8 +71,11 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.log(f"User PWD: {self.user_password}") self.users_found: DataTable = DataTable(id="users_found") self.my_network: DataTable = DataTable(id="my_network") + self.my_user_contact_details: DataTable = DataTable(id="my_user_contact_details") + self.my_user_communities: DataTable = DataTable(id="my_user_communities") self.nickname = "" self.description = "" + self.selected_network_member = None def on_mount(self) -> None: """Perform actions when the application is mounted.""" @@ -112,7 +115,6 @@ def on_mount(self) -> None: token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) peers = eclient.get_relationships( relationship_type="Peer", - # source_guid=self.user_GUID, output_format="JSON", page_size=30) self.log(f"Peers: {peers}") @@ -126,15 +128,18 @@ def on_mount(self) -> None: for peer in peers: self.log(f"Peer: {peer}") self.my_network.add_row(peer.get("Label"), - peer.get("Display Name"), - peer.get("GUID"), + peer.get("uniqueName"), + peer.get("guid"), peer.get("Qualified Name"), - peer.get("Description")) + peer.get("relationshipProperties", "description")) continue self.my_network.refresh() container=self.query_one("#users_network_container", ScrollableContainer) - container.remove_children() + container.remove_children("#users_network_placeholder") container.mount(self.my_network) + container.mount(Button(label="User Details", id="user_details1", variant="primary")) + container.refresh() + self.my_network.refresh() return def compose(self) -> ComposeResult: @@ -270,11 +275,113 @@ def handle_link_peer_button(self, event: Button.Pressed): self.selected_GUID, body ) + self.notify(f"Peer person linked successfully") except PyegeriaException as e: self.log(f"Error linking peer person: {e}") self.notify(f"Error linking peer person: {e}") finally: + # clear input fields + self.query_one("#nickname_input", Input).clear() + self.query_one("#description_input", Input).clear() + # reset display to allow another search + self.query_one("#users_input_container", ScrollableContainer).remove_children() + self.query_one("#users_network_container", ScrollableContainer).remove_children() + self.query_one("#users_input_container").mount( + Static("Search for user, enter the (partial) name to search for and press return:"), + Input(placeholder="Name", id="user_search_input") + ) + self.query_one("#users_network_container").mount( + Static("Peer person network"), + self.my_network, + Button(label="User Details", id="user_details2", variant="primary") + ) + return + + @on(DataTable.RowHighlighted, "#user_network_table") + def handle_network_member_selected(self,event: DataTable.RowHighlighted): + self.log(f"Row highlighted: {event.row_key}") + self.selected_network_member = event.row_key + + @on(DataTable.RowSelected, "#user_network_table") + def handle_network_member_selected(self, event: DataTable.RowSelected): + self.log(f"Row selected: {event.row_key}") + self.selected_network_member = event.row_key + + @on(Button.Pressed, "#user_details1") + def handle_user_details_1_button(self, event: Button.Pressed): + if self.selected_network_member: + selected_row_data = self.my_network.get_row(self.selected_network_member) + self.show_user_details(selected_row_data) + return + else: + self.notify(f"Please select a network member to view details before using the Button.", severity="warning", timeout=10) + return + + @on(Button.Pressed, "#user_details2") + def handle_user_details_2_button(self, event: Button.Pressed): + if self.selected_network_member: + selected_row_data = self.my_network.get_row(self.selected_network_member) + self.show_user_details(selected_row_data) return + else: + self.notify(f"Please select a network member to view details before using the Button.", severity="warning", + timeout=10) + return + + def show_user_details(self, selected_row_data): + """ Show both contact details and community membership details for the selected network member.""" + self.selected_network_member = selected_row_data + self.user_GUID = selected_row_data[2] + self.user_qname = selected_row_data[3] + self.user_nickname = selected_row_data[0] + #clear and set up the datatables + self.my_user_contact_details.clear(columns=True) + self.my_user_contact_details.add_columns("Name", "Method Type", "Contact Type", "Service", "Value", "GUID") + self.my_user_contact_details.cursor_type = "row" + self.my_user_contact_details.zebra_stripes=True + self.my_user_communities.clear(columns=True) + self.my_user_communities.add_columns("Name", "Assignment Type", "Description", "GUID") + self.my_user_communities.cursor_type = "row" + self.my_user_communities.zebra_stripes=True + # retrieve the users contact information + try: + eclient=Egeria( + self.view_server, + self.platform_url, + self.user_name, + self.user_password) + token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) + self.user_contact_details = exec_report_spec( + format_set_name="My-User-Contact-Detail") + self.log(f"Retrieved user contact details: {self.user_contact_details}") + except Exception as e: + self.notify(f"Error retrieving user contact details: {str(e)}", severity="error", timeout=10) + return + try: + self.user_communities = exec_report_spec( + format_set_name="My-User-Communities-Detail") + self.log(f"Retrieved user community memberships: {self.user_communities}") + except Exception as e: + self.notify(f"Error retrieving user community memberships: {str(e)}", severity="error", timeout=10) + return + finally: + if eclient: + eclient.close_session() + if isinstance(self.user_contact_details, list): + for contact in self.user_contact_details: + self.my_user_contact_details.add_row(contact.get("Name"), + contact.get("Method Type"), + contact.get("Contact Type"), + contact.get("Service"), + contact.get("Value"), + contact.get("GUID")) + if isinstance(self.user_communities, list): + for community in self.user_communities: + self.my_user_communities.add_row(community.get("Name"), + community.get("Assignment Type"), + community.get("Desacription"), + community.get("GUID")) + return def main() -> None: """Entry point for the my_profile console script.""" diff --git a/my_egeria/my_egeria/DemoCode/Journals_Demo/__init__.py b/my_egeria/my_egeria/DemoCode/Journals_Demo/__init__.py deleted file mode 100644 index d7c7a021..00000000 --- a/my_egeria/my_egeria/DemoCode/Journals_Demo/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" \ No newline at end of file diff --git a/my_egeria/my_egeria/README.md b/my_egeria/my_egeria/README.md index 1288dffa..fcbdbbe0 100644 --- a/my_egeria/my_egeria/README.md +++ b/my_egeria/my_egeria/README.md @@ -10,11 +10,11 @@ The `my-egeria` Textual TUI application's package root — see the parent |---|---| | `main.py` | Entry point (`uv run my-egeria` / `python -m my_egeria.main`). | | `my_egeria_app.py` | The main Textual `App` class. | -| `screens/` | Textual screens, one per feature area (`GovernanceOfficer`, `ProductManager`, `ProjectManager`, `collections`, `glossary`, `platform`). | -| `widgets/` | Reusable Textual widgets shared across screens. | -| `services/`, `con_services/` | Backend service layer wrapping pyegeria SDK calls for the screens/widgets to use. | -| `utils/` | Shared helpers. | -| `styles/` | Textual CSS. | +| `DemoCode/Deprecated/screens` | Textual screens, one per feature area (`GovernanceOfficer`, `ProductManager`, `ProjectManager`, `collections`, `glossary`, `platform`). | +| `DemoCode/Deprecated/widgets` | Reusable Textual widgets shared across screens. | +| `DemoCode/Deprecated/services`, `con_services/` | Backend service layer wrapping pyegeria SDK calls for the screens/widgets to use. | +| `DemoCode/Deprecated/utils` | Shared helpers. | +| `DemoCode/Deprecated/styles` | Textual CSS. | | `tests/` | Tests for this app. | | `error_popup_app.py`, `startup_check.py` | Startup/error-handling helpers. | | `serve.py` | Browser-mode serving via the `textual-serve` library — see the `serve_my_egeria`/`serve_my_profile` entry points in the root `pyproject.toml`. | diff --git a/my_egeria/my_egeria/con_services/egeria_connection.py b/my_egeria/my_egeria/con_services/egeria_connection.py index 3f577c6a..dd77cc04 100644 --- a/my_egeria/my_egeria/con_services/egeria_connection.py +++ b/my_egeria/my_egeria/con_services/egeria_connection.py @@ -10,8 +10,8 @@ from __future__ import annotations from typing import Optional -from ..utils.config import get_global_config -from ..utils.egeria_client import preflight_origin +from my_egeria.DemoCode.Deprecated.utils import get_global_config +from my_egeria.DemoCode.Deprecated.utils import preflight_origin class EgeriaConnectionService: """Checks if Egeria is reachable and authenticates.""" diff --git a/my_egeria/my_egeria/error_popup_app.py b/my_egeria/my_egeria/error_popup_app.py index 2ef978a6..0f036111 100644 --- a/my_egeria/my_egeria/error_popup_app.py +++ b/my_egeria/my_egeria/error_popup_app.py @@ -6,7 +6,7 @@ """ from textual.app import App -from my_egeria.screens.error_popup_screen import ErrorPopup +from my_egeria.DemoCode.Deprecated.screens import ErrorPopup class ErrorPopupApp (App): diff --git a/my_egeria/my_egeria/my_egeria_app.py b/my_egeria/my_egeria/my_egeria_app.py index 075854ae..386d29fc 100644 --- a/my_egeria/my_egeria/my_egeria_app.py +++ b/my_egeria/my_egeria/my_egeria_app.py @@ -8,12 +8,9 @@ """ import os -from typing import Any from textual import on -from . import utils - # Set safe defaults BEFORE importing anything that might import pyegeria os.environ.setdefault("EGERIA_USER", "erinoverview") os.environ.setdefault("EGERIA_USER_PASSWORD", "secret") @@ -22,26 +19,25 @@ from textual.app import App, ComposeResult from textual.widgets import Footer -from my_egeria.screens.login_screen import LoginScreen -from my_egeria.screens.main_menu import MainMenuScreen -from my_egeria.screens.glossary.glossary_browser import GlossaryBrowserScreen -from my_egeria.screens.collections.collection_browser import CollectionBrowserScreen -from my_egeria.screens.collections.collection_members_screen import CollectionMemberScreen -from my_egeria.screens.collections.collection_details import CollectionDetailsScreen -from my_egeria.screens.collections.add_collection import AddCollectionScreen -from my_egeria.screens.collections.delete_collection import DeleteCollectionScreen -from my_egeria.screens.glossary.glossary_list_screen import GlossaryListScreen -from my_egeria.screens.glossary.term_details import TermDetailsScreen -from my_egeria.screens.glossary.term_list_screen import TermListScreen -from my_egeria.screens.GovernanceOfficer.governance_officer_browser import GovernanceOfficerBrowserScreen -from my_egeria.screens.GovernanceOfficer.add_governance_definition import AddGovernanceDefinitionScreen -from my_egeria.screens.GovernanceOfficer.delete_governance_definition import DeleteGovernanceDefinitionScreen -from my_egeria.screens.GovernanceOfficer.marketplace_tree import MarketPlaceTree -from my_egeria.screens.ProductManager.product_manager_browser import ProductManagerBrowser -from my_egeria.utils.egeria_client import close_all_managers -from my_egeria.utils.config import EgeriaConfig -from my_egeria.screens.splash_screen import SplashScreen # your existing splash screen -from my_egeria.services.term_service import get_terms_for_glossary +from my_egeria.DemoCode.Deprecated.screens import LoginScreen +from my_egeria.DemoCode.Deprecated.screens.main_menu import MainMenuScreen +from my_egeria.DemoCode.Deprecated.screens.glossary import GlossaryBrowserScreen +from my_egeria.DemoCode.Deprecated.screens.collections.collection_browser import CollectionBrowserScreen +from my_egeria.DemoCode.Deprecated.screens.collections.collection_members_screen import CollectionMemberScreen +from my_egeria.DemoCode.Deprecated.screens.collections.collection_details import CollectionDetailsScreen +from my_egeria.DemoCode.Deprecated.screens.collections import AddCollectionScreen +from my_egeria.DemoCode.Deprecated.screens.collections.delete_collection import DeleteCollectionScreen +from my_egeria.DemoCode.Deprecated.screens.glossary.glossary_list_screen import GlossaryListScreen +from my_egeria.DemoCode.Deprecated.screens.glossary.term_details import TermDetailsScreen +from my_egeria.DemoCode.Deprecated.screens.glossary.term_list_screen import TermListScreen +from my_egeria.DemoCode.Deprecated.screens.GovernanceOfficer.governance_officer_browser import GovernanceOfficerBrowserScreen +from my_egeria.DemoCode.Deprecated.screens.GovernanceOfficer import AddGovernanceDefinitionScreen +from my_egeria.DemoCode.Deprecated.screens.GovernanceOfficer import DeleteGovernanceDefinitionScreen +from my_egeria.DemoCode.Deprecated.screens.GovernanceOfficer.marketplace_tree import MarketPlaceTree +from my_egeria.DemoCode.Deprecated.screens.ProductManager.product_manager_browser import ProductManagerBrowser +from my_egeria.DemoCode.Deprecated.utils.egeria_client import close_all_managers +from my_egeria.DemoCode.Deprecated.screens.splash_screen import SplashScreen # your existing splash screen + class MyEgeria(App): """Main app class of my_egeria.""" diff --git a/my_egeria/my_egeria/screens/GovernanceOfficer/add_governance_definition.py b/my_egeria/my_egeria/screens/GovernanceOfficer/add_governance_definition.py index 101099a9..4d3bafaf 100644 --- a/my_egeria/my_egeria/screens/GovernanceOfficer/add_governance_definition.py +++ b/my_egeria/my_egeria/screens/GovernanceOfficer/add_governance_definition.py @@ -17,9 +17,9 @@ from textual.widgets import Static, Input, Button, Pretty, OptionList, Footer, Header # from .governance_officer_browser import GovernanceOfficerBrowserScreen from ..base_screen import BaseScreen -from my_egeria.services.governance_officer_service import GovernanceOfficerService +from my_egeria.DemoCode.Deprecated.services.governance_officer_service import GovernanceOfficerService from typing import Dict, Any -from my_egeria.utils.config import EgeriaConfig + def parse_kv_pairs(text: str) -> Dict[str, Any]: """ diff --git a/my_egeria/my_egeria/screens/GovernanceOfficer/delete_governance_definition.py b/my_egeria/my_egeria/screens/GovernanceOfficer/delete_governance_definition.py index c24f5e98..ae3a34c1 100644 --- a/my_egeria/my_egeria/screens/GovernanceOfficer/delete_governance_definition.py +++ b/my_egeria/my_egeria/screens/GovernanceOfficer/delete_governance_definition.py @@ -9,8 +9,8 @@ """ from textual.messages import Message from typing import Dict, Any -from my_egeria.screens.base_screen import BaseScreen -from my_egeria.services.governance_officer_service import GovernanceOfficerService +from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen +from my_egeria.DemoCode.Deprecated.services.governance_officer_service import GovernanceOfficerService from textual.containers import Container, Vertical, Horizontal from textual.widgets import Static, Button import asyncio diff --git a/my_egeria/my_egeria/screens/GovernanceOfficer/governance_officer_browser.py b/my_egeria/my_egeria/screens/GovernanceOfficer/governance_officer_browser.py index e1ecfef9..086ce45e 100644 --- a/my_egeria/my_egeria/screens/GovernanceOfficer/governance_officer_browser.py +++ b/my_egeria/my_egeria/screens/GovernanceOfficer/governance_officer_browser.py @@ -13,12 +13,12 @@ from textual.containers import Container, Vertical, Horizontal from ..base_screen import BaseScreen from .marketplace_tree import MarketPlaceTree -from my_egeria.services.governance_officer_service import GovernanceOfficerService +from my_egeria.DemoCode.Deprecated.services.governance_officer_service import GovernanceOfficerService from .add_governance_definition import AddGovernanceDefinitionScreen from .delete_governance_definition import DeleteGovernanceDefinitionScreen import asyncio from textual import on -from my_egeria.utils.config import EgeriaConfig, get_global_config +from my_egeria.DemoCode.Deprecated.utils.config import get_global_config # ... existing imports ... diff --git a/my_egeria/my_egeria/screens/GovernanceOfficer/marketplace_tree.py b/my_egeria/my_egeria/screens/GovernanceOfficer/marketplace_tree.py index 29867d67..6d0aca88 100644 --- a/my_egeria/my_egeria/screens/GovernanceOfficer/marketplace_tree.py +++ b/my_egeria/my_egeria/screens/GovernanceOfficer/marketplace_tree.py @@ -9,7 +9,6 @@ """ from ..base_screen import BaseScreen from textual.widgets import Button, Tree, Static, Input -from my_egeria.utils.config import EgeriaConfig, get_global_config from textual import on from textual.containers import Container, Horizontal diff --git a/my_egeria/my_egeria/screens/ProductManager/product_manager_browser.py b/my_egeria/my_egeria/screens/ProductManager/product_manager_browser.py index 5ee717a1..60919390 100644 --- a/my_egeria/my_egeria/screens/ProductManager/product_manager_browser.py +++ b/my_egeria/my_egeria/screens/ProductManager/product_manager_browser.py @@ -7,8 +7,8 @@ """ # from textual.geometry import Coordinate # remove unused/unsupported import -from my_egeria.screens.base_screen import BaseScreen -from my_egeria.services.product_manager_service import ProductManagerService +from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen +from my_egeria.DemoCode.Deprecated.services.product_manager_service import ProductManagerService # from .collection_details import CollectionDetailsScreen # from .add_collection import AddCollectionScreen # from .delete_collection import DeleteCollectionScreen diff --git a/my_egeria/my_egeria/screens/ProjectManager/ProjectManagerBrowser.py b/my_egeria/my_egeria/screens/ProjectManager/ProjectManagerBrowser.py index 38a5d294..0cfd6055 100644 --- a/my_egeria/my_egeria/screens/ProjectManager/ProjectManagerBrowser.py +++ b/my_egeria/my_egeria/screens/ProjectManager/ProjectManagerBrowser.py @@ -7,7 +7,7 @@ """ # from textual.geometry import Coordinate # remove unused/unsupported import from ..base_screen import BaseScreen -from my_egeria.services.project_manager_service import ProjectManagerService +from my_egeria.DemoCode.Deprecated.services.project_manager_service import ProjectManagerService # from .collection_details import CollectionDetailsScreen # from .add_collection import AddCollectionScreen # from .delete_collection import DeleteCollectionScreen diff --git a/my_egeria/my_egeria/screens/base_screen.py b/my_egeria/my_egeria/screens/base_screen.py index f664fc7f..aac7be26 100644 --- a/my_egeria/my_egeria/screens/base_screen.py +++ b/my_egeria/my_egeria/screens/base_screen.py @@ -13,8 +13,8 @@ from textual.app import ComposeResult from textual import on from textual.containers import Container -from my_egeria.utils.config import get_global_config -from my_egeria.utils.egeria_client import EgeriaTechClientManager +from my_egeria.DemoCode.Deprecated.utils.config import get_global_config +from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager from my_egeria.con_services.egeria_connection import EgeriaConnectionService class BaseScreen(Screen): diff --git a/my_egeria/my_egeria/screens/collections/add_collection.py b/my_egeria/my_egeria/screens/collections/add_collection.py index a2a09939..0401b473 100644 --- a/my_egeria/my_egeria/screens/collections/add_collection.py +++ b/my_egeria/my_egeria/screens/collections/add_collection.py @@ -14,8 +14,8 @@ from textual.widgets import Static, Input, Button # from .collection_browser import CollectionBrowserScreen -from my_egeria.screens.base_screen import BaseScreen -from my_egeria.services.collection_service import CollectionService +from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen +from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService from typing import Dict, Any def parse_kv_pairs(text: str) -> Dict[str, Any]: diff --git a/my_egeria/my_egeria/screens/collections/collection_browser.py b/my_egeria/my_egeria/screens/collections/collection_browser.py index 1cbcd0fc..d3798f6c 100644 --- a/my_egeria/my_egeria/screens/collections/collection_browser.py +++ b/my_egeria/my_egeria/screens/collections/collection_browser.py @@ -13,7 +13,7 @@ from textual.containers import Container, Vertical, Horizontal # from textual.geometry import Coordinate # remove unused/unsupported import from ..base_screen import BaseScreen -from my_egeria.services.collection_service import CollectionService +from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService from .add_collection import AddCollectionScreen from .delete_collection import DeleteCollectionScreen import asyncio diff --git a/my_egeria/my_egeria/screens/collections/collection_details.py b/my_egeria/my_egeria/screens/collections/collection_details.py index 0f2f2f9f..3686adc8 100644 --- a/my_egeria/my_egeria/screens/collections/collection_details.py +++ b/my_egeria/my_egeria/screens/collections/collection_details.py @@ -10,7 +10,7 @@ from textual.widgets import Static, DataTable, Button, Input from textual.containers import Vertical, Horizontal -from my_egeria.services.collection_service import CollectionService +from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService from ..base_screen import BaseScreen diff --git a/my_egeria/my_egeria/screens/collections/collection_members_screen.py b/my_egeria/my_egeria/screens/collections/collection_members_screen.py index 9eeeaf1e..7b4f66d0 100644 --- a/my_egeria/my_egeria/screens/collections/collection_members_screen.py +++ b/my_egeria/my_egeria/screens/collections/collection_members_screen.py @@ -14,7 +14,7 @@ from textual.app import ComposeResult from textual import on -from my_egeria.services.collection_service import CollectionService +from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService # from utils.egeria_client import EgeriaTechClientManager # import os from ..base_screen import BaseScreen diff --git a/my_egeria/my_egeria/screens/collections/delete_collection.py b/my_egeria/my_egeria/screens/collections/delete_collection.py index 0d1514fd..2cc0632d 100644 --- a/my_egeria/my_egeria/screens/collections/delete_collection.py +++ b/my_egeria/my_egeria/screens/collections/delete_collection.py @@ -14,7 +14,7 @@ from textual.containers import Container, Vertical, Horizontal from textual.widgets import Static, Button from ..base_screen import BaseScreen -from my_egeria.services.collection_service import CollectionService +from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService from typing import Dict, Any def parse_kv_pairs(text: str) -> Dict[str, Any]: diff --git a/my_egeria/my_egeria/screens/glossary/glossary_browser.py b/my_egeria/my_egeria/screens/glossary/glossary_browser.py index d4fa4eb4..621d2fc8 100644 --- a/my_egeria/my_egeria/screens/glossary/glossary_browser.py +++ b/my_egeria/my_egeria/screens/glossary/glossary_browser.py @@ -8,12 +8,10 @@ """ from textual.app import ComposeResult -from textual.screen import Screen from textual.widgets import Static, DataTable, Input, Button from textual.containers import Horizontal, Vertical, Container -from my_egeria.screens.base_screen import BaseScreen -from my_egeria.services.glossary_service import GlossaryService -from .term_details import TermDetailsScreen +from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen +from my_egeria.DemoCode.Deprecated.services.glossary_service import GlossaryService import asyncio diff --git a/my_egeria/my_egeria/screens/glossary/glossary_browser_old.py b/my_egeria/my_egeria/screens/glossary/glossary_browser_old.py index ec437a3c..a5cb8501 100644 --- a/my_egeria/my_egeria/screens/glossary/glossary_browser_old.py +++ b/my_egeria/my_egeria/screens/glossary/glossary_browser_old.py @@ -10,8 +10,8 @@ from textual.widgets import Static, DataTable, Input, Button from textual.containers import Horizontal, Vertical, Container -from my_egeria.screens.base_screen import BaseScreen -from my_egeria.services.glossary_service import GlossaryService +from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen +from my_egeria.DemoCode.Deprecated.services.glossary_service import GlossaryService from .term_details import TermDetailsScreen import asyncio diff --git a/my_egeria/my_egeria/screens/glossary/glossary_list_screen.py b/my_egeria/my_egeria/screens/glossary/glossary_list_screen.py index c2503aa8..ceb28637 100644 --- a/my_egeria/my_egeria/screens/glossary/glossary_list_screen.py +++ b/my_egeria/my_egeria/screens/glossary/glossary_list_screen.py @@ -10,9 +10,9 @@ from textual.widgets import Static from textual.containers import Vertical -from my_egeria.widgets.editable_table import EditableDataTable -from my_egeria.services.glossary_service import GlossaryService -from my_egeria.screens.base_screen import BaseScreen +from my_egeria.DemoCode.Deprecated.widgets.editable_table import EditableDataTable +from my_egeria.DemoCode.Deprecated.services.glossary_service import GlossaryService +from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen class GlossaryListScreen(BaseScreen): CSS_PATH = ["../styles/common.css", "/styles/glossary_list_screen.css"] diff --git a/my_egeria/my_egeria/screens/glossary/term_details.py b/my_egeria/my_egeria/screens/glossary/term_details.py index d176f54a..36aa98f2 100644 --- a/my_egeria/my_egeria/screens/glossary/term_details.py +++ b/my_egeria/my_egeria/screens/glossary/term_details.py @@ -9,8 +9,7 @@ """ from ..base_screen import BaseScreen -from my_egeria.widgets.editable_table import EditableDataTable, CellEdited -from my_egeria.widgets.ok_popup import OkPopup + class TermDetailsScreen(BaseScreen): diff --git a/my_egeria/my_egeria/screens/glossary/term_list_screen.py b/my_egeria/my_egeria/screens/glossary/term_list_screen.py index 027befa2..9c950a1b 100644 --- a/my_egeria/my_egeria/screens/glossary/term_list_screen.py +++ b/my_egeria/my_egeria/screens/glossary/term_list_screen.py @@ -10,8 +10,7 @@ from textual.widgets import Static from textual.containers import Vertical -from my_egeria.widgets.editable_table import EditableDataTable -from my_egeria.services.term_service import get_terms_for_glossary +from my_egeria.DemoCode.Deprecated.services.term_service import get_terms_for_glossary from ..base_screen import BaseScreen diff --git a/my_egeria/my_egeria/screens/governance_screen.py b/my_egeria/my_egeria/screens/governance_screen.py index 499edd08..f0b4e5a1 100644 --- a/my_egeria/my_egeria/screens/governance_screen.py +++ b/my_egeria/my_egeria/screens/governance_screen.py @@ -14,8 +14,8 @@ from textual.containers import Container from .base_screen import BaseScreen -from my_egeria.utils.config import get_global_config -from my_egeria.utils.egeria_client import EgeriaTechClientManager +from my_egeria.DemoCode.Deprecated.utils.config import get_global_config +from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager class GovernanceScreen(BaseScreen): diff --git a/my_egeria/my_egeria/screens/login_screen.py b/my_egeria/my_egeria/screens/login_screen.py index bc77150a..387d5821 100644 --- a/my_egeria/my_egeria/screens/login_screen.py +++ b/my_egeria/my_egeria/screens/login_screen.py @@ -13,7 +13,7 @@ from textual.message import Message from textual.widgets import Static, Input, Button from .base_screen import BaseScreen -from ..con_services.egeria_connection import connect_to_egeria +from my_egeria.con_services.egeria_connection import connect_to_egeria class LoginScreen(BaseScreen): diff --git a/my_egeria/my_egeria/screens/main_menu.py b/my_egeria/my_egeria/screens/main_menu.py index df5587b5..8b722280 100644 --- a/my_egeria/my_egeria/screens/main_menu.py +++ b/my_egeria/my_egeria/screens/main_menu.py @@ -12,8 +12,8 @@ from textual.containers import Container, Vertical from textual import events from my_egeria.con_services.egeria_connection import EgeriaConnectionService -from my_egeria.widgets.popup import show_popup_and_exit -from my_egeria.screens.base_screen import BaseScreen +from my_egeria.DemoCode.Deprecated.widgets.popup import show_popup_and_exit +from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen class MainMenuScreen(BaseScreen): diff --git a/my_egeria/my_egeria/screens/platform/platform_services.py b/my_egeria/my_egeria/screens/platform/platform_services.py index 96f16121..188f789c 100644 --- a/my_egeria/my_egeria/screens/platform/platform_services.py +++ b/my_egeria/my_egeria/screens/platform/platform_services.py @@ -13,8 +13,8 @@ from textual.containers import Container from textual import events -from my_egeria.services.platform_service import PlatformServices -from my_egeria.widgets.popup import show_popup_and_exit +from my_egeria.DemoCode.Deprecated.services.platform_service import PlatformServices +from my_egeria.DemoCode.Deprecated.widgets.popup import show_popup_and_exit class PlatformServicesScreen(Screen): diff --git a/my_egeria/my_egeria/screens/platform/platforms_screen.py b/my_egeria/my_egeria/screens/platform/platforms_screen.py index 046bed54..f2cb1148 100644 --- a/my_egeria/my_egeria/screens/platform/platforms_screen.py +++ b/my_egeria/my_egeria/screens/platform/platforms_screen.py @@ -11,8 +11,8 @@ from textual.widgets import Static, DataTable from textual.app import ComposeResult from textual.containers import Container -from my_egeria.utils.config import get_global_config -from my_egeria.utils.egeria_client import EgeriaTechClientManager +from my_egeria.DemoCode.Deprecated.utils.config import get_global_config +from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager from ..base_screen import BaseScreen diff --git a/my_egeria/my_egeria/screens/project_screen.py b/my_egeria/my_egeria/screens/project_screen.py index de666249..813ddc55 100644 --- a/my_egeria/my_egeria/screens/project_screen.py +++ b/my_egeria/my_egeria/screens/project_screen.py @@ -10,9 +10,9 @@ from textual.widgets import DataTable from textual.containers import Container -from my_egeria.services.project_manager_service import ProjectManagerService +from my_egeria.DemoCode.Deprecated.services.project_manager_service import ProjectManagerService from .base_screen import BaseScreen -from my_egeria.widgets.back_button import BackButton +from my_egeria.DemoCode.Deprecated.widgets.back_button import BackButton class ProjectScreen(BaseScreen): CSS_PATH = ["/styles/common.css", "/styles/project_screen.css"] diff --git a/my_egeria/my_egeria/screens/subject_area_screen.py b/my_egeria/my_egeria/screens/subject_area_screen.py index 1942bdaa..df4da247 100644 --- a/my_egeria/my_egeria/screens/subject_area_screen.py +++ b/my_egeria/my_egeria/screens/subject_area_screen.py @@ -11,8 +11,8 @@ from textual.screen import Screen from textual.widgets import DataTable, Header, Footer from textual.containers import Container -from my_egeria.widgets.back_button import BackButton -from my_egeria.services.subject_area_service import SubjectAreaService +from my_egeria.DemoCode.Deprecated.widgets.back_button import BackButton +from my_egeria.DemoCode.Deprecated.services.subject_area_service import SubjectAreaService class SubjectAreaScreen(Screen): diff --git a/my_egeria/my_egeria/services/base_service.py b/my_egeria/my_egeria/services/base_service.py index 192b6ec3..5e69c65d 100644 --- a/my_egeria/my_egeria/services/base_service.py +++ b/my_egeria/my_egeria/services/base_service.py @@ -9,8 +9,8 @@ """ from typing import Any, List, Dict, Optional, Tuple -from my_egeria.utils.egeria_client import EgeriaTechClientManager -from my_egeria.utils.config import EgeriaConfig, get_global_config +from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager +from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig, get_global_config from os import getenv class BaseService: diff --git a/my_egeria/my_egeria/services/collection_service.py b/my_egeria/my_egeria/services/collection_service.py index 8d74c717..07ecd7ac 100644 --- a/my_egeria/my_egeria/services/collection_service.py +++ b/my_egeria/my_egeria/services/collection_service.py @@ -10,7 +10,7 @@ import asyncio from typing import Any, Dict, List, Optional from .base_service import BaseService -from my_egeria.utils.config import EgeriaConfig +from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig class CollectionService(BaseService): """Wrapper around pyegeria collection functions with token-managed client.""" diff --git a/my_egeria/my_egeria/services/glossary_service.py b/my_egeria/my_egeria/services/glossary_service.py index 209f9c8e..1d53316d 100644 --- a/my_egeria/my_egeria/services/glossary_service.py +++ b/my_egeria/my_egeria/services/glossary_service.py @@ -15,7 +15,7 @@ import asyncio from typing import Any, Dict, List, Optional from .base_service import BaseService -from my_egeria.utils.config import EgeriaConfig +from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig # Hook for tests to inject a mock client diff --git a/my_egeria/my_egeria/services/governance_officer_service.py b/my_egeria/my_egeria/services/governance_officer_service.py index ae48699e..8d09174f 100644 --- a/my_egeria/my_egeria/services/governance_officer_service.py +++ b/my_egeria/my_egeria/services/governance_officer_service.py @@ -10,8 +10,8 @@ from typing import Optional from .base_service import BaseService -from my_egeria.utils.egeria_client import EgeriaTechClientManager -from my_egeria.utils.config import EgeriaConfig +from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager +from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig class GovernanceOfficerService(BaseService): def __init__(self, config): diff --git a/my_egeria/my_egeria/services/product_manager_service.py b/my_egeria/my_egeria/services/product_manager_service.py index 32c8277c..74b0c203 100644 --- a/my_egeria/my_egeria/services/product_manager_service.py +++ b/my_egeria/my_egeria/services/product_manager_service.py @@ -5,10 +5,9 @@ """ -import asyncio -from typing import Any, Dict, List, Optional +from typing import Optional from .base_service import BaseService -from my_egeria.utils.config import EgeriaConfig +from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig class ProductManagerService(BaseService): """Wrapper around pyegeria collection functions with token-managed client.""" diff --git a/my_egeria/my_egeria/services/project_manager_service.py b/my_egeria/my_egeria/services/project_manager_service.py index c10c8cf1..0981f000 100644 --- a/my_egeria/my_egeria/services/project_manager_service.py +++ b/my_egeria/my_egeria/services/project_manager_service.py @@ -8,10 +8,9 @@ """ -import asyncio from typing import Any, Dict, List, Optional from .base_service import BaseService -from my_egeria.utils.config import EgeriaConfig +from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig class ProjectManagerService(BaseService): """Wrapper around pyegeria collection functions with token-managed client.""" diff --git a/my_egeria/my_egeria/services/subject_area_service.py b/my_egeria/my_egeria/services/subject_area_service.py index d1b7cfac..455be0af 100644 --- a/my_egeria/my_egeria/services/subject_area_service.py +++ b/my_egeria/my_egeria/services/subject_area_service.py @@ -7,7 +7,7 @@ """ -from my_egeria.services.base_service import BaseService +from my_egeria.DemoCode.Deprecated.services.base_service import BaseService class SubjectAreaService(BaseService): diff --git a/my_egeria/my_egeria/tests/test_login_screen.py b/my_egeria/my_egeria/tests/test_login_screen.py index 7ebf0135..f883995d 100644 --- a/my_egeria/my_egeria/tests/test_login_screen.py +++ b/my_egeria/my_egeria/tests/test_login_screen.py @@ -8,11 +8,7 @@ """ -import pytest -from unittest.mock import patch from textual.app import App -from textual.widgets import Input, Button -from my_egeria.screens.login_screen import LoginScreen class LoginApp(App): diff --git a/my_egeria/my_egeria/tests/test_startup_check.py b/my_egeria/my_egeria/tests/test_startup_check.py index 1500c92d..2440f52c 100644 --- a/my_egeria/my_egeria/tests/test_startup_check.py +++ b/my_egeria/my_egeria/tests/test_startup_check.py @@ -10,7 +10,7 @@ import os import pytest -from ..startup_check import validate_envs, check_connection +from my_egeria.startup_check import validate_envs, check_connection def test_validate_envs_missing(monkeypatch): diff --git a/my_egeria/my_egeria/tests/test_term_details_screen.py b/my_egeria/my_egeria/tests/test_term_details_screen.py index fca5ccf2..e2ae04dd 100644 --- a/my_egeria/my_egeria/tests/test_term_details_screen.py +++ b/my_egeria/my_egeria/tests/test_term_details_screen.py @@ -9,7 +9,6 @@ """ from textual.app import App -from my_egeria.screens.glossary.term_details import TermDetailsScreen class TestApp(App): diff --git a/my_report_specs/my_profile_report_specs.json b/my_report_specs/my_profile_report_specs.json new file mode 100644 index 00000000..bf6d3cf4 --- /dev/null +++ b/my_report_specs/my_profile_report_specs.json @@ -0,0 +1,47 @@ +{ + "//_comment1": "PDX-License-Identifier: Apache-2.0", + "//_comment2": "Copyright Contributors to the ODPi Egeria project.", + "//_comment3": " ", + "//_comment4": " This file provides the custom report specs for My Profile Textual App. ", + "//_comment5": " Author: Peter A Coldicott ", + + "My-User-Peers": { + "target_type": "Peer", + "heading": "Peers", + "description": "Detailed Peer Connections", + "family": "MyProfile", + "formats": [ + { + "types": [ + "LIST", + "REPORT", + "DICT", + "TABLE" + ], + "attributes": [ + { + "name": "Label", + "key": "label" + }, + { + "name": "Qualified Name", + "key": "uniqueName" + }, + { + "name": "Scope", + "key": "scope" + }, + { + "name": "Description", + "key": "relationshipProperties.description" + }, + { + "name": "GUID", + "key": "guid", + "format": "True" + } + ] + } + ] + } +} \ No newline at end of file From 024c5988ed6928e2f52f5d3458a655a628117aeb Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:11:50 -0500 Subject: [PATCH 04/19] Further reformatting to clear out deprecated files and add in the use of the custome report format Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../DemoCode/Experimental Code/my_network.py | 8 ++-- my_egeria/my_egeria/README.md | 10 ++--- .../con_services/egeria_connection.py | 4 +- my_egeria/my_egeria/error_popup_app.py | 2 +- my_egeria/my_egeria/my_egeria_app.py | 42 ++++++++++--------- .../add_governance_definition.py | 4 +- .../delete_governance_definition.py | 4 +- .../governance_officer_browser.py | 4 +- .../GovernanceOfficer/marketplace_tree.py | 1 + .../ProductManager/product_manager_browser.py | 4 +- .../ProjectManager/ProjectManagerBrowser.py | 2 +- my_egeria/my_egeria/screens/base_screen.py | 4 +- .../screens/collections/add_collection.py | 4 +- .../screens/collections/collection_browser.py | 2 +- .../screens/collections/collection_details.py | 2 +- .../collections/collection_members_screen.py | 2 +- .../screens/collections/delete_collection.py | 2 +- .../screens/glossary/glossary_browser.py | 6 ++- .../screens/glossary/glossary_browser_old.py | 4 +- .../screens/glossary/glossary_list_screen.py | 6 +-- .../screens/glossary/term_details.py | 3 +- .../screens/glossary/term_list_screen.py | 3 +- .../my_egeria/screens/governance_screen.py | 4 +- my_egeria/my_egeria/screens/login_screen.py | 2 +- my_egeria/my_egeria/screens/main_menu.py | 4 +- .../screens/platform/platform_services.py | 4 +- .../screens/platform/platforms_screen.py | 4 +- my_egeria/my_egeria/screens/project_screen.py | 4 +- .../my_egeria/screens/subject_area_screen.py | 4 +- my_egeria/my_egeria/services/base_service.py | 4 +- .../my_egeria/services/collection_service.py | 2 +- .../my_egeria/services/glossary_service.py | 2 +- .../services/governance_officer_service.py | 4 +- .../services/product_manager_service.py | 5 ++- .../services/project_manager_service.py | 3 +- .../services/subject_area_service.py | 2 +- .../my_egeria/tests/test_login_screen.py | 4 ++ .../my_egeria/tests/test_startup_check.py | 2 +- .../tests/test_term_details_screen.py | 1 + 39 files changed, 97 insertions(+), 81 deletions(-) diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py index 8a20343d..baa7da53 100644 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py @@ -113,10 +113,10 @@ def on_mount(self) -> None: try: eclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - peers = eclient.get_relationships( - relationship_type="Peer", - output_format="JSON", - page_size=30) + peers = eclient.exec_report_spec( + format_set_name="My-User-Peers", + output_format="DICT", + ) self.log(f"Peers: {peers}") except PyegeriaException as e: self.log(f"Error creating peer relationship: {e}") diff --git a/my_egeria/my_egeria/README.md b/my_egeria/my_egeria/README.md index fcbdbbe0..1288dffa 100644 --- a/my_egeria/my_egeria/README.md +++ b/my_egeria/my_egeria/README.md @@ -10,11 +10,11 @@ The `my-egeria` Textual TUI application's package root — see the parent |---|---| | `main.py` | Entry point (`uv run my-egeria` / `python -m my_egeria.main`). | | `my_egeria_app.py` | The main Textual `App` class. | -| `DemoCode/Deprecated/screens` | Textual screens, one per feature area (`GovernanceOfficer`, `ProductManager`, `ProjectManager`, `collections`, `glossary`, `platform`). | -| `DemoCode/Deprecated/widgets` | Reusable Textual widgets shared across screens. | -| `DemoCode/Deprecated/services`, `con_services/` | Backend service layer wrapping pyegeria SDK calls for the screens/widgets to use. | -| `DemoCode/Deprecated/utils` | Shared helpers. | -| `DemoCode/Deprecated/styles` | Textual CSS. | +| `screens/` | Textual screens, one per feature area (`GovernanceOfficer`, `ProductManager`, `ProjectManager`, `collections`, `glossary`, `platform`). | +| `widgets/` | Reusable Textual widgets shared across screens. | +| `services/`, `con_services/` | Backend service layer wrapping pyegeria SDK calls for the screens/widgets to use. | +| `utils/` | Shared helpers. | +| `styles/` | Textual CSS. | | `tests/` | Tests for this app. | | `error_popup_app.py`, `startup_check.py` | Startup/error-handling helpers. | | `serve.py` | Browser-mode serving via the `textual-serve` library — see the `serve_my_egeria`/`serve_my_profile` entry points in the root `pyproject.toml`. | diff --git a/my_egeria/my_egeria/con_services/egeria_connection.py b/my_egeria/my_egeria/con_services/egeria_connection.py index dd77cc04..3f577c6a 100644 --- a/my_egeria/my_egeria/con_services/egeria_connection.py +++ b/my_egeria/my_egeria/con_services/egeria_connection.py @@ -10,8 +10,8 @@ from __future__ import annotations from typing import Optional -from my_egeria.DemoCode.Deprecated.utils import get_global_config -from my_egeria.DemoCode.Deprecated.utils import preflight_origin +from ..utils.config import get_global_config +from ..utils.egeria_client import preflight_origin class EgeriaConnectionService: """Checks if Egeria is reachable and authenticates.""" diff --git a/my_egeria/my_egeria/error_popup_app.py b/my_egeria/my_egeria/error_popup_app.py index 0f036111..2ef978a6 100644 --- a/my_egeria/my_egeria/error_popup_app.py +++ b/my_egeria/my_egeria/error_popup_app.py @@ -6,7 +6,7 @@ """ from textual.app import App -from my_egeria.DemoCode.Deprecated.screens import ErrorPopup +from my_egeria.screens.error_popup_screen import ErrorPopup class ErrorPopupApp (App): diff --git a/my_egeria/my_egeria/my_egeria_app.py b/my_egeria/my_egeria/my_egeria_app.py index 386d29fc..075854ae 100644 --- a/my_egeria/my_egeria/my_egeria_app.py +++ b/my_egeria/my_egeria/my_egeria_app.py @@ -8,9 +8,12 @@ """ import os +from typing import Any from textual import on +from . import utils + # Set safe defaults BEFORE importing anything that might import pyegeria os.environ.setdefault("EGERIA_USER", "erinoverview") os.environ.setdefault("EGERIA_USER_PASSWORD", "secret") @@ -19,25 +22,26 @@ from textual.app import App, ComposeResult from textual.widgets import Footer -from my_egeria.DemoCode.Deprecated.screens import LoginScreen -from my_egeria.DemoCode.Deprecated.screens.main_menu import MainMenuScreen -from my_egeria.DemoCode.Deprecated.screens.glossary import GlossaryBrowserScreen -from my_egeria.DemoCode.Deprecated.screens.collections.collection_browser import CollectionBrowserScreen -from my_egeria.DemoCode.Deprecated.screens.collections.collection_members_screen import CollectionMemberScreen -from my_egeria.DemoCode.Deprecated.screens.collections.collection_details import CollectionDetailsScreen -from my_egeria.DemoCode.Deprecated.screens.collections import AddCollectionScreen -from my_egeria.DemoCode.Deprecated.screens.collections.delete_collection import DeleteCollectionScreen -from my_egeria.DemoCode.Deprecated.screens.glossary.glossary_list_screen import GlossaryListScreen -from my_egeria.DemoCode.Deprecated.screens.glossary.term_details import TermDetailsScreen -from my_egeria.DemoCode.Deprecated.screens.glossary.term_list_screen import TermListScreen -from my_egeria.DemoCode.Deprecated.screens.GovernanceOfficer.governance_officer_browser import GovernanceOfficerBrowserScreen -from my_egeria.DemoCode.Deprecated.screens.GovernanceOfficer import AddGovernanceDefinitionScreen -from my_egeria.DemoCode.Deprecated.screens.GovernanceOfficer import DeleteGovernanceDefinitionScreen -from my_egeria.DemoCode.Deprecated.screens.GovernanceOfficer.marketplace_tree import MarketPlaceTree -from my_egeria.DemoCode.Deprecated.screens.ProductManager.product_manager_browser import ProductManagerBrowser -from my_egeria.DemoCode.Deprecated.utils.egeria_client import close_all_managers -from my_egeria.DemoCode.Deprecated.screens.splash_screen import SplashScreen # your existing splash screen - +from my_egeria.screens.login_screen import LoginScreen +from my_egeria.screens.main_menu import MainMenuScreen +from my_egeria.screens.glossary.glossary_browser import GlossaryBrowserScreen +from my_egeria.screens.collections.collection_browser import CollectionBrowserScreen +from my_egeria.screens.collections.collection_members_screen import CollectionMemberScreen +from my_egeria.screens.collections.collection_details import CollectionDetailsScreen +from my_egeria.screens.collections.add_collection import AddCollectionScreen +from my_egeria.screens.collections.delete_collection import DeleteCollectionScreen +from my_egeria.screens.glossary.glossary_list_screen import GlossaryListScreen +from my_egeria.screens.glossary.term_details import TermDetailsScreen +from my_egeria.screens.glossary.term_list_screen import TermListScreen +from my_egeria.screens.GovernanceOfficer.governance_officer_browser import GovernanceOfficerBrowserScreen +from my_egeria.screens.GovernanceOfficer.add_governance_definition import AddGovernanceDefinitionScreen +from my_egeria.screens.GovernanceOfficer.delete_governance_definition import DeleteGovernanceDefinitionScreen +from my_egeria.screens.GovernanceOfficer.marketplace_tree import MarketPlaceTree +from my_egeria.screens.ProductManager.product_manager_browser import ProductManagerBrowser +from my_egeria.utils.egeria_client import close_all_managers +from my_egeria.utils.config import EgeriaConfig +from my_egeria.screens.splash_screen import SplashScreen # your existing splash screen +from my_egeria.services.term_service import get_terms_for_glossary class MyEgeria(App): """Main app class of my_egeria.""" diff --git a/my_egeria/my_egeria/screens/GovernanceOfficer/add_governance_definition.py b/my_egeria/my_egeria/screens/GovernanceOfficer/add_governance_definition.py index 4d3bafaf..101099a9 100644 --- a/my_egeria/my_egeria/screens/GovernanceOfficer/add_governance_definition.py +++ b/my_egeria/my_egeria/screens/GovernanceOfficer/add_governance_definition.py @@ -17,9 +17,9 @@ from textual.widgets import Static, Input, Button, Pretty, OptionList, Footer, Header # from .governance_officer_browser import GovernanceOfficerBrowserScreen from ..base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.governance_officer_service import GovernanceOfficerService +from my_egeria.services.governance_officer_service import GovernanceOfficerService from typing import Dict, Any - +from my_egeria.utils.config import EgeriaConfig def parse_kv_pairs(text: str) -> Dict[str, Any]: """ diff --git a/my_egeria/my_egeria/screens/GovernanceOfficer/delete_governance_definition.py b/my_egeria/my_egeria/screens/GovernanceOfficer/delete_governance_definition.py index ae3a34c1..c24f5e98 100644 --- a/my_egeria/my_egeria/screens/GovernanceOfficer/delete_governance_definition.py +++ b/my_egeria/my_egeria/screens/GovernanceOfficer/delete_governance_definition.py @@ -9,8 +9,8 @@ """ from textual.messages import Message from typing import Dict, Any -from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.governance_officer_service import GovernanceOfficerService +from my_egeria.screens.base_screen import BaseScreen +from my_egeria.services.governance_officer_service import GovernanceOfficerService from textual.containers import Container, Vertical, Horizontal from textual.widgets import Static, Button import asyncio diff --git a/my_egeria/my_egeria/screens/GovernanceOfficer/governance_officer_browser.py b/my_egeria/my_egeria/screens/GovernanceOfficer/governance_officer_browser.py index 086ce45e..e1ecfef9 100644 --- a/my_egeria/my_egeria/screens/GovernanceOfficer/governance_officer_browser.py +++ b/my_egeria/my_egeria/screens/GovernanceOfficer/governance_officer_browser.py @@ -13,12 +13,12 @@ from textual.containers import Container, Vertical, Horizontal from ..base_screen import BaseScreen from .marketplace_tree import MarketPlaceTree -from my_egeria.DemoCode.Deprecated.services.governance_officer_service import GovernanceOfficerService +from my_egeria.services.governance_officer_service import GovernanceOfficerService from .add_governance_definition import AddGovernanceDefinitionScreen from .delete_governance_definition import DeleteGovernanceDefinitionScreen import asyncio from textual import on -from my_egeria.DemoCode.Deprecated.utils.config import get_global_config +from my_egeria.utils.config import EgeriaConfig, get_global_config # ... existing imports ... diff --git a/my_egeria/my_egeria/screens/GovernanceOfficer/marketplace_tree.py b/my_egeria/my_egeria/screens/GovernanceOfficer/marketplace_tree.py index 6d0aca88..29867d67 100644 --- a/my_egeria/my_egeria/screens/GovernanceOfficer/marketplace_tree.py +++ b/my_egeria/my_egeria/screens/GovernanceOfficer/marketplace_tree.py @@ -9,6 +9,7 @@ """ from ..base_screen import BaseScreen from textual.widgets import Button, Tree, Static, Input +from my_egeria.utils.config import EgeriaConfig, get_global_config from textual import on from textual.containers import Container, Horizontal diff --git a/my_egeria/my_egeria/screens/ProductManager/product_manager_browser.py b/my_egeria/my_egeria/screens/ProductManager/product_manager_browser.py index 60919390..5ee717a1 100644 --- a/my_egeria/my_egeria/screens/ProductManager/product_manager_browser.py +++ b/my_egeria/my_egeria/screens/ProductManager/product_manager_browser.py @@ -7,8 +7,8 @@ """ # from textual.geometry import Coordinate # remove unused/unsupported import -from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.product_manager_service import ProductManagerService +from my_egeria.screens.base_screen import BaseScreen +from my_egeria.services.product_manager_service import ProductManagerService # from .collection_details import CollectionDetailsScreen # from .add_collection import AddCollectionScreen # from .delete_collection import DeleteCollectionScreen diff --git a/my_egeria/my_egeria/screens/ProjectManager/ProjectManagerBrowser.py b/my_egeria/my_egeria/screens/ProjectManager/ProjectManagerBrowser.py index 0cfd6055..38a5d294 100644 --- a/my_egeria/my_egeria/screens/ProjectManager/ProjectManagerBrowser.py +++ b/my_egeria/my_egeria/screens/ProjectManager/ProjectManagerBrowser.py @@ -7,7 +7,7 @@ """ # from textual.geometry import Coordinate # remove unused/unsupported import from ..base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.project_manager_service import ProjectManagerService +from my_egeria.services.project_manager_service import ProjectManagerService # from .collection_details import CollectionDetailsScreen # from .add_collection import AddCollectionScreen # from .delete_collection import DeleteCollectionScreen diff --git a/my_egeria/my_egeria/screens/base_screen.py b/my_egeria/my_egeria/screens/base_screen.py index aac7be26..f664fc7f 100644 --- a/my_egeria/my_egeria/screens/base_screen.py +++ b/my_egeria/my_egeria/screens/base_screen.py @@ -13,8 +13,8 @@ from textual.app import ComposeResult from textual import on from textual.containers import Container -from my_egeria.DemoCode.Deprecated.utils.config import get_global_config -from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager +from my_egeria.utils.config import get_global_config +from my_egeria.utils.egeria_client import EgeriaTechClientManager from my_egeria.con_services.egeria_connection import EgeriaConnectionService class BaseScreen(Screen): diff --git a/my_egeria/my_egeria/screens/collections/add_collection.py b/my_egeria/my_egeria/screens/collections/add_collection.py index 0401b473..a2a09939 100644 --- a/my_egeria/my_egeria/screens/collections/add_collection.py +++ b/my_egeria/my_egeria/screens/collections/add_collection.py @@ -14,8 +14,8 @@ from textual.widgets import Static, Input, Button # from .collection_browser import CollectionBrowserScreen -from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService +from my_egeria.screens.base_screen import BaseScreen +from my_egeria.services.collection_service import CollectionService from typing import Dict, Any def parse_kv_pairs(text: str) -> Dict[str, Any]: diff --git a/my_egeria/my_egeria/screens/collections/collection_browser.py b/my_egeria/my_egeria/screens/collections/collection_browser.py index d3798f6c..1cbcd0fc 100644 --- a/my_egeria/my_egeria/screens/collections/collection_browser.py +++ b/my_egeria/my_egeria/screens/collections/collection_browser.py @@ -13,7 +13,7 @@ from textual.containers import Container, Vertical, Horizontal # from textual.geometry import Coordinate # remove unused/unsupported import from ..base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService +from my_egeria.services.collection_service import CollectionService from .add_collection import AddCollectionScreen from .delete_collection import DeleteCollectionScreen import asyncio diff --git a/my_egeria/my_egeria/screens/collections/collection_details.py b/my_egeria/my_egeria/screens/collections/collection_details.py index 3686adc8..0f2f2f9f 100644 --- a/my_egeria/my_egeria/screens/collections/collection_details.py +++ b/my_egeria/my_egeria/screens/collections/collection_details.py @@ -10,7 +10,7 @@ from textual.widgets import Static, DataTable, Button, Input from textual.containers import Vertical, Horizontal -from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService +from my_egeria.services.collection_service import CollectionService from ..base_screen import BaseScreen diff --git a/my_egeria/my_egeria/screens/collections/collection_members_screen.py b/my_egeria/my_egeria/screens/collections/collection_members_screen.py index 7b4f66d0..9eeeaf1e 100644 --- a/my_egeria/my_egeria/screens/collections/collection_members_screen.py +++ b/my_egeria/my_egeria/screens/collections/collection_members_screen.py @@ -14,7 +14,7 @@ from textual.app import ComposeResult from textual import on -from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService +from my_egeria.services.collection_service import CollectionService # from utils.egeria_client import EgeriaTechClientManager # import os from ..base_screen import BaseScreen diff --git a/my_egeria/my_egeria/screens/collections/delete_collection.py b/my_egeria/my_egeria/screens/collections/delete_collection.py index 2cc0632d..0d1514fd 100644 --- a/my_egeria/my_egeria/screens/collections/delete_collection.py +++ b/my_egeria/my_egeria/screens/collections/delete_collection.py @@ -14,7 +14,7 @@ from textual.containers import Container, Vertical, Horizontal from textual.widgets import Static, Button from ..base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.collection_service import CollectionService +from my_egeria.services.collection_service import CollectionService from typing import Dict, Any def parse_kv_pairs(text: str) -> Dict[str, Any]: diff --git a/my_egeria/my_egeria/screens/glossary/glossary_browser.py b/my_egeria/my_egeria/screens/glossary/glossary_browser.py index 621d2fc8..d4fa4eb4 100644 --- a/my_egeria/my_egeria/screens/glossary/glossary_browser.py +++ b/my_egeria/my_egeria/screens/glossary/glossary_browser.py @@ -8,10 +8,12 @@ """ from textual.app import ComposeResult +from textual.screen import Screen from textual.widgets import Static, DataTable, Input, Button from textual.containers import Horizontal, Vertical, Container -from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.glossary_service import GlossaryService +from my_egeria.screens.base_screen import BaseScreen +from my_egeria.services.glossary_service import GlossaryService +from .term_details import TermDetailsScreen import asyncio diff --git a/my_egeria/my_egeria/screens/glossary/glossary_browser_old.py b/my_egeria/my_egeria/screens/glossary/glossary_browser_old.py index a5cb8501..ec437a3c 100644 --- a/my_egeria/my_egeria/screens/glossary/glossary_browser_old.py +++ b/my_egeria/my_egeria/screens/glossary/glossary_browser_old.py @@ -10,8 +10,8 @@ from textual.widgets import Static, DataTable, Input, Button from textual.containers import Horizontal, Vertical, Container -from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.services.glossary_service import GlossaryService +from my_egeria.screens.base_screen import BaseScreen +from my_egeria.services.glossary_service import GlossaryService from .term_details import TermDetailsScreen import asyncio diff --git a/my_egeria/my_egeria/screens/glossary/glossary_list_screen.py b/my_egeria/my_egeria/screens/glossary/glossary_list_screen.py index ceb28637..c2503aa8 100644 --- a/my_egeria/my_egeria/screens/glossary/glossary_list_screen.py +++ b/my_egeria/my_egeria/screens/glossary/glossary_list_screen.py @@ -10,9 +10,9 @@ from textual.widgets import Static from textual.containers import Vertical -from my_egeria.DemoCode.Deprecated.widgets.editable_table import EditableDataTable -from my_egeria.DemoCode.Deprecated.services.glossary_service import GlossaryService -from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen +from my_egeria.widgets.editable_table import EditableDataTable +from my_egeria.services.glossary_service import GlossaryService +from my_egeria.screens.base_screen import BaseScreen class GlossaryListScreen(BaseScreen): CSS_PATH = ["../styles/common.css", "/styles/glossary_list_screen.css"] diff --git a/my_egeria/my_egeria/screens/glossary/term_details.py b/my_egeria/my_egeria/screens/glossary/term_details.py index 36aa98f2..d176f54a 100644 --- a/my_egeria/my_egeria/screens/glossary/term_details.py +++ b/my_egeria/my_egeria/screens/glossary/term_details.py @@ -9,7 +9,8 @@ """ from ..base_screen import BaseScreen - +from my_egeria.widgets.editable_table import EditableDataTable, CellEdited +from my_egeria.widgets.ok_popup import OkPopup class TermDetailsScreen(BaseScreen): diff --git a/my_egeria/my_egeria/screens/glossary/term_list_screen.py b/my_egeria/my_egeria/screens/glossary/term_list_screen.py index 9c950a1b..027befa2 100644 --- a/my_egeria/my_egeria/screens/glossary/term_list_screen.py +++ b/my_egeria/my_egeria/screens/glossary/term_list_screen.py @@ -10,7 +10,8 @@ from textual.widgets import Static from textual.containers import Vertical -from my_egeria.DemoCode.Deprecated.services.term_service import get_terms_for_glossary +from my_egeria.widgets.editable_table import EditableDataTable +from my_egeria.services.term_service import get_terms_for_glossary from ..base_screen import BaseScreen diff --git a/my_egeria/my_egeria/screens/governance_screen.py b/my_egeria/my_egeria/screens/governance_screen.py index f0b4e5a1..499edd08 100644 --- a/my_egeria/my_egeria/screens/governance_screen.py +++ b/my_egeria/my_egeria/screens/governance_screen.py @@ -14,8 +14,8 @@ from textual.containers import Container from .base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.utils.config import get_global_config -from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager +from my_egeria.utils.config import get_global_config +from my_egeria.utils.egeria_client import EgeriaTechClientManager class GovernanceScreen(BaseScreen): diff --git a/my_egeria/my_egeria/screens/login_screen.py b/my_egeria/my_egeria/screens/login_screen.py index 387d5821..bc77150a 100644 --- a/my_egeria/my_egeria/screens/login_screen.py +++ b/my_egeria/my_egeria/screens/login_screen.py @@ -13,7 +13,7 @@ from textual.message import Message from textual.widgets import Static, Input, Button from .base_screen import BaseScreen -from my_egeria.con_services.egeria_connection import connect_to_egeria +from ..con_services.egeria_connection import connect_to_egeria class LoginScreen(BaseScreen): diff --git a/my_egeria/my_egeria/screens/main_menu.py b/my_egeria/my_egeria/screens/main_menu.py index 8b722280..df5587b5 100644 --- a/my_egeria/my_egeria/screens/main_menu.py +++ b/my_egeria/my_egeria/screens/main_menu.py @@ -12,8 +12,8 @@ from textual.containers import Container, Vertical from textual import events from my_egeria.con_services.egeria_connection import EgeriaConnectionService -from my_egeria.DemoCode.Deprecated.widgets.popup import show_popup_and_exit -from my_egeria.DemoCode.Deprecated.screens.base_screen import BaseScreen +from my_egeria.widgets.popup import show_popup_and_exit +from my_egeria.screens.base_screen import BaseScreen class MainMenuScreen(BaseScreen): diff --git a/my_egeria/my_egeria/screens/platform/platform_services.py b/my_egeria/my_egeria/screens/platform/platform_services.py index 188f789c..96f16121 100644 --- a/my_egeria/my_egeria/screens/platform/platform_services.py +++ b/my_egeria/my_egeria/screens/platform/platform_services.py @@ -13,8 +13,8 @@ from textual.containers import Container from textual import events -from my_egeria.DemoCode.Deprecated.services.platform_service import PlatformServices -from my_egeria.DemoCode.Deprecated.widgets.popup import show_popup_and_exit +from my_egeria.services.platform_service import PlatformServices +from my_egeria.widgets.popup import show_popup_and_exit class PlatformServicesScreen(Screen): diff --git a/my_egeria/my_egeria/screens/platform/platforms_screen.py b/my_egeria/my_egeria/screens/platform/platforms_screen.py index f2cb1148..046bed54 100644 --- a/my_egeria/my_egeria/screens/platform/platforms_screen.py +++ b/my_egeria/my_egeria/screens/platform/platforms_screen.py @@ -11,8 +11,8 @@ from textual.widgets import Static, DataTable from textual.app import ComposeResult from textual.containers import Container -from my_egeria.DemoCode.Deprecated.utils.config import get_global_config -from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager +from my_egeria.utils.config import get_global_config +from my_egeria.utils.egeria_client import EgeriaTechClientManager from ..base_screen import BaseScreen diff --git a/my_egeria/my_egeria/screens/project_screen.py b/my_egeria/my_egeria/screens/project_screen.py index 813ddc55..de666249 100644 --- a/my_egeria/my_egeria/screens/project_screen.py +++ b/my_egeria/my_egeria/screens/project_screen.py @@ -10,9 +10,9 @@ from textual.widgets import DataTable from textual.containers import Container -from my_egeria.DemoCode.Deprecated.services.project_manager_service import ProjectManagerService +from my_egeria.services.project_manager_service import ProjectManagerService from .base_screen import BaseScreen -from my_egeria.DemoCode.Deprecated.widgets.back_button import BackButton +from my_egeria.widgets.back_button import BackButton class ProjectScreen(BaseScreen): CSS_PATH = ["/styles/common.css", "/styles/project_screen.css"] diff --git a/my_egeria/my_egeria/screens/subject_area_screen.py b/my_egeria/my_egeria/screens/subject_area_screen.py index df4da247..1942bdaa 100644 --- a/my_egeria/my_egeria/screens/subject_area_screen.py +++ b/my_egeria/my_egeria/screens/subject_area_screen.py @@ -11,8 +11,8 @@ from textual.screen import Screen from textual.widgets import DataTable, Header, Footer from textual.containers import Container -from my_egeria.DemoCode.Deprecated.widgets.back_button import BackButton -from my_egeria.DemoCode.Deprecated.services.subject_area_service import SubjectAreaService +from my_egeria.widgets.back_button import BackButton +from my_egeria.services.subject_area_service import SubjectAreaService class SubjectAreaScreen(Screen): diff --git a/my_egeria/my_egeria/services/base_service.py b/my_egeria/my_egeria/services/base_service.py index 5e69c65d..192b6ec3 100644 --- a/my_egeria/my_egeria/services/base_service.py +++ b/my_egeria/my_egeria/services/base_service.py @@ -9,8 +9,8 @@ """ from typing import Any, List, Dict, Optional, Tuple -from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager -from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig, get_global_config +from my_egeria.utils.egeria_client import EgeriaTechClientManager +from my_egeria.utils.config import EgeriaConfig, get_global_config from os import getenv class BaseService: diff --git a/my_egeria/my_egeria/services/collection_service.py b/my_egeria/my_egeria/services/collection_service.py index 07ecd7ac..8d74c717 100644 --- a/my_egeria/my_egeria/services/collection_service.py +++ b/my_egeria/my_egeria/services/collection_service.py @@ -10,7 +10,7 @@ import asyncio from typing import Any, Dict, List, Optional from .base_service import BaseService -from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig +from my_egeria.utils.config import EgeriaConfig class CollectionService(BaseService): """Wrapper around pyegeria collection functions with token-managed client.""" diff --git a/my_egeria/my_egeria/services/glossary_service.py b/my_egeria/my_egeria/services/glossary_service.py index 1d53316d..209f9c8e 100644 --- a/my_egeria/my_egeria/services/glossary_service.py +++ b/my_egeria/my_egeria/services/glossary_service.py @@ -15,7 +15,7 @@ import asyncio from typing import Any, Dict, List, Optional from .base_service import BaseService -from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig +from my_egeria.utils.config import EgeriaConfig # Hook for tests to inject a mock client diff --git a/my_egeria/my_egeria/services/governance_officer_service.py b/my_egeria/my_egeria/services/governance_officer_service.py index 8d09174f..ae48699e 100644 --- a/my_egeria/my_egeria/services/governance_officer_service.py +++ b/my_egeria/my_egeria/services/governance_officer_service.py @@ -10,8 +10,8 @@ from typing import Optional from .base_service import BaseService -from my_egeria.DemoCode.Deprecated.utils.egeria_client import EgeriaTechClientManager -from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig +from my_egeria.utils.egeria_client import EgeriaTechClientManager +from my_egeria.utils.config import EgeriaConfig class GovernanceOfficerService(BaseService): def __init__(self, config): diff --git a/my_egeria/my_egeria/services/product_manager_service.py b/my_egeria/my_egeria/services/product_manager_service.py index 74b0c203..32c8277c 100644 --- a/my_egeria/my_egeria/services/product_manager_service.py +++ b/my_egeria/my_egeria/services/product_manager_service.py @@ -5,9 +5,10 @@ """ -from typing import Optional +import asyncio +from typing import Any, Dict, List, Optional from .base_service import BaseService -from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig +from my_egeria.utils.config import EgeriaConfig class ProductManagerService(BaseService): """Wrapper around pyegeria collection functions with token-managed client.""" diff --git a/my_egeria/my_egeria/services/project_manager_service.py b/my_egeria/my_egeria/services/project_manager_service.py index 0981f000..c10c8cf1 100644 --- a/my_egeria/my_egeria/services/project_manager_service.py +++ b/my_egeria/my_egeria/services/project_manager_service.py @@ -8,9 +8,10 @@ """ +import asyncio from typing import Any, Dict, List, Optional from .base_service import BaseService -from my_egeria.DemoCode.Deprecated.utils.config import EgeriaConfig +from my_egeria.utils.config import EgeriaConfig class ProjectManagerService(BaseService): """Wrapper around pyegeria collection functions with token-managed client.""" diff --git a/my_egeria/my_egeria/services/subject_area_service.py b/my_egeria/my_egeria/services/subject_area_service.py index 455be0af..d1b7cfac 100644 --- a/my_egeria/my_egeria/services/subject_area_service.py +++ b/my_egeria/my_egeria/services/subject_area_service.py @@ -7,7 +7,7 @@ """ -from my_egeria.DemoCode.Deprecated.services.base_service import BaseService +from my_egeria.services.base_service import BaseService class SubjectAreaService(BaseService): diff --git a/my_egeria/my_egeria/tests/test_login_screen.py b/my_egeria/my_egeria/tests/test_login_screen.py index f883995d..7ebf0135 100644 --- a/my_egeria/my_egeria/tests/test_login_screen.py +++ b/my_egeria/my_egeria/tests/test_login_screen.py @@ -8,7 +8,11 @@ """ +import pytest +from unittest.mock import patch from textual.app import App +from textual.widgets import Input, Button +from my_egeria.screens.login_screen import LoginScreen class LoginApp(App): diff --git a/my_egeria/my_egeria/tests/test_startup_check.py b/my_egeria/my_egeria/tests/test_startup_check.py index 2440f52c..1500c92d 100644 --- a/my_egeria/my_egeria/tests/test_startup_check.py +++ b/my_egeria/my_egeria/tests/test_startup_check.py @@ -10,7 +10,7 @@ import os import pytest -from my_egeria.startup_check import validate_envs, check_connection +from ..startup_check import validate_envs, check_connection def test_validate_envs_missing(monkeypatch): diff --git a/my_egeria/my_egeria/tests/test_term_details_screen.py b/my_egeria/my_egeria/tests/test_term_details_screen.py index e2ae04dd..fca5ccf2 100644 --- a/my_egeria/my_egeria/tests/test_term_details_screen.py +++ b/my_egeria/my_egeria/tests/test_term_details_screen.py @@ -9,6 +9,7 @@ """ from textual.app import App +from my_egeria.screens.glossary.term_details import TermDetailsScreen class TestApp(App): From f8b76d30da868270c4bc9153865c5e27d6c2565d Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:46:45 -0500 Subject: [PATCH 05/19] Updates to Support Custom Report Spec Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- config/config.json | 7 +++---- .../DemoCode/Experimental Code/my_network.py | 2 +- my_report_specs/my_profile_report_specs.json | 12 +++++------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/config/config.json b/config/config.json index 6d3e25ad..08e0ce2e 100644 --- a/config/config.json +++ b/config/config.json @@ -28,7 +28,8 @@ "Pyegeria Publishing Root": "http://localhost:8085/outbox", "Pyegeria User Report Specs Dir": "~/.pyegeria/report_specs", "Pyegeria Report Spec Modules": [ - "pyegeria.view.analytic_demo_specs:get_analytic_demo_specs" + "pyegeria.view.analytic_demo_specs:get_analytic_demo_specs", + "~/my_report_specs/my_profile_report_specs.json" ] }, "feature_x_enabled": false, @@ -52,7 +53,5 @@ "Egeria Local Qualifier": "myLocal", "Egeria Usage Level": "Basic" }, - "Pyegeria Report Spec Modules": [ - "~/my_report_specs/my_profile_report_specs.json" - ] + } \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py index baa7da53..3eadf97b 100644 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py @@ -113,7 +113,7 @@ def on_mount(self) -> None: try: eclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - peers = eclient.exec_report_spec( + peers = exec_report_spec( format_set_name="My-User-Peers", output_format="DICT", ) diff --git a/my_report_specs/my_profile_report_specs.json b/my_report_specs/my_profile_report_specs.json index bf6d3cf4..73f18b1f 100644 --- a/my_report_specs/my_profile_report_specs.json +++ b/my_report_specs/my_profile_report_specs.json @@ -1,10 +1,4 @@ { - "//_comment1": "PDX-License-Identifier: Apache-2.0", - "//_comment2": "Copyright Contributors to the ODPi Egeria project.", - "//_comment3": " ", - "//_comment4": " This file provides the custom report specs for My Profile Textual App. ", - "//_comment5": " Author: Peter A Coldicott ", - "My-User-Peers": { "target_type": "Peer", "heading": "Peers", @@ -42,6 +36,10 @@ } ] } - ] + ], + "action": { + "function": "classification_manager.get_elements", + "metadataElementTypeName": ["Peer", "PeerProperties"] + } } } \ No newline at end of file From 121c9b200b934571200a55e556e1d669943e5d6b Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:05:08 -0500 Subject: [PATCH 06/19] start of week clean up and synch Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- config/config.json | 7 +- .../DemoCode/Experimental Code/my_network.py | 26 +- .../related_elements_return_data.json | 252 ++++++++++++++++++ my_report_specs/my_profile_report_specs.json | 8 +- .../test_classification_manager.py | 5 +- 5 files changed, 279 insertions(+), 19 deletions(-) create mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/related_elements_return_data.json diff --git a/config/config.json b/config/config.json index 08e0ce2e..d41ff2e2 100644 --- a/config/config.json +++ b/config/config.json @@ -29,7 +29,7 @@ "Pyegeria User Report Specs Dir": "~/.pyegeria/report_specs", "Pyegeria Report Spec Modules": [ "pyegeria.view.analytic_demo_specs:get_analytic_demo_specs", - "~/my_report_specs/my_profile_report_specs.json" + "/Users/petercoldicott/PycharmProjects/egeria/egeria-workspaces/egeria-python/my_report_specs/my_profile_report_specs.json" ] }, "feature_x_enabled": false, @@ -43,7 +43,7 @@ "console_logging_level": "ERROR", "enable_logging": true, "file_logging_level": "WARNING", - "log_directory": "/Users/dwolfson/localGit/egeria-v5-3/egeria-python/logs", + "log_directory": "/Users/petercoldicott/PycharmProjects/egeria/egeria-workspaces/egeria-python/logs", "logging_console_format": "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}", "logging_file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}-{function}:{line}-{message}-{extra}" }, @@ -52,6 +52,5 @@ "Egeria Home Glossary Name": "Egeria-Markdown", "Egeria Local Qualifier": "myLocal", "Egeria Usage Level": "Basic" - }, - + } } \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py index 3eadf97b..b6651c10 100644 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py @@ -9,7 +9,6 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any -import asyncio from textual import on from textual.app import App, ComposeResult @@ -26,6 +25,9 @@ if str(current_dir) not in sys.path: sys.path.append(str(current_dir)) +import os +os.environ.setdefault("PYEGERIA_CONFIG_DIRECTORY", str(root_path / "config")) + from pyegeria import ( load_app_config, settings, @@ -56,7 +58,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.title = "My_Network" self.sub_title = "Egeria network of contacts for the current user" self.description = "Display the user's network of (Egeria) contacts." - load_app_config() + app_config = settings.Environment self.log(f"Application Config: {app_config}") app_user = settings.User_Profile @@ -79,11 +81,11 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def on_mount(self) -> None: """Perform actions when the application is mounted.""" - self.my_network.add_column("Nickname", key="nickname") - self.my_network.add_column("User ID", key="user_id") - self.my_network.add_column("GUID", key="guid") + self.my_network.add_column("Label", key="label") + self.my_network.add_column("Scope", key="scope") + self.my_network.add_column("GUID", key="GUID") self.my_network.add_column("Qualified Name", key="qualified_name") - self.my_network.add_column("Notes", key="notes") + self.my_network.add_column("Description", key="description") self.my_network.cursor_type="row" self.my_network.zebra_stripes=True try: @@ -116,7 +118,9 @@ def on_mount(self) -> None: peers = exec_report_spec( format_set_name="My-User-Peers", output_format="DICT", - ) + params={"element_guid": self.user_GUID}, + token = token + ) self.log(f"Peers: {peers}") except PyegeriaException as e: self.log(f"Error creating peer relationship: {e}") @@ -125,13 +129,13 @@ def on_mount(self) -> None: finally: if eclient: eclient.close_session() - for peer in peers: + for peer in peers.get("data", []): self.log(f"Peer: {peer}") self.my_network.add_row(peer.get("Label"), - peer.get("uniqueName"), - peer.get("guid"), + peer.get("Scope"), + peer.get("GUID"), peer.get("Qualified Name"), - peer.get("relationshipProperties", "description")) + peer.get("Description")) continue self.my_network.refresh() container=self.query_one("#users_network_container", ScrollableContainer) diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/related_elements_return_data.json b/my_egeria/my_egeria/DemoCode/Experimental Code/related_elements_return_data.json new file mode 100644 index 00000000..df4798a0 --- /dev/null +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/related_elements_return_data.json @@ -0,0 +1,252 @@ +{ + "class": "OpenMetadataRootElement", + "elementHeader": { + "class": "ElementHeader", + "headerVersion": 0, + "status": "ACTIVE", + "type": { + "typeId": "ac406bf8-e53e-49f1-9088-2af28bbbd285", + "typeName": "Person", + "superTypeNames": [ + "ActorProfile", + "Actor", + "Referenceable", + "OpenMetadataRoot" + ], + "typeVersion": 2, + "typeDescription": "An individual.", + "typeCategory": "ENTITY_DEF" + }, + "origin": { + "sourceServer": "qs-metadata-store", + "originCategory": "CONTENT_PACK", + "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", + "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", + "license": "Apache 2.0" + }, + "versions": { + "createdBy": "Egeria Project", + "updatedBy": "Egeria Project", + "maintainedBy": [ + "Egeria Project" + ], + "createTime": "2021-12-20T07:20:40.038+00:00", + "updateTime": "2021-12-20T07:20:40.038+00:00", + "version": 1787846084192 + }, + "guid": "8ba14282-6c51-48fa-bc68-fd241b19cad1", + "anchor": { + "class": "ElementClassification", + "headerVersion": 0, + "status": "ACTIVE", + "type": { + "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", + "typeName": "Anchors", + "typeVersion": 1, + "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", + "typeCategory": "CLASSIFICATION_DEF" + }, + "origin": { + "sourceServer": "qs-metadata-store", + "originCategory": "CONTENT_PACK", + "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", + "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" + }, + "versions": { + "createdBy": "Egeria Project", + "updatedBy": "Egeria Project", + "maintainedBy": [ + "Egeria Project" + ], + "createTime": "2021-12-20T07:20:40.038+00:00", + "updateTime": "2021-12-20T07:20:40.038+00:00", + "version": 1787846084192 + }, + "classificationOrigin": "ASSIGNED", + "classificationName": "Anchors", + "classificationProperties": { + "class": "AnchorsProperties", + "typeName": "Anchors", + "anchorTypeName": "Person", + "anchorDomainName": "Actor" + } + } + }, + "contactDetails": [ + { + "class": "RelatedMetadataElementSummary", + "relationshipHeader": { + "class": "ElementHeader", + "headerVersion": 0, + "status": "ACTIVE", + "type": { + "typeId": "6cb9af43-184e-4dfa-854a-1572bcf0fe75", + "typeName": "ContactThrough", + "typeVersion": 2, + "typeDescription": "he contact details associated with an actor profile.", + "typeCategory": "RELATIONSHIP_DEF" + }, + "origin": { + "sourceServer": "qs-metadata-store", + "originCategory": "CONTENT_PACK", + "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", + "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", + "license": "Apache 2.0" + }, + "versions": { + "createdBy": "Egeria Project", + "updatedBy": "Egeria Project", + "maintainedBy": [ + "Egeria Project" + ], + "createTime": "2021-12-20T07:20:40.038+00:00", + "updateTime": "2021-12-20T07:20:40.038+00:00", + "version": 1787846084192 + }, + "guid": "9758d8cb-e9a0-481e-aa5c-1aa21459d6dc" + }, + "relatedElement": { + "elementHeader": { + "class": "ElementHeader", + "headerVersion": 0, + "status": "ACTIVE", + "type": { + "typeId": "79296df8-645a-4ef7-a011-912d1cdcf75a", + "typeName": "ContactDetails", + "superTypeNames": [ + "Referenceable", + "OpenMetadataRoot" + ], + "typeVersion": 1, + "typeDescription": "Information on how to send a message to an individual or automated process.", + "typeCategory": "ENTITY_DEF" + }, + "origin": { + "sourceServer": "qs-metadata-store", + "originCategory": "CONTENT_PACK", + "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", + "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", + "license": "Apache 2.0" + }, + "versions": { + "createdBy": "Egeria Project", + "updatedBy": "Egeria Project", + "maintainedBy": [ + "Egeria Project" + ], + "createTime": "2021-12-20T07:20:40.038+00:00", + "updateTime": "2021-12-20T07:20:40.038+00:00", + "version": 1787846084192 + }, + "guid": "1e9e83c8-7d70-42f6-a49c-0b093ccdb807", + "anchor": { + "class": "ElementClassification", + "headerVersion": 0, + "status": "ACTIVE", + "type": { + "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", + "typeName": "Anchors", + "typeVersion": 1, + "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", + "typeCategory": "CLASSIFICATION_DEF" + }, + "origin": { + "sourceServer": "qs-metadata-store", + "originCategory": "CONTENT_PACK", + "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", + "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" + }, + "versions": { + "createdBy": "Egeria Project", + "updatedBy": "Egeria Project", + "maintainedBy": [ + "Egeria Project" + ], + "createTime": "2021-12-20T07:20:40.038+00:00", + "updateTime": "2021-12-20T07:20:40.038+00:00", + "version": 1787846084192 + }, + "classificationOrigin": "ASSIGNED", + "classificationName": "Anchors", + "classificationProperties": { + "class": "AnchorsProperties", + "typeName": "Anchors", + "anchorGUID": "8ba14282-6c51-48fa-bc68-fd241b19cad1", + "anchorTypeName": "Person", + "anchorDomainName": "Actor" + } + } + }, + "properties": { + "class": "ContactDetailsProperties", + "typeName": "ContactDetails", + "displayName": "Work Email Address", + "contactType": "Work Email", + "contactMethodType": "EMAIL", + "contactMethodService": "Email address assigned by Coco Pharmaceuticals.", + "contactMethodValue": "TessaTube@Coco-Pharmaceuticals.org" + } + }, + "relatedElementAtEnd1": false + } + ], + "relatedBy": { + "relationshipHeader": { + "class": "ElementHeader", + "headerVersion": 0, + "status": "ACTIVE", + "type": { + "typeId": "4a316abe-bccd-4d11-ad5a-4bfb4079b80b", + "typeName": "Peer", + "typeVersion": 2, + "typeDescription": "Relationship identifying a person's peer network.", + "typeCategory": "RELATIONSHIP_DEF" + }, + "origin": { + "sourceServer": "qs-metadata-store", + "originCategory": "LOCAL_COHORT", + "homeMetadataCollectionId": "9905c3cb-94c5-4494-9229-0d6f69c0b842", + "homeMetadataCollectionName": "qs-metadata-store" + }, + "versions": { + "createdBy": "tessatube", + "updatedBy": "tessatube", + "maintainedBy": [ + "tessatube" + ], + "createTime": "2026-10-06T17:42:03.814+00:00", + "updateTime": "2026-10-06T17:42:03.814+00:00", + "version": 1, + "lastRequestId": "4a59ea13-a10f-419d-a0d2-8ad50cdc94aa" + }, + "guid": "0e9c5f02-8797-4716-ba2d-945256092924" + }, + "relationshipProperties": { + "class": "PeerProperties", + "typeName": "Peer", + "description": "Testing Peers" + } + }, + "properties": { + "class": "PersonProperties", + "typeName": "Person", + "qualifiedName": "Person::USA::302145", + "displayName": "Tessa Tube", + "additionalProperties": { + "workLocation": "3", + "countryCode": "USA", + "organization": "Coco Pharmaceuticals Ltd", + "jobLevel": "7" + }, + "courtesyTitle": "Dr", + "initials": "T", + "givenNames": "Tessa", + "surname": "Tube", + "fullName": "Dr Tessa Tube", + "pronouns": "She/her/hers", + "jobTitle": "Lead Researcher and head of Salk Research Lab", + "employeeNumber": "302145", + "employeeType": "FTP" + }, + "mermaidGraph": "---\ntitle: Person - Tessa Tube [8ba14282-6c51-48fa-bc68-fd241b19cad1]\n---\nflowchart TD\n%%{init: {\"flowchart\": {\"htmlLabels\": false}} }%%\n\n1@{ shape: rounded, label: \"*Person*\n**Tessa Tube**\"}\n2@{ shape: rounded, label: \"*Contact Details*\n**Work Email Address**\"}\n1==>|\"Contact Through\"|2\nstyle 1 color:#000000, fill:#e0ab18, stroke:#004563\nstyle 2 color:#000000, fill:#39add1, stroke:#004563\n" + } \ No newline at end of file diff --git a/my_report_specs/my_profile_report_specs.json b/my_report_specs/my_profile_report_specs.json index 73f18b1f..84eaf42f 100644 --- a/my_report_specs/my_profile_report_specs.json +++ b/my_report_specs/my_profile_report_specs.json @@ -38,8 +38,12 @@ } ], "action": { - "function": "classification_manager.get_elements", - "metadataElementTypeName": ["Peer", "PeerProperties"] + "function": "ClassificationExplorer.get_related_elements", + "required_params": ["element_guid"], + "spec_params": { "relationship_type": "ResourceList", + "property_value": "Catalog Resource", + "property_names" : ["resourceUse"] + } } } } \ No newline at end of file diff --git a/tests/functional-tests/test_classification_manager.py b/tests/functional-tests/test_classification_manager.py index 1ae4762d..ccc2c897 100644 --- a/tests/functional-tests/test_classification_manager.py +++ b/tests/functional-tests/test_classification_manager.py @@ -590,14 +590,14 @@ def test_get_all_related_elements(): def test_get_related_elements(): # metadata_element_type = 'CertificationType' # element_guid = '2ce8d10d-08a2-4fca-b0c2-1d5a335d00fc' - element_guid = "acca7cf3-40d4-40fd-9922-5c74075e9651" + element_guid = "79bd39ed-6048-4731-85c4-3cde50f99a30" # metadata_element_type = "Organization" # metadata_element_type = "CSVFile" # metadata_element_type = "InformationSupplyChain" open_metadata_type_name = "Referenceable" # element_guid = "8dca6e76-d454-4344-9c93-faa837a1a898" # relationship_type = "DataContentForDataSet" - relationship_type = "CollectionMembership" + relationship_type = "Peer" # relationship_type = "InformationSupplyChainComposition" c_client = ClassificationExplorer(view_server, platform_url) try: @@ -605,6 +605,7 @@ def test_get_related_elements(): body = { "class": "ResultsRequestBody", "metadataElementTypeName": open_metadata_type_name, + "includeOnlyRelationships": ["ContactThrough"] } response = c_client.get_related_elements( element_guid, relationship_type, body=body From 9f2782ff00cffe6fc06abf2d55a6542aa932e6c5 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:53:08 -0500 Subject: [PATCH 07/19] feat(my_profile): add My Network option for managing peers Merge the experimental my_network.py functionality into the My Profile app as a "My Network" option: list the user's peers, search for people and add them, view a peer's contact methods and communities, and remove a peer (with confirmation) via unlink_peer_person. - network_handler.py: NetworkMixin doing the Egeria calls - MyNetworkScreen.py: network screen and remove-confirmation dialog - my_network.py: add Remove from Network button with confirmation, hide stale details after removal - my_profile_report_specs.json: fix invalid JSON, query the Peer relationship, and show the person's GUID - tests for the mixin, screen and option routing Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../DemoCode/Experimental Code/my_network.py | 348 ++++++++++++++---- .../related_elements_return_data.json | 2 +- .../DemoCode/My_Profile/MainScreen.py | 1 + .../DemoCode/My_Profile/MyNetworkScreen.py | 272 ++++++++++++++ .../DemoCode/My_Profile/my_profile.tcss | 44 ++- .../DemoCode/My_Profile/my_profile_app.py | 5 +- .../DemoCode/My_Profile/network_handler.py | 186 ++++++++++ my_report_specs/my_profile_report_specs.json | 35 +- .../my_profile/test_my_profile_app.py | 9 + .../my_profile/test_network_handler.py | 267 ++++++++++++++ 10 files changed, 1061 insertions(+), 108 deletions(-) create mode 100644 my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py create mode 100644 my_egeria/my_egeria/DemoCode/My_Profile/network_handler.py create mode 100644 tests/micro-tests/my_profile/test_network_handler.py diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py index b6651c10..2fe435aa 100644 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py @@ -13,8 +13,10 @@ from textual import on from textual.app import App, ComposeResult from textual.binding import Binding -from textual.containers import ScrollableContainer -from textual.widgets import Header, Footer, Static, Placeholder, Input, DataTable, Button +from textual.containers import ScrollableContainer, Grid +from textual.css.query import NoMatches +from textual.screen import ModalScreen +from textual.widgets import Header, Footer, Static, Placeholder, Input, DataTable, Button, Label # Add the project root to sys.path to allow running this script from any directory root_path = Path(__file__).resolve().parents[4] @@ -37,6 +39,61 @@ exec_report_spec, Egeria, ) +class ConfirmRemoveScreen(ModalScreen[bool]): + """Modal dialog asking the user to confirm removing a peer from their network. + Dismisses with True if the user confirms, False otherwise.""" + + DEFAULT_CSS = """ + ConfirmRemoveScreen { + align: center middle; + } + #confirm_dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: thick $background 80%; + background: $surface; + } + #confirm_question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; + } + #confirm_dialog Button { + width: 100%; + } + """ + + BINDINGS = [("escape", "cancel", "Cancel")] + + def __init__(self, question: str) -> None: + super().__init__() + self.question = question + + def compose(self) -> ComposeResult: + yield Grid( + Label(self.question, id="confirm_question"), + Button("Remove", variant="error", id="confirm_remove"), + Button("Cancel", variant="primary", id="confirm_cancel"), + id="confirm_dialog", + ) + + @on(Button.Pressed, "#confirm_remove") + def handle_confirm(self, event: Button.Pressed) -> None: + self.dismiss(True) + + @on(Button.Pressed, "#confirm_cancel") + def handle_cancel(self, event: Button.Pressed) -> None: + self.dismiss(False) + + def action_cancel(self) -> None: + self.dismiss(False) + + class MyNetworkApp(App): """My Network App. @@ -78,16 +135,21 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.nickname = "" self.description = "" self.selected_network_member = None + self.peer_qname = None, + self.peer_dname = None, + self.peer_fname = None, + self.peer_pronouns = None, + self.peer_guid = None, + self.peer_description = None + # GUID of the peer whose details are currently displayed (get_peer_data resets self.peer_guid) + self.details_peer_guid = None def on_mount(self) -> None: """Perform actions when the application is mounted.""" - self.my_network.add_column("Label", key="label") - self.my_network.add_column("Scope", key="scope") - self.my_network.add_column("GUID", key="GUID") - self.my_network.add_column("Qualified Name", key="qualified_name") - self.my_network.add_column("Description", key="description") + self.my_network.add_columns("Qualified Name", "Display Name", "Full Name", "Pronouns", "GUID", "Description") self.my_network.cursor_type="row" self.my_network.zebra_stripes=True + error_flag = 0 try: eclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) @@ -96,31 +158,39 @@ def on_mount(self) -> None: except PyegeriaException as e: self.log(f"Error: {e}") self.notify(f"Error, unable to retrieve user profile: {e}", severity="error", timeout=20) - self.exit(400) + error_flag = 1 finally: if eclient: eclient.close_session() - if self.my_profile: - if isinstance(self.my_profile, list): - self.user_GUID = self.my_profile[0].get("GUID") or None - else: - self.notify(f"User profile is not a list: {self.my_profile}", severity="error", timeout=20) + if error_flag == 1: + # Use the error_flag to see if there was an error and if so exit self.exit(400) + if isinstance(self.my_profile, list): + self.user_GUID = self.my_profile[0].get("GUID") or None else: - self.notify(f"User profile not found: {self.user_name}", severity="error", timeout=20) + self.notify(f"User profile is not a list: {self.my_profile}", severity="error", timeout=20) self.exit(400) + self.get_peer_data() + return - # Get existing peer relationship list + def get_peer_data(self): + # clear out DataTable + self.my_network.clear() + # Get existing peer relationship list try: eclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - peers = exec_report_spec( - format_set_name="My-User-Peers", - output_format="DICT", - params={"element_guid": self.user_GUID}, - token = token - ) + open_metadata_type_name = "Referenceable" + relationship_type = "Peer" + body = { + "class": "ResultsRequestBody", + "metadataElementTypeName": open_metadata_type_name, + "includeOnlyRelationships": ["ContactThrough"] + } + peers = eclient.get_related_elements(element_guid=self.user_GUID, + relationship_type=relationship_type, + body=body) self.log(f"Peers: {peers}") except PyegeriaException as e: self.log(f"Error creating peer relationship: {e}") @@ -129,22 +199,61 @@ def on_mount(self) -> None: finally: if eclient: eclient.close_session() - for peer in peers.get("data", []): - self.log(f"Peer: {peer}") - self.my_network.add_row(peer.get("Label"), - peer.get("Scope"), - peer.get("GUID"), - peer.get("Qualified Name"), - peer.get("Description")) - continue + if peers != "No elements found": + for peer in peers: + self.log(f"Peer: {peer}") + peer_properties = peer.get("properties", None) + self.log(f"Peer Properties: {peer_properties}") + peer_related_by = peer.get("relatedBy", None) + self.log(f"Peer Related By: {peer_related_by}") + peer_relationship_properties = peer.get("relationshipProperties", None) + self.log(f"Peer Relationship Properties: {peer_relationship_properties}") + if peer_properties is not None: + if peer_properties.get("class") == "PersonProperties" and peer_properties.get("typeName") == "Person": + self.peer_qname = peer_properties.get("qualifiedName") + self.peer_dname = peer_properties.get("displayName") + self.peer_fname = peer_properties.get("fullName") + self.peer_pronouns = peer_properties.get("pronouns") + # the peer's own GUID is in its elementHeader - relatedBy describes the Peer relationship + self.peer_guid = (peer.get("elementHeader") or {}).get("guid") + if peer_relationship_properties is not None: + self.peer_description = peer_relationship_properties.get("description") + self.my_network.add_row(self.peer_qname, + self.peer_dname, + self.peer_fname, + self.peer_pronouns, + self.peer_guid, + self.peer_description) + self.my_network.cursor_type = "row" + # reset variables + self.peer_qname = None + self.peer_dname = None + self.peer_fname = None + self.peer_pronouns = None + self.peer_guid = None + self.peer_description = None + continue + continue + else: + self.notify(f"No peers found for user: {self.user_name}", timeout=15, severity="error") self.my_network.refresh() container=self.query_one("#users_network_container", ScrollableContainer) container.remove_children("#users_network_placeholder") container.mount(self.my_network) - container.mount(Button(label="User Details", id="user_details1", variant="primary")) - container.refresh() - self.my_network.refresh() - return + try: + self.query_one("#user_details1", Button) + except NoMatches: + self.log("Button 1 not found") + try: self.query_one("#user_details2", Button) + except NoMatches: + self.log("Button 2 not found") + container.mount(Button(label="User Details", id="user_details1", variant="primary")) + try: + self.query_one("#remove_peer_button", Button) + except NoMatches: + container.mount(Button(label="Remove from Network", id="remove_peer_button", variant="error")) + # container.refresh() + # self.my_network.refresh() def compose(self) -> ComposeResult: yield Header(show_clock=True) @@ -156,6 +265,7 @@ def compose(self) -> ComposeResult: Static("Search for user, enter the (partial) name to search for and press return:"), Input(placeholder="Name", id="user_search_input"), id="users_input_container") + yield ScrollableContainer(id="user_details_container") yield Footer() @on(Input.Submitted, "#user_search_input") @@ -215,7 +325,7 @@ def search_users(self, user_name: str) -> Any: return user_found @on(DataTable.RowSelected, "#users_found") - def on_users_found_row_selected(self, event: DataTable.RowSelected) -> None: + def handle_users_found_row_selected(self, event: DataTable.RowSelected) -> None: self.log(f"User selected: {event.row_key}") self.row_selected = event.row_key row_data = self.users_found.get_row(self.row_selected) @@ -254,7 +364,6 @@ def handle_link_peer_button(self, event: Button.Pressed): """Handle the submission of the link peer button. Since there is no validity check and both NickName and Description are optional we link the peer person without further upfront validation """ - self.my_network.add_row(self.nickname, self.selected_user_id, self.selected_GUID, self.selected_qualified_name) #add link to peer self.log(f"Owner GUID: {self.user_GUID} Peer selected GUID: {self.selected_GUID}") now = datetime.now(timezone.utc).isoformat(timespec="milliseconds") @@ -293,51 +402,111 @@ def handle_link_peer_button(self, event: Button.Pressed): self.query_one("#users_input_container").mount( Static("Search for user, enter the (partial) name to search for and press return:"), Input(placeholder="Name", id="user_search_input") - ) + ) self.query_one("#users_network_container").mount( Static("Peer person network"), - self.my_network, - Button(label="User Details", id="user_details2", variant="primary") - ) + self.my_network + ) + # update the data table to display the updated list of peers + # (get_peer_data also mounts the User Details button if it isn't already present) + self.get_peer_data() return - @on(DataTable.RowHighlighted, "#user_network_table") - def handle_network_member_selected(self,event: DataTable.RowHighlighted): + @on(DataTable.RowHighlighted, "#my_network") + def handle_network_member_highlighted(self,event: DataTable.RowHighlighted): self.log(f"Row highlighted: {event.row_key}") self.selected_network_member = event.row_key + return - @on(DataTable.RowSelected, "#user_network_table") + @on(DataTable.RowSelected, "#my_network") def handle_network_member_selected(self, event: DataTable.RowSelected): self.log(f"Row selected: {event.row_key}") self.selected_network_member = event.row_key + return @on(Button.Pressed, "#user_details1") def handle_user_details_1_button(self, event: Button.Pressed): if self.selected_network_member: selected_row_data = self.my_network.get_row(self.selected_network_member) + self.log(f"Selected row data: {selected_row_data}") self.show_user_details(selected_row_data) return else: self.notify(f"Please select a network member to view details before using the Button.", severity="warning", timeout=10) return - @on(Button.Pressed, "#user_details2") - def handle_user_details_2_button(self, event: Button.Pressed): - if self.selected_network_member: - selected_row_data = self.my_network.get_row(self.selected_network_member) - self.show_user_details(selected_row_data) + @on(Button.Pressed, "#remove_peer_button") + def handle_remove_peer_button(self, event: Button.Pressed): + """Remove the Peer relationship between the current user and the highlighted network member.""" + if not self.selected_network_member: + self.notify(f"Please select a network member to remove before using the Button.", severity="warning", timeout=10) return - else: - self.notify(f"Please select a network member to view details before using the Button.", severity="warning", - timeout=10) + selected_row_data = self.my_network.get_row(self.selected_network_member) + self.log(f"Selected row data for removal: {selected_row_data}") + peer_dname = selected_row_data[1] + peer_guid = selected_row_data[4] + if not peer_guid: + self.notify(f"No GUID found for network member {peer_dname}", severity="error", timeout=10) return + def confirm_removal(confirmed: bool | None) -> None: + if confirmed: + self.remove_peer(peer_guid, peer_dname) + else: + self.notify(f"{peer_dname} was not removed from your network", severity="information", timeout=5) + + self.push_screen(ConfirmRemoveScreen(f"Remove {peer_dname} from your network?"), confirm_removal) + return + + def remove_peer(self, peer_guid: str, peer_dname: str) -> None: + """Unlink the peer from the current user's profile and refresh the network display.""" + body = { + "class": "DeleteRelationshipRequestBody" + } + self.log(f"Unlinking peer person: body {body} Owner GUID: {self.user_GUID} and Peer GUID: {peer_guid}") + eclient = None + try: + eclient = Egeria(self.view_server, + self.platform_url, + self.user_name, + self.user_password) + token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) + # same GUID order as link_peer_person: owner first, peer second + eclient.unlink_peer_person(self.user_GUID, + peer_guid, + body + ) + self.notify(f"{peer_dname} removed from your network") + except PyegeriaException as e: + self.log(f"Error unlinking peer person: {e}") + self.notify(f"Error removing {peer_dname} from your network: {e}", severity="error", timeout=15) + return + finally: + if eclient: + eclient.close_session() + # the removed row's key is no longer valid, so clear the selection before reloading the peers + self.selected_network_member = None + # hide the details panel if it is showing the peer that was just removed + if self.details_peer_guid == peer_guid: + self.query_one("#user_details_container", ScrollableContainer).display = False + self.details_peer_guid = None + self.get_peer_data() + return + def show_user_details(self, selected_row_data): """ Show both contact details and community membership details for the selected network member.""" - self.selected_network_member = selected_row_data - self.user_GUID = selected_row_data[2] - self.user_qname = selected_row_data[3] - self.user_nickname = selected_row_data[0] + # keep the peer's details separate from self.user_GUID, which is the logged-in user's own profile GUID + self.log(f"Selected row data: {selected_row_data}") + self.peer_qname = selected_row_data[0] + self.peer_dname = selected_row_data[1] + self.peer_fname = selected_row_data[2] + self.peer_pronouns = selected_row_data[3] + self.peer_guid = selected_row_data[4] + self.peer_description = selected_row_data[5] + self.log(f"Peer details: GUID={self.peer_guid}, qname={self.peer_qname}, full_name={self.peer_fname}, display_name={self.peer_dname}, pronouns={self.peer_pronouns}, description={self.peer_description}") + if not self.peer_guid: + self.notify(f"No GUID found for network member {self.peer_dname}", severity="error", timeout=10) + return #clear and set up the datatables self.my_user_contact_details.clear(columns=True) self.my_user_contact_details.add_columns("Name", "Method Type", "Contact Type", "Service", "Value", "GUID") @@ -347,7 +516,9 @@ def show_user_details(self, selected_row_data): self.my_user_communities.add_columns("Name", "Assignment Type", "Description", "GUID") self.my_user_communities.cursor_type = "row" self.my_user_communities.zebra_stripes=True - # retrieve the users contact information + # retrieve the peer's profile and extract contact methods and communities using the + # My-User report spec (whose detail specs are My-User-Contact-Detail / My-User-Communities-Detail) + eclient = None try: eclient=Egeria( self.view_server, @@ -355,36 +526,53 @@ def show_user_details(self, selected_row_data): self.user_name, self.user_password) token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) - self.user_contact_details = exec_report_spec( - format_set_name="My-User-Contact-Detail") - self.log(f"Retrieved user contact details: {self.user_contact_details}") + peer_profile = eclient.get_actor_profile_by_guid(self.peer_guid, output_format="JSON") + peer_details = eclient._generate_my_profile_output(peer_profile, output_format="DICT", + report_spec="My-User") + self.log(f"Retrieved peer profile details: {peer_details}") except Exception as e: - self.notify(f"Error retrieving user contact details: {str(e)}", severity="error", timeout=10) - return - try: - self.user_communities = exec_report_spec( - format_set_name="My-User-Communities-Detail") - self.log(f"Retrieved user community memberships: {self.user_communities}") - except Exception as e: - self.notify(f"Error retrieving user community memberships: {str(e)}", severity="error", timeout=10) + self.notify(f"Error retrieving details for {self.peer_dname}: {str(e)}", severity="error", timeout=10) return finally: if eclient: eclient.close_session() - if isinstance(self.user_contact_details, list): - for contact in self.user_contact_details: - self.my_user_contact_details.add_row(contact.get("Name"), - contact.get("Method Type"), - contact.get("Contact Type"), - contact.get("Service"), - contact.get("Value"), - contact.get("GUID")) - if isinstance(self.user_communities, list): - for community in self.user_communities: - self.my_user_communities.add_row(community.get("Name"), - community.get("Assignment Type"), - community.get("Desacription"), - community.get("GUID")) + if isinstance(peer_details, list) and peer_details: + peer_details = peer_details[0] + if not isinstance(peer_details, dict): + self.notify(f"No profile details found for {self.peer_dname}", severity="warning", timeout=10) + return + self.user_contact_details = peer_details.get("Contact Methods") or [] + self.user_communities = peer_details.get("Communities") or [] + self.log(f"User details: {self.user_contact_details}") + self.log(f"User communities: {self.user_communities}") + for contact in self.user_contact_details: + self.my_user_contact_details.add_row(contact.get("name"), + contact.get("contactMethodType"), + contact.get("contactType"), + contact.get("contactMethodService"), + contact.get("contactMethodValue"), + contact.get("guid")) + for community in self.user_communities: + self.my_user_communities.add_row(community.get("name"), + community.get("assignmentType"), + community.get("description"), + community.get("guid")) + # display the tables, mounting them the first time through + container = self.query_one("#user_details_container", ScrollableContainer) + container.display = True + self.details_peer_guid = self.peer_guid + try: + container.query_one("#user_details_heading", Static).update(f"Details for {self.peer_dname}") + except NoMatches: + container.mount(Static(f"Details for {self.peer_dname}", id="user_details_heading"), + Static("Contact Methods:"), + self.my_user_contact_details, + Static("Communities:"), + self.my_user_communities) + if not self.user_contact_details: + self.notify(f"No contact methods found for {self.peer_dname}", severity="information", timeout=10) + if not self.user_communities: + self.notify(f"No community memberships found for {self.peer_dname}", severity="information", timeout=10) return def main() -> None: diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/related_elements_return_data.json b/my_egeria/my_egeria/DemoCode/Experimental Code/related_elements_return_data.json index df4798a0..5652e33a 100644 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/related_elements_return_data.json +++ b/my_egeria/my_egeria/DemoCode/Experimental Code/related_elements_return_data.json @@ -72,7 +72,7 @@ } } }, - "contactDetails": [ + "contactDetails":[ { "class": "RelatedMetadataElementSummary", "relationshipHeader": { diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py index 4184fe39..9b183932 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py @@ -60,6 +60,7 @@ def compose(self) -> ComposeResult: Option("[] Subscriptions"), Option("[] Technology Types"), Option("[] User Bookmarks"), + Option("[] My Network"), Option("[] Leave Feedback"), Option("[] Feedback Log"), id="other_function_list" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py new file mode 100644 index 00000000..b2f42a50 --- /dev/null +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py @@ -0,0 +1,272 @@ +""" + PDX-License-Identifier: Apache-2.0 + Copyright Contributors to the ODPi Egeria project. + + This file provides the My Network screen for My Profile. + +""" +from textual import on +from textual.app import ComposeResult +from textual.containers import ScrollableContainer, Container, Horizontal +from textual.screen import ModalScreen +from textual.widgets import DataTable, Header, Static, Footer, Input, Button + + +class ConfirmRemovePeerScreen(ModalScreen[bool]): + """Confirmation prompt shown before a person is removed from the user's network. + + Dismisses with True to proceed with the removal, False to cancel. + """ + + BINDINGS = [("escape", "cancel", "Cancel")] + + CSS_PATH = "my_profile.tcss" + + def __init__(self, peer_name: str, *args, **kwargs): + super().__init__(*args, **kwargs, id="confirm_remove_peer_screen") + self.peer_name = peer_name + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + yield Static("Confirm Removal", classes="span-3", id="confirm_remove_peer_title") + yield ScrollableContainer( + Static(f"Remove {self.peer_name} from your network?"), + Static("Their profile is not changed, only the link between you."), + Horizontal( + Button("Remove", id="confirm_remove_peer_btn", variant="error"), + Button("Cancel", id="cancel_remove_peer_btn", variant="primary"), + ), + id="confirm_remove_peer_container", + ) + yield Footer() + + def on_mount(self) -> None: + self.title = "Egeria - my_profile" + self.sub_title = "Confirm Removal - My Network" + + @on(Button.Pressed, "#confirm_remove_peer_btn") + def handle_confirm_button(self, event: Button.Pressed) -> None: + self.dismiss(True) + + @on(Button.Pressed, "#cancel_remove_peer_btn") + def handle_cancel_button(self, event: Button.Pressed) -> None: + self.dismiss(False) + + def action_cancel(self) -> None: + self.dismiss(False) + + +class MyNetworkScreen(ModalScreen): + """List the current user's network of peers, add people to it and remove them. + + The Egeria work is done by the app's NetworkMixin (list_my_peers, search_people, + link_peer, unlink_peer, get_peer_details); this screen only drives it. + """ + + BINDINGS = [ + ("q", "quit", "Quit"), + ("v", "view_details", "View Highlighted Person"), + ("d", "remove_peer", "Remove Highlighted Person"), + ] + + CSS_PATH = "my_profile.tcss" + + def __init__(self, my_peers, *args, **kwargs): + super().__init__(id="my_network_screen", *args, **kwargs) + self.title = "Egeria" + self.sub_title = "My Network" + # Rows of (qualified name, display name, full name, pronouns, GUID, description) + self.my_peers_data = list(my_peers or []) + # (display name, GUID) of the person picked from the search results to add + self.selected_person = None + # GUID of the peer whose details are currently displayed + self.details_peer_guid = None + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + yield Static("My Network", classes="span-3", id="my_network_title") + yield ScrollableContainer( + Static("The people in your network. 'v' shows the highlighted person's details; 'd' removes them."), + DataTable(id="my_network_table", zebra_stripes=True, cursor_type="row"), + Horizontal( + Button("Person Details", id="peer_details_btn", variant="primary"), + Button("Remove from Network", id="remove_peer_btn", variant="error"), + id="my_network_buttons", + ), + id="my_network_container", + ) + yield ScrollableContainer( + Static("Add to your network: enter a (partial) name and press return"), + Input(placeholder="Name", id="people_search_input"), + DataTable(id="people_found_table", zebra_stripes=True, cursor_type="row"), + Container(id="add_peer_container"), + id="people_search_container", + ) + yield ScrollableContainer( + Static("", id="peer_details_heading"), + Static("Contact Methods:"), + DataTable(id="peer_contacts_table", zebra_stripes=True, cursor_type="row"), + Static("Communities:"), + DataTable(id="peer_communities_table", zebra_stripes=True, cursor_type="row"), + id="peer_details_container", + ) + yield Footer() + + def on_mount(self) -> None: + self.query_one("#my_network_table", DataTable).add_columns( + "Qualified Name", "Display Name", "Full Name", "Pronouns", "GUID", "Description") + self.query_one("#people_found_table", DataTable).add_columns("Name", "GUID", "Qualified Name") + self.query_one("#peer_contacts_table", DataTable).add_columns( + "Name", "Method Type", "Contact Type", "Service", "Value", "GUID") + self.query_one("#peer_communities_table", DataTable).add_columns( + "Name", "Assignment Type", "Description", "GUID") + self.query_one("#peer_details_container").display = False + self._fill_table() + + def _fill_table(self) -> None: + table = self.query_one("#my_network_table", DataTable) + table.clear() + for row in self.my_peers_data: + table.add_row(*row, key=row[4]) + if not self.my_peers_data: + self.notify("There is no-one in your network yet - search for someone to add them", timeout=5) + + def _reload(self) -> None: + rows = self.app.list_my_peers() + if rows is not None: + self.my_peers_data = rows + self._fill_table() + + def _highlighted_peer(self) -> tuple[str, str] | None: + """(display name, GUID) of the highlighted network row, or None if the network is empty.""" + table = self.query_one("#my_network_table", DataTable) + if table.row_count == 0: + self.notify("There is no-one in your network to select", timeout=5, severity="warning") + return None + row = table.get_row_at(table.cursor_row) + return row[1] or row[0], row[4] + + def action_quit(self) -> None: + """ The user elects to quit the network function """ + self.dismiss(200) + + @on(Button.Pressed, "#peer_details_btn") + def handle_peer_details_button(self, event: Button.Pressed) -> None: + self.action_view_details() + + def action_view_details(self) -> None: + """ Show the contact methods and communities of the highlighted peer """ + peer = self._highlighted_peer() + if not peer: + return + peer_name, peer_guid = peer + details = self.app.get_peer_details(peer_guid) + if details is None: + return + contacts, communities = details + contacts_table = self.query_one("#peer_contacts_table", DataTable) + contacts_table.clear() + for contact in contacts: + contacts_table.add_row(contact.get("name"), + contact.get("contactMethodType"), + contact.get("contactType"), + contact.get("contactMethodService"), + contact.get("contactMethodValue"), + contact.get("guid")) + communities_table = self.query_one("#peer_communities_table", DataTable) + communities_table.clear() + for community in communities: + communities_table.add_row(community.get("name"), + community.get("assignmentType"), + community.get("description"), + community.get("guid")) + self.query_one("#peer_details_heading", Static).update(f"[b]Details for {peer_name}[/b]") + self.query_one("#peer_details_container").display = True + self.details_peer_guid = peer_guid + if not contacts: + self.notify(f"No contact methods found for {peer_name}", timeout=10) + if not communities: + self.notify(f"No community memberships found for {peer_name}", timeout=10) + + @on(Button.Pressed, "#remove_peer_btn") + def handle_remove_peer_button(self, event: Button.Pressed) -> None: + self.action_remove_peer() + + def action_remove_peer(self) -> None: + """ The user wants to remove the highlighted peer, after confirmation """ + peer = self._highlighted_peer() + if not peer: + return + peer_name, peer_guid = peer + + def on_confirm(confirmed: bool | None) -> None: + if not confirmed: + self.notify(f"{peer_name} was not removed from your network", timeout=5) + return + if self.app.unlink_peer(peer_guid): + # hide the details panel if it is showing the person just removed + if self.details_peer_guid == peer_guid: + self.query_one("#peer_details_container").display = False + self.details_peer_guid = None + self._reload() + + self.app.push_screen(ConfirmRemovePeerScreen(peer_name), callback=on_confirm) + + @on(Input.Submitted, "#people_search_input") + def handle_people_search(self, event: Input.Submitted) -> None: + search_string = event.value.strip() + if not search_string: + self.notify("Enter a (partial) name to search for", timeout=5, severity="warning") + return + found = self.app.search_people(search_string) + if found is None: + return + table = self.query_one("#people_found_table", DataTable) + table.clear() + in_network = {row[4] for row in self.my_peers_data} + for name, guid, qualified_name in found: + if guid not in in_network: + table.add_row(name, guid, qualified_name, key=guid) + if table.row_count == 0: + self.notify(f"No-one new found matching {search_string}", timeout=5, severity="warning") + else: + self.notify("Select a person to add them to your network", timeout=5) + table.focus() + + @on(DataTable.RowSelected, "#people_found_table") + def handle_person_selected(self, event: DataTable.RowSelected) -> None: + name, guid, qualified_name = event.data_table.get_row(event.row_key) + self.selected_person = (name, guid) + container = self.query_one("#add_peer_container", Container) + container.remove_children() + container.mount( + Static(f"Add [b]{name}[/b] ({qualified_name}) to your network"), + Input(placeholder="Nickname (optional)", id="peer_nickname_input"), + Input(placeholder="Description/comment about this relationship (optional)", id="peer_description_input"), + Horizontal( + Button("Add to Network", id="add_peer_btn", variant="primary"), + Button("Cancel", id="cancel_add_peer_btn", variant="warning"), + ), + ) + self.query_one("#peer_nickname_input", Input).focus() + + @on(Button.Pressed, "#add_peer_btn") + def handle_add_peer_button(self, event: Button.Pressed) -> None: + if not self.selected_person: + return + nickname = self.query_one("#peer_nickname_input", Input).value.strip() + description = self.query_one("#peer_description_input", Input).value.strip() + if self.app.link_peer(self.selected_person[1], nickname, description): + self._clear_search() + self._reload() + + @on(Button.Pressed, "#cancel_add_peer_btn") + def handle_cancel_add_peer_button(self, event: Button.Pressed) -> None: + self.query_one("#add_peer_container", Container).remove_children() + self.selected_person = None + + def _clear_search(self) -> None: + self.query_one("#add_peer_container", Container).remove_children() + self.query_one("#people_found_table", DataTable).clear() + self.query_one("#people_search_input", Input).clear() + self.selected_person = None diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss index 5793f19d..705c8252 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss @@ -62,11 +62,52 @@ FeedbackScreen, FeedbackLogScreen { background: $background; } +/* Title row, then network + search side by side, then the selected peer's details */ +MyNetworkScreen { + grid-rows: auto 1fr 1fr; + background: $background; +} + +#my_network_container { + column-span: 2; + width: 100%; + height: 100%; +} + +#people_search_container { + width: 100%; + height: 100%; +} + +#peer_details_container { + column-span: 3; + width: 100%; + height: 100%; +} + +#my_network_table { + width: 100%; + height: 1fr; +} + +#my_network_buttons, +#add_peer_container, +#add_peer_container Horizontal { + height: auto; +} + +#people_found_table, +#peer_contacts_table, +#peer_communities_table { + height: auto; + max-height: 12; +} + ModalScreen > Header, ModalScreen > Footer { column-span: 3; } -#edit_profile_title, #create_profile_title, #edit_teams_title, #edit_roles_title, #edit_communities_title, #edit_identities_title, #status_title, #generic_data_view_title, #feedback_title, #feedback_log_title, .span-3 { +#my_network_title, #confirm_remove_peer_title, #edit_profile_title, #create_profile_title, #edit_teams_title, #edit_roles_title, #edit_communities_title, #edit_identities_title, #status_title, #generic_data_view_title, #feedback_title, #feedback_log_title, .span-3 { column-span: 3; text-align: center; background: darkblue; @@ -96,6 +137,7 @@ ModalScreen > Header, ModalScreen > Footer { #edit_projects_container, #edit_todos_container, #confirm_delete_container, +#confirm_remove_peer_container, #status_container, #user_identities_container, #generic_data_view_container, diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py index 17fea454..2b3674bf 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py @@ -88,10 +88,11 @@ from feedback_handler import FeedbackMixin, FEEDBACK_LOG_OWNER from bookmarks_handler import BookmarksMixin, bookmarks_qualified_name from MyBookMarksScreen import MyBookMarksScreen +from network_handler import NetworkMixin class MyProfileApp(App, TechTypesMixin, ShopForDataMixin, TeamRolesMixin, ElementsCrudMixin, FeedbackMixin, - BookmarksMixin): + BookmarksMixin, NetworkMixin): """My Profile App. Retrieves a user's profile from Egeria and displays current work items. @@ -678,6 +679,8 @@ async def handle_option_selected(self, event: OptionList.OptionSelected) -> None ) elif selected_option == "User Bookmarks": self.show_my_bookmarks() + elif selected_option == "My Network": + self.show_my_network() elif selected_option == "Subscriptions": await self.push_screen(ViewSubscriptionsScreen(), callback=self.view_subscriptions_callback) elif selected_option == "Leave Feedback": diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/network_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/network_handler.py new file mode 100644 index 00000000..aa1a27d8 --- /dev/null +++ b/my_egeria/my_egeria/DemoCode/My_Profile/network_handler.py @@ -0,0 +1,186 @@ +""" + PDX-License-Identifier: Apache-2.0 + Copyright Contributors to the ODPi Egeria project. + + My Network handler mixin for My Profile Textual App. + + A user's network is the set of people linked to their profile by a Peer + relationship. Peers are found with find_actor_profiles, linked with + link_peer_person and removed with unlink_peer_person. +""" + +import sys +from pathlib import Path +from typing import Any + +# Ensure project root is on sys.path +root_path = Path(__file__).resolve().parents[4] +if str(root_path) not in sys.path: + sys.path.append(str(root_path)) + +current_dir = Path(__file__).resolve().parent +if str(current_dir) not in sys.path: + sys.path.append(str(current_dir)) + +from pyegeria import Egeria, PyegeriaException +from MyNetworkScreen import MyNetworkScreen + +PEER_RELATIONSHIP = "Peer" + + +class NetworkMixin: + """Mixin class providing the My Network feature for MyProfileApp.""" + + def _network_client(self) -> Egeria: + client = Egeria(view_server=self.view_server, platform_url=self.platform_url, + user_id=self.user_name, user_pwd=self.user_password) + client.create_egeria_bearer_token(self.user_name, self.user_password) + return client + + def list_my_peers(self) -> list[tuple[str, str, str, str, str, str]] | None: + """The user's peers as (qualified name, display name, full name, pronouns, GUID, description) rows, + or None if Egeria could not be reached.""" + client = None + try: + client = self._network_client() + body = { + "class": "ResultsRequestBody", + "metadataElementTypeName": "Referenceable", + "includeOnlyRelationships": ["ContactThrough"], + } + peers = client.get_related_elements(element_guid=self.user_GUID, + relationship_type=PEER_RELATIONSHIP, + body=body) + except PyegeriaException as e: + self.log(f"Retrieving peers failed: {e}") + self.notify(f"Could not retrieve your network: {e}", timeout=10, severity="error") + return None + finally: + if client: + client.close_session() + self.log(f"Peers: {peers}") + rows = [] + # "No elements found" is returned, rather than an exception, when the user has no peers + for peer in peers if isinstance(peers, list) else []: + properties = peer.get("properties") or {} + if properties.get("class") != "PersonProperties": + continue + # the peer's own GUID is in its elementHeader - relatedBy describes the Peer relationship + guid = (peer.get("elementHeader") or {}).get("guid") + if not guid: + continue + relationship_properties = peer.get("relationshipProperties") or {} + rows.append((properties.get("qualifiedName") or "", + properties.get("displayName") or "", + properties.get("fullName") or "", + properties.get("pronouns") or "", + guid, + relationship_properties.get("description") or "")) + return rows + + def show_my_network(self) -> None: + """Show the My Network screen for the current user.""" + if not getattr(self, "user_GUID", ""): + self.notify("Your profile has not loaded yet, so your network can't be shown", timeout=10, + severity="warning") + return + peers = self.list_my_peers() + if peers is None: + return + self.push_screen(MyNetworkScreen(peers), callback=self.my_network_callback) + + def my_network_callback(self, result: Any) -> None: + self.log(f"My Network screen returned: {result}") + + def search_people(self, search_string: str) -> list[tuple[str, str, str]] | None: + """People whose profile matches search_string, as (display name, GUID, qualified name) rows, + or None if Egeria could not be reached.""" + client = None + try: + client = self._network_client() + found = client.find_actor_profiles(search_string=search_string, ignore_case=True, + output_format="DICT", report_spec="User-Identities", + query_depth=0, page_size=10) + except PyegeriaException as e: + self.log(f"Searching for {search_string} failed: {e}") + self.notify(f"Searching for people failed: {e}", timeout=10, severity="error") + return None + finally: + if client: + client.close_session() + self.log(f"People found for {search_string}: {found}") + if isinstance(found, dict): + found = [found] + rows = [] + for person in found if isinstance(found, list) else []: + guid = person.get("GUID") + # the current user can't be their own peer + if guid and guid != self.user_GUID: + rows.append((person.get("Display Name") or "", guid, person.get("Qualified Name") or "")) + return rows + + def link_peer(self, peer_guid: str, nickname: str = "", description: str = "") -> bool: + """Add the person with the given GUID to the user's network. Returns True on success.""" + body = { + "class": "NewRelationshipRequestBody", + "properties": { + "class": "PeerProperties", + "peer_relationship_name": nickname, + "description": description, + "effective_from": None, + "effective_to": None, + } + } + client = None + try: + client = self._network_client() + client.link_peer_person(self.user_GUID, peer_guid, body) + self.notify("Added to your network", timeout=10, severity="information") + return True + except PyegeriaException as e: + self.log(f"Linking peer {peer_guid} failed: {e}") + self.notify(f"Adding to your network failed: {e}", timeout=10, severity="error") + return False + finally: + if client: + client.close_session() + + def unlink_peer(self, peer_guid: str) -> bool: + """Remove the person with the given GUID from the user's network. Returns True on success.""" + client = None + try: + client = self._network_client() + # same GUID order as link_peer_person: owner first, peer second + client.unlink_peer_person(self.user_GUID, peer_guid, {"class": "DeleteRelationshipRequestBody"}) + self.notify("Removed from your network", timeout=10, severity="information") + return True + except PyegeriaException as e: + self.log(f"Unlinking peer {peer_guid} failed: {e}") + self.notify(f"Removing from your network failed: {e}", timeout=10, severity="error") + return False + finally: + if client: + client.close_session() + + def get_peer_details(self, peer_guid: str) -> tuple[list[dict], list[dict]] | None: + """The peer's (contact methods, communities), or None if they could not be retrieved.""" + client = None + try: + client = self._network_client() + profile = client.get_actor_profile_by_guid(peer_guid, output_format="JSON") + # My-User's detail specs are My-User-Contact-Detail / My-User-Communities-Detail + details = client._generate_my_profile_output(profile, output_format="DICT", report_spec="My-User") + except PyegeriaException as e: + self.log(f"Retrieving details for peer {peer_guid} failed: {e}") + self.notify(f"Could not retrieve the details: {e}", timeout=10, severity="error") + return None + finally: + if client: + client.close_session() + self.log(f"Peer details: {details}") + if isinstance(details, list) and details: + details = details[0] + if not isinstance(details, dict): + self.notify("No profile details found", timeout=10, severity="warning") + return None + return details.get("Contact Methods") or [], details.get("Communities") or [] diff --git a/my_report_specs/my_profile_report_specs.json b/my_report_specs/my_profile_report_specs.json index 84eaf42f..cb88fd3d 100644 --- a/my_report_specs/my_profile_report_specs.json +++ b/my_report_specs/my_profile_report_specs.json @@ -6,33 +6,19 @@ "family": "MyProfile", "formats": [ { - "types": [ - "LIST", - "REPORT", - "DICT", - "TABLE" - ], + "types": ["LIST", "REPORT", "DICT", "TABLE"], "attributes": [ { - "name": "Label", - "key": "label" + "name": "Peer", + "key": "relatedBy.relationshipHeader.type.typeName" }, { - "name": "Qualified Name", - "key": "uniqueName" + "name": "guid", + "key": "elementHeader.guid" }, { - "name": "Scope", - "key": "scope" - }, - { - "name": "Description", - "key": "relationshipProperties.description" - }, - { - "name": "GUID", - "key": "guid", - "format": "True" + "name": "peerProperties", + "key": "relatedBy.relationshipProperties.class" } ] } @@ -40,10 +26,9 @@ "action": { "function": "ClassificationExplorer.get_related_elements", "required_params": ["element_guid"], - "spec_params": { "relationship_type": "ResourceList", - "property_value": "Catalog Resource", - "property_names" : ["resourceUse"] + "spec_params": { + "relationship_type": "Peer" } } } -} \ No newline at end of file +} diff --git a/tests/micro-tests/my_profile/test_my_profile_app.py b/tests/micro-tests/my_profile/test_my_profile_app.py index caf2f476..ce5d9b6d 100644 --- a/tests/micro-tests/my_profile/test_my_profile_app.py +++ b/tests/micro-tests/my_profile/test_my_profile_app.py @@ -256,6 +256,15 @@ async def test_handle_option_selected(self, option_name, handler_attr, screen_cl call_args = app.push_screen.call_args[0] assert isinstance(call_args[0], screen_cls) + @pytest.mark.asyncio + async def test_my_network_option_shows_network(self): + app = MyProfileApp() + app.show_my_network = MagicMock() + event = MagicMock() + event.option.prompt = "[] My Network" + await app.handle_option_selected(event) + app.show_my_network.assert_called_once_with() + def test_status_callback(self): """Closing a status screen returns to the main screen; it must not end the app.""" app = MyProfileApp() diff --git a/tests/micro-tests/my_profile/test_network_handler.py b/tests/micro-tests/my_profile/test_network_handler.py new file mode 100644 index 00000000..04b78245 --- /dev/null +++ b/tests/micro-tests/my_profile/test_network_handler.py @@ -0,0 +1,267 @@ +""" + PDX-License-Identifier: Apache-2.0 + Copyright Contributors to the ODPi Egeria project. + + Unit tests for the My Network mixin and screen (a user's network is the people + linked to their profile by a Peer relationship). +""" + +from unittest.mock import MagicMock, patch + +import pytest +from textual.app import App +from textual.widgets import DataTable, Input, Button + +from network_handler import NetworkMixin +from MyNetworkScreen import MyNetworkScreen, ConfirmRemovePeerScreen +from pyegeria import PyegeriaException + + +def _peer(guid, display_name, description="", type_class="PersonProperties"): + return {"elementHeader": {"guid": guid}, + "properties": {"class": type_class, "qualifiedName": f"Person::{guid}", "displayName": display_name, + "fullName": f"{display_name} Full", "pronouns": "they/them"}, + "relatedBy": {"relationshipHeader": {"guid": f"rel-{guid}"}}, + "relationshipProperties": {"description": description}} + + +class DummyApp(NetworkMixin): + def __init__(self): + self.view_server = "qs-view-server" + self.platform_url = "https://127.0.0.1:9443" + self.user_name = "garygeeke" + self.user_password = "secret" + self.user_GUID = "profile-guid" + self.notices = [] + self.pushed = [] + + def log(self, *args, **kwargs): + pass + + def notify(self, message, **kwargs): + self.notices.append((message, kwargs.get("severity"))) + + def push_screen(self, screen, callback=None): + self.pushed.append((screen, callback)) + + +@pytest.fixture +def client(): + with patch("network_handler.Egeria") as mock_egeria: + yield mock_egeria.return_value + + +class TestNetworkMixin: + def test_list_returns_peer_rows_with_person_guid(self, client): + client.get_related_elements.return_value = [ + _peer("erin-guid", "Erin", "Governance buddy"), + _peer("thing-guid", "Not a person", type_class="AssetProperties"), + ] + rows = DummyApp().list_my_peers() + assert client.get_related_elements.call_args.kwargs["element_guid"] == "profile-guid" + assert client.get_related_elements.call_args.kwargs["relationship_type"] == "Peer" + # The person's own GUID, not the relationship's; non-people are skipped + assert rows == [("Person::erin-guid", "Erin", "Erin Full", "they/them", "erin-guid", "Governance buddy")] + client.close_session.assert_called_once() + + def test_list_without_peers_is_empty(self, client): + client.get_related_elements.return_value = "No elements found" + assert DummyApp().list_my_peers() == [] + + def test_list_error_returns_none(self, client): + client.get_related_elements.side_effect = PyegeriaException("boom") + app = DummyApp() + assert app.list_my_peers() is None + assert app.notices[-1][1] == "error" + + def test_show_pushes_screen(self, client): + client.get_related_elements.return_value = [_peer("erin-guid", "Erin")] + app = DummyApp() + app.show_my_network() + screen, callback = app.pushed[-1] + assert isinstance(screen, MyNetworkScreen) + assert screen.my_peers_data[0][4] == "erin-guid" + + def test_show_without_profile_is_refused(self, client): + app = DummyApp() + app.user_GUID = "" + app.show_my_network() + assert app.pushed == [] + client.get_related_elements.assert_not_called() + assert app.notices[-1][1] == "warning" + + def test_search_excludes_current_user(self, client): + client.find_actor_profiles.return_value = [ + {"Display Name": "Tanya", "GUID": "tanya-guid", "Qualified Name": "Person::tanya"}, + {"Display Name": "Gary", "GUID": "profile-guid", "Qualified Name": "Person::gary"}, + ] + assert DummyApp().search_people("a") == [("Tanya", "tanya-guid", "Person::tanya")] + + def test_search_single_result_dict(self, client): + client.find_actor_profiles.return_value = {"Display Name": "Tanya", "GUID": "tanya-guid", + "Qualified Name": "Person::tanya"} + assert DummyApp().search_people("tan") == [("Tanya", "tanya-guid", "Person::tanya")] + + def test_search_error_returns_none(self, client): + client.find_actor_profiles.side_effect = PyegeriaException("boom") + assert DummyApp().search_people("tan") is None + + def test_link_peer(self, client): + assert DummyApp().link_peer("tanya-guid", "Tan", "Met at the summit") is True + owner, peer, body = client.link_peer_person.call_args.args + assert (owner, peer) == ("profile-guid", "tanya-guid") + assert body["properties"]["class"] == "PeerProperties" + assert body["properties"]["peer_relationship_name"] == "Tan" + assert body["properties"]["description"] == "Met at the summit" + + def test_link_peer_error(self, client): + client.link_peer_person.side_effect = PyegeriaException("boom") + app = DummyApp() + assert app.link_peer("tanya-guid") is False + assert app.notices[-1][1] == "error" + + def test_unlink_peer(self, client): + assert DummyApp().unlink_peer("erin-guid") is True + client.unlink_peer_person.assert_called_once_with( + "profile-guid", "erin-guid", {"class": "DeleteRelationshipRequestBody"}) + + def test_unlink_peer_error(self, client): + client.unlink_peer_person.side_effect = PyegeriaException("boom") + app = DummyApp() + assert app.unlink_peer("erin-guid") is False + assert app.notices[-1][1] == "error" + + def test_peer_details(self, client): + client._generate_my_profile_output.return_value = [ + {"Contact Methods": [{"name": "Work email"}], "Communities": [{"name": "Council"}]}] + assert DummyApp().get_peer_details("erin-guid") == ([{"name": "Work email"}], [{"name": "Council"}]) + client.get_actor_profile_by_guid.assert_called_once_with("erin-guid", output_format="JSON") + + def test_peer_details_missing(self, client): + client._generate_my_profile_output.return_value = "No elements found" + app = DummyApp() + assert app.get_peer_details("erin-guid") is None + assert app.notices[-1][1] == "warning" + + +PEER_ROWS = [("Person::erin", "Erin", "Erin Full", "she/her", "erin-guid", "Governance buddy"), + ("Person::peter", "Peter", "Peter Full", "he/him", "peter-guid", "")] + + +class NetworkHostApp(App): + """Host app standing in for MyProfileApp's network methods.""" + + def __init__(self, rows): + super().__init__() + self.rows = list(rows) + self.link_peer = MagicMock(side_effect=self._link) + self.unlink_peer = MagicMock(side_effect=self._unlink) + self.search_people = MagicMock(return_value=[("Tanya", "tanya-guid", "Person::tanya"), + ("Erin", "erin-guid", "Person::erin")]) + self.get_peer_details = MagicMock(return_value=([{"name": "Work email"}], [{"name": "Council"}])) + self.dismissed = None + + def _link(self, guid, nickname="", description=""): + self.rows.append(("Person::new", "New", "", "", guid, description)) + return True + + def _unlink(self, guid): + self.rows = [r for r in self.rows if r[4] != guid] + return True + + def list_my_peers(self): + return list(self.rows) + + async def on_mount(self): + self.screen_under_test = MyNetworkScreen(list(self.rows)) + await self.push_screen(self.screen_under_test, callback=lambda r: setattr(self, "dismissed", r)) + + +class TestMyNetworkScreen: + @pytest.mark.asyncio + async def test_lists_peers(self): + app = NetworkHostApp(PEER_ROWS) + async with app.run_test(): + table = app.screen_under_test.query_one("#my_network_table", DataTable) + assert table.row_count == 2 + assert table.get_row_at(0) == list(PEER_ROWS[0]) + assert app.screen_under_test.query_one("#peer_details_container").display is False + + @pytest.mark.asyncio + async def test_view_details_of_highlighted_peer(self): + app = NetworkHostApp(PEER_ROWS) + async with app.run_test() as pilot: + screen = app.screen_under_test + screen.query_one("#my_network_table", DataTable).move_cursor(row=1) + screen.action_view_details() + await pilot.pause() + app.get_peer_details.assert_called_once_with("peter-guid") + assert screen.query_one("#peer_details_container").display is True + assert screen.query_one("#peer_contacts_table", DataTable).row_count == 1 + assert screen.query_one("#peer_communities_table", DataTable).row_count == 1 + + @pytest.mark.asyncio + async def test_remove_asks_for_confirmation(self): + app = NetworkHostApp(PEER_ROWS) + async with app.run_test() as pilot: + screen = app.screen_under_test + screen.action_remove_peer() + await pilot.pause() + assert isinstance(app.screen, ConfirmRemovePeerScreen) + app.screen.query_one("#cancel_remove_peer_btn", Button).press() + await pilot.pause() + app.unlink_peer.assert_not_called() + assert screen.query_one("#my_network_table", DataTable).row_count == 2 + + @pytest.mark.asyncio + async def test_remove_confirmed_hides_that_peers_details(self): + app = NetworkHostApp(PEER_ROWS) + async with app.run_test() as pilot: + screen = app.screen_under_test + screen.action_view_details() + await pilot.pause() + screen.action_remove_peer() + await pilot.pause() + app.screen.query_one("#confirm_remove_peer_btn", Button).press() + await pilot.pause() + app.unlink_peer.assert_called_once_with("erin-guid") + assert screen.query_one("#my_network_table", DataTable).row_count == 1 + assert screen.query_one("#peer_details_container").display is False + + @pytest.mark.asyncio + async def test_remove_with_empty_network(self): + app = NetworkHostApp([]) + async with app.run_test() as pilot: + app.screen_under_test.action_remove_peer() + await pilot.pause() + assert app.screen is app.screen_under_test + + @pytest.mark.asyncio + async def test_search_and_add_peer(self): + app = NetworkHostApp(PEER_ROWS) + async with app.run_test() as pilot: + screen = app.screen_under_test + search = screen.query_one("#people_search_input", Input) + search.value = "a" + await search.action_submit() + await pilot.pause() + found = screen.query_one("#people_found_table", DataTable) + # Erin is already in the network so isn't offered again + assert found.row_count == 1 + found.focus() + await pilot.press("enter") + await pilot.pause() + screen.query_one("#peer_description_input", Input).value = "Met at the summit" + screen.query_one("#add_peer_btn", Button).press() + await pilot.pause() + app.link_peer.assert_called_once_with("tanya-guid", "", "Met at the summit") + assert screen.query_one("#my_network_table", DataTable).row_count == 3 + assert found.row_count == 0 + + @pytest.mark.asyncio + async def test_quit(self): + app = NetworkHostApp([]) + async with app.run_test() as pilot: + app.screen_under_test.action_quit() + await pilot.pause() + assert app.dismissed == 200 From 5f945d8ffb05931b0a13eebf1255e736d423162c Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:46:37 -0500 Subject: [PATCH 08/19] fix(my_profile): stop Technology Types "go back" from crashing TechnologyTypesScreen dismisses quit/go back with the strings "200"/"201", but tech_type_callback compared against the int 201. The back result was then treated as a tech type name, get_tech_type_detail returned the string "No elements found", and .get() on it raised AttributeError. - match "200"/"201" as either str or int and return to the main screen - if the tech type detail is not a dict, notify and return to the main screen instead of crashing - stop after showing the error screen when the detail lookup fails - use the Egeria client for get_tech_type_detail Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../DemoCode/My_Profile/tech_types_handler.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py index b4a66bb9..0b2284f4 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py @@ -20,7 +20,7 @@ if str(current_dir) not in sys.path: sys.path.append(str(current_dir)) -from pyegeria import AutomatedCuration, PyegeriaException, print_basic_exception, load_app_config, settings +from pyegeria import AutomatedCuration, PyegeriaException, print_basic_exception, load_app_config, settings, Egeria from StatusScreen import StatusScreen from TechnologyTypeScreens import ( TechnologyTypesScreen, @@ -64,6 +64,7 @@ async def tech_type_callback(self, result: Any) -> int: If the result is int (4xx) it indicates an error in the screen. If the result is str it contains the GUID of the selected technology type. """ + self.result = result # clear local data fields tech_type_description = "" self.tech_type_data = {} @@ -71,15 +72,21 @@ async def tech_type_callback(self, result: Any) -> int: self.tech_type_templates = [] self.tech_type_processes = [] # check that we got a valid result from the screen and process accordingly - if not result or isinstance(result, int): + if not self.result or isinstance(self.result, int): self.log(f"Technology Types screen cancelled or failed; return: {result}.") self._show_main_screen() - return result + return self.result + # The screen dismisses quit/go back as the strings "200"/"201", not ints + if str(self.result) in ("200", "201"): + self.log(f"Technology Types Screen returned on a quit/go back directive: {self.result}") + self._show_main_screen() + return self.result + self.selected_t_node = str(result) self.log(f"Technology Types screen returned: {self.selected_t_node}") # Request details for selected tech type try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) + p_client = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) self.tech_type_data = p_client.get_tech_type_detail( filter_string=self.selected_t_node, @@ -91,8 +98,15 @@ async def tech_type_callback(self, result: Any) -> int: error_message = f"Failed to retrieve technology type process details for {getattr(self, 'selected_q_name', self.selected_t_node)}: {str(e)}" self.log(f"Error retrieving technology type details: {error_category}, {error_message}") await self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) + return 417 self.log(f"Technology Type Data: {self.tech_type_data}, type: {type(self.tech_type_data)}") + # Egeria returns a plain string such as "No elements found" when there is no detail + if not isinstance(self.tech_type_data, dict): + self.notify(f"No details found for technology type '{self.selected_t_node}'", + timeout=10, severity="warning") + self._show_main_screen() + return 200 if "specificationMermaidGraph" in self.tech_type_data: del self.tech_type_data["specificationMermaidGraph"] From 689ff46b822844b50691b7ab86a6297fd5f1a99a Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:46:37 -0500 Subject: [PATCH 09/19] docs(my_profile): document the My Network option - user manual: My Network section (view details, search and add, remove with confirmation), keyboard reference rows, intro mention - reference guide: NetworkMixin section with sequence diagram and function table, plus source layout, class diagram, menu dispatch, report spec, client and testing updates Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- docs/My-Egeria-Doc.md | 72 ++++++++++++++++++++++++++++++++++- docs/my_profile_app_manual.md | 14 ++++++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/docs/My-Egeria-Doc.md b/docs/My-Egeria-Doc.md index f90359bd..b51df45a 100644 --- a/docs/My-Egeria-Doc.md +++ b/docs/My-Egeria-Doc.md @@ -28,6 +28,7 @@ All source files named here are in `my_egeria/my_egeria/DemoCode/My_Profile/`. | `tech_types_handler.py`, `TechnologyTypeScreens.py` | `TechTypesMixin` and the technology type screens | | `team_roles_handler.py`, `MyTeamScreen.py` | `TeamRolesMixin` and the team roster screen | | `bookmarks_handler.py`, `MyBookMarksScreen.py` | `BookmarksMixin` (bookmarks kept in a personal collection) and the bookmarks screen | +| `network_handler.py`, `MyNetworkScreen.py` | `NetworkMixin` (peers linked by `Peer` relationships), `MyNetworkScreen` and `ConfirmRemovePeerScreen` | | `UserIdentitiesScreen.py` | Read-only user identity table | | `StatusScreen.py` | Result message with copy-GUID | | `profile_utils.py` | Environment check and data clean-up helpers | @@ -64,6 +65,13 @@ classDiagram list_my_bookmarks() add_my_bookmark() / delete_my_bookmark() } + class NetworkMixin { + show_my_network() + list_my_peers() + search_people() + link_peer() / unlink_peer() + get_peer_details() + } App <|-- MyProfileApp TechTypesMixin <|-- MyProfileApp ShopForDataMixin <|-- MyProfileApp @@ -71,6 +79,7 @@ classDiagram ElementsCrudMixin <|-- MyProfileApp FeedbackMixin <|-- MyProfileApp BookmarksMixin <|-- MyProfileApp + NetworkMixin <|-- MyProfileApp ``` - **App bindings:** @@ -156,6 +165,7 @@ It returns `200` on success and `401` on failure. | Subscriptions | `ViewSubscriptionsScreen()` | | Technology Types | `handle_technology_types_option()` | | User Bookmarks | `show_my_bookmarks()` | +| My Network | `show_my_network()` | | Leave Feedback | `action_feedback()` | | Feedback Log | `action_view_feedback_log()` (owner only) | @@ -481,6 +491,61 @@ After each change it re-reads the list through the app. The lookup matches on th --- +## My Network + +A user's network is the set of people linked to their profile by a `Peer` relationship. `NetworkMixin` (`network_handler.py`) makes the Egeria calls; `MyNetworkScreen` only drives it through `self.app`. Each call builds a fresh `Egeria` client with a bearer token (`_network_client()`) and closes its session afterwards. + +```mermaid +sequenceDiagram + autonumber + actor User + participant App as NetworkMixin + participant MNS as MyNetworkScreen + participant Confirm as ConfirmRemovePeerScreen + participant EG as Egeria + + User->>App: Other Functions → My Network + App->>EG: get_related_elements(user_GUID, "Peer") + App->>MNS: push(peer rows) + opt view details (v) + MNS->>App: get_peer_details(peer GUID) + App->>EG: get_actor_profile_by_guid(JSON) + App->>App: _generate_my_profile_output(DICT, "My-User") + App-->>MNS: (contact methods, communities) + end + opt add (search, select, Add to Network) + MNS->>App: search_people(name) + App->>EG: find_actor_profiles(DICT, "User-Identities", page_size=10) + App-->>MNS: (name, GUID, qualified name) rows + MNS->>App: link_peer(GUID, nickname, description) + App->>EG: link_peer_person(user_GUID, GUID, PeerProperties) + MNS->>App: list_my_peers() + end + opt remove (d) + MNS->>Confirm: push(peer name) + Confirm-->>MNS: True / False + MNS->>App: unlink_peer(GUID) + App->>EG: unlink_peer_person(user_GUID, GUID) + MNS->>App: list_my_peers() + end +``` + +| Function | What it does | +| :--- | :--- | +| `show_my_network()` | Refuses (with a notice) if `user_GUID` isn't set yet, i.e. the profile hasn't loaded; otherwise pushes `MyNetworkScreen(list_my_peers())` | +| `list_my_peers()` | `get_related_elements(user_GUID, "Peer")`, keeping only elements whose properties are `PersonProperties`. Returns `(qualified name, display name, full name, pronouns, GUID, description)` rows; the GUID comes from the peer's `elementHeader` and the description from `relationshipProperties`. `[]` when Egeria answers "No elements found", `None` if Egeria can't be reached | +| `search_people(search_string)` | `find_actor_profiles(..., ignore_case=True, report_spec="User-Identities", page_size=10)` → `(display name, GUID, qualified name)` rows, excluding the current user. The screen also drops anyone already in the network | +| `link_peer(guid, nickname, description)` | `link_peer_person(user_GUID, guid, NewRelationshipRequestBody)` with `PeerProperties` (`peer_relationship_name` = nickname, `description`) | +| `unlink_peer(guid)` | `unlink_peer_person(user_GUID, guid, DeleteRelationshipRequestBody)`, using the same GUID order as `link_peer_person` | +| `get_peer_details(guid)` | `get_actor_profile_by_guid(guid, JSON)`, then `_generate_my_profile_output(..., report_spec="My-User")`. Returns the `Contact Methods` and `Communities` lists, or `None` with a notice if there are no details | + +- **Removing only unlinks.** The peer's profile is never changed. If the details panel is showing the person just removed, it is hidden. +- **Refresh.** After a successful add or remove the screen re-reads the network with `list_my_peers()`. +- **Errors.** Every Egeria failure is reported with `notify(...)` and the screen stays open; none of them exit the app. +- **Return code.** `MyNetworkScreen` dismisses with `200` on `q`; `my_network_callback` just logs it. + +--- + ## Status Screen `StatusScreen(message)` shows a read-only message: @@ -497,7 +562,8 @@ The app's `status_callback` then returns to the main screen. | Report spec | Used by | | :--- | :--- | | `My-User-MD` | Profile load and reload | -| `User-Identities` | User identity table | +| `User-Identities` | User identity table; My Network people search | +| `My-User` | My Network peer details (contact methods, communities) | | `My-User-ToDos` | To-dos table | | `Actor-Profiles` | User GUID fallback | | `Comment-by-Element` | Comments screen | @@ -509,7 +575,7 @@ The app's `status_callback` then returns to the main screen. | Client | Used for | | :--- | :--- | | `MyProfile` | Profile, identities, to-dos, `add_my_profile` | -| `Egeria` | Element create, link and delete; comments from main; bookmarks; feedback note logs; tabular data; `update_actor_profile`; `find_collections` | +| `Egeria` | Element create, link and delete; comments from main; bookmarks; My Network; feedback note logs; tabular data; `update_actor_profile`; `find_collections` | | `EgeriaTech` | Comments screen | | `AutomatedCuration` | Technology types | | `ProductManager` | Digital subscriptions | @@ -548,6 +614,8 @@ The suite in `tests/micro-tests/my_profile/` has one module per handler and scre **Running the whole app in a test.** Tests that run `MyProfileApp.run_test()` stub the profile client with `stub_profile_client()`. +**My Network.** `test_network_handler.py` covers `NetworkMixin` and `MyNetworkScreen`, including the remove confirmation; `test_my_profile_app.py` checks the menu routing. The feature has also been tested by hand against a live server. + **Add screens.** `test_screens.py::TestBaseAddScreen` covers the `BaseAddScreen` flow: required-field checks, create, link or no link, and keeping the form after a failed create. --- diff --git a/docs/my_profile_app_manual.md b/docs/my_profile_app_manual.md index 0bd49f6d..a9cd3b6a 100644 --- a/docs/my_profile_app_manual.md +++ b/docs/my_profile_app_manual.md @@ -3,7 +3,7 @@ # My Profile App User Manual -The **My Profile App** is a terminal user interface (TUI), built with [Textual](https://textual.textualize.io/) and the `pyegeria` SDK, that gives an Egeria user a personal dashboard: their profile, roles, teams, communities, blogs, journal, to-dos and user identities, plus access to comments, feedback, data shopping, subscriptions and technology types. +The **My Profile App** is a terminal user interface (TUI), built with [Textual](https://textual.textualize.io/) and the `pyegeria` SDK, that gives an Egeria user a personal dashboard: their profile, roles, teams, communities, blogs, journal, to-dos and user identities, plus access to comments, feedback, data shopping, subscriptions, technology types and their network of peers. This manual describes how to *use* the app. For how it works internally (screens, handlers, Egeria calls and flow diagrams) see the [My Profile Reference Guide](My-Egeria-Doc.md). @@ -250,6 +250,16 @@ On the bookmarks screen: Your bookmarks are kept in Egeria as the members of your private **My Bookmarks** collection (qualified name `Bookmarks::`), and only that collection. It is created the first time you bookmark something, anchored to your profile and linked from it with a `ResourceList` relationship whose resource use is "Bookmarks". +### My Network +Your network is the people linked to your profile in Egeria. The screen lists them (qualified name, display name, full name, pronouns, GUID and the description you gave the link). If your profile has not finished loading, the app tells you to wait and does not open the screen. + +- **View details:** highlight a person and press `v` (or **Person Details**). A panel shows their **contact methods** and the **communities** they belong to. A message tells you if either is empty. +- **Add someone:** type all or part of a name in the search field and press `Enter`. Up to 10 matching people are listed, leaving out you and anyone already in your network. Select a person, optionally enter a **nickname** and a **description** of your relationship, then press **Add to Network**. **Cancel** clears the selection. +- **Remove someone:** highlight a person and press `d` (or **Remove from Network**), then confirm with **Remove**. **Cancel** or `Escape` keeps them. Only the link between you is removed; their profile is not changed. +- `q` returns to the main screen. + +Each person in your network is linked to your profile with a `Peer` relationship. The nickname and description are stored on that relationship. + ### Leave Feedback / Feedback Log See [Leaving Feedback](#leaving-feedback). @@ -267,6 +277,8 @@ See [Leaving Feedback](#leaving-feedback). | `ctrl+b` | Main screen, Shop for Data | Bookmarks, with the highlighted row's GUID ready to add | | `ctrl+k` | Main screen | Bookmark the selected row | | `ctrl+n` / `d` / `q` | Bookmarks | Add by GUID / remove highlighted / close | +| `v` / `d` / `q` | My Network | Show highlighted person's details / remove highlighted person / close | +| `Enter` | My Network search field | Search for people to add | | `a` / `d` / `Escape` | Edit screens | Add row / delete row / back | | `ctrl+a` / `q` | Add screens | Add / close (outside a text field) | | `ctrl+a` / `ctrl+r` / `q` | Comments screen | Add comment / show responses to highlighted comment / close | From 04dd837d39a865dd3c5afa7a9327b22f8c30000d Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:50:10 -0500 Subject: [PATCH 10/19] chore: revert machine-specific config.json and test edits Restore config/config.json and test_classification_manager.py to their upstream versions. The config change pointed the report spec modules and log directory at local absolute paths, and the test change was local debugging of the Peer relationship query. Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- config/config.json | 5 ++--- tests/functional-tests/test_classification_manager.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/config.json b/config/config.json index d41ff2e2..dac20f26 100644 --- a/config/config.json +++ b/config/config.json @@ -28,8 +28,7 @@ "Pyegeria Publishing Root": "http://localhost:8085/outbox", "Pyegeria User Report Specs Dir": "~/.pyegeria/report_specs", "Pyegeria Report Spec Modules": [ - "pyegeria.view.analytic_demo_specs:get_analytic_demo_specs", - "/Users/petercoldicott/PycharmProjects/egeria/egeria-workspaces/egeria-python/my_report_specs/my_profile_report_specs.json" + "pyegeria.view.analytic_demo_specs:get_analytic_demo_specs" ] }, "feature_x_enabled": false, @@ -43,7 +42,7 @@ "console_logging_level": "ERROR", "enable_logging": true, "file_logging_level": "WARNING", - "log_directory": "/Users/petercoldicott/PycharmProjects/egeria/egeria-workspaces/egeria-python/logs", + "log_directory": "/Users/dwolfson/localGit/egeria-v5-3/egeria-python/logs", "logging_console_format": "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}", "logging_file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}-{function}:{line}-{message}-{extra}" }, diff --git a/tests/functional-tests/test_classification_manager.py b/tests/functional-tests/test_classification_manager.py index ccc2c897..1ae4762d 100644 --- a/tests/functional-tests/test_classification_manager.py +++ b/tests/functional-tests/test_classification_manager.py @@ -590,14 +590,14 @@ def test_get_all_related_elements(): def test_get_related_elements(): # metadata_element_type = 'CertificationType' # element_guid = '2ce8d10d-08a2-4fca-b0c2-1d5a335d00fc' - element_guid = "79bd39ed-6048-4731-85c4-3cde50f99a30" + element_guid = "acca7cf3-40d4-40fd-9922-5c74075e9651" # metadata_element_type = "Organization" # metadata_element_type = "CSVFile" # metadata_element_type = "InformationSupplyChain" open_metadata_type_name = "Referenceable" # element_guid = "8dca6e76-d454-4344-9c93-faa837a1a898" # relationship_type = "DataContentForDataSet" - relationship_type = "Peer" + relationship_type = "CollectionMembership" # relationship_type = "InformationSupplyChainComposition" c_client = ClassificationExplorer(view_server, platform_url) try: @@ -605,7 +605,6 @@ def test_get_related_elements(): body = { "class": "ResultsRequestBody", "metadataElementTypeName": open_metadata_type_name, - "includeOnlyRelationships": ["ContactThrough"] } response = c_client.get_related_elements( element_guid, relationship_type, body=body From a298d8f8dbc91d18c07bcde6c0706702476172f5 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:58:54 -0500 Subject: [PATCH 11/19] test(my_profile): cover Technology Types go back and missing details - go back/quit as "201"/"200" (str or int) returns to the main screen without looking up a tech type - a string detail result such as "No elements found" notifies and returns to the main screen - patch Egeria rather than AutomatedCuration, matching the handler, so the callback tests no longer try to reach the server - a failed detail lookup now pushes only the StatusScreen Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../my_profile/test_tech_types_handler.py | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/tests/micro-tests/my_profile/test_tech_types_handler.py b/tests/micro-tests/my_profile/test_tech_types_handler.py index 358e90f0..a1da2ac2 100644 --- a/tests/micro-tests/my_profile/test_tech_types_handler.py +++ b/tests/micro-tests/my_profile/test_tech_types_handler.py @@ -119,14 +119,14 @@ async def test_tech_type_callback_valid_selection( ): app = DummyTechTypesApp(backend) if backend.live: - backend.patch("tech_types_handler.AutomatedCuration") + backend.patch("tech_types_handler.Egeria") # get_tech_type_detail matches on display name, so use a real one. selection = request.getfixturevalue("live_tech_type_name") else: mock_ac = MagicMock() mock_ac.create_egeria_bearer_token.return_value = "token" mock_ac.get_tech_type_detail.return_value = sample_tech_type_detail - backend.always_fake("tech_types_handler.AutomatedCuration", returns=mock_ac) + backend.always_fake("tech_types_handler.Egeria", returns=mock_ac) selection = "tech-type-guid-999" res = await app.tech_type_callback(selection) @@ -161,12 +161,43 @@ async def test_tech_type_callback_exception(self, backend): app = DummyTechTypesApp(backend) mock_ac = MagicMock() mock_ac.create_egeria_bearer_token.side_effect = PyegeriaException("Auth failed") - backend.always_fake("tech_types_handler.AutomatedCuration", returns=mock_ac) + backend.always_fake("tech_types_handler.Egeria", returns=mock_ac) - await app.tech_type_callback("tech-type-guid-999") - assert len(app.pushed_screens) == 2 # StatusScreen pushed on error, then TechnologyTypeOptionsScreen + res = await app.tech_type_callback("tech-type-guid-999") + # Only the StatusScreen: the options screen is not opened after a failed lookup + assert res == 417 + assert len(app.pushed_screens) == 1 assert isinstance(app.pushed_screens[0][0], StatusScreen) + @pytest.mark.parametrize("result", ["201", 201, "200", 200]) + @pytest.mark.asyncio + async def test_tech_type_callback_go_back(self, backend, result): + # TechnologyTypesScreen dismisses go back/quit as the strings "201"/"200" + app = DummyTechTypesApp(backend) + mock_egeria = MagicMock() + backend.always_fake("tech_types_handler.Egeria", returns=mock_egeria) + + res = await app.tech_type_callback(result) + assert res == result + assert app.shown_main_screen is True + assert app.pushed_screens == [] + mock_egeria.get_tech_type_detail.assert_not_called() + + @pytest.mark.asyncio + async def test_tech_type_callback_no_details(self, backend): + # Always faked: Egeria answers with a string when the type has no details + app = DummyTechTypesApp(backend) + app.notify = MagicMock() + mock_egeria = MagicMock() + mock_egeria.get_tech_type_detail.return_value = "No elements found" + backend.always_fake("tech_types_handler.Egeria", returns=mock_egeria) + + res = await app.tech_type_callback("Unknown Type") + assert res == 200 + assert app.shown_main_screen is True + assert app.pushed_screens == [] + app.notify.assert_called_once() + @pytest.mark.asyncio async def test_tech_type_options_callback_template(self): app = DummyTechTypesApp() From f90844291338913623dba7d5878364220470e85a Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:06:20 -0500 Subject: [PATCH 12/19] fix(my_profile): restore Shop for Data go back and letter shortcuts The screen's prompt was a TextArea, which took focus when the screen opened. A focused TextArea consumes printable keys, so b (go back), q, s, u, t and k did nothing and Textual hid them from the footer. - make the prompt a Static, and mention b/Escape in it - add Escape as a go-back key (not shown in the footer) - move Go back next to Quit so it isn't cut off on narrow terminals - tests: b and Escape pressed straight after the screen opens dismiss with [200]; the callback returns [200] to the main screen - user manual: document Escape on Shop for Data Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- docs/my_profile_app_manual.md | 4 ++-- .../DemoCode/My_Profile/ShopForDataScreen.py | 11 +++++++---- tests/micro-tests/my_profile/test_screens.py | 12 ++++++++++++ .../my_profile/test_shop_for_data_handler.py | 6 ++++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/docs/my_profile_app_manual.md b/docs/my_profile_app_manual.md index a9cd3b6a..1065387c 100644 --- a/docs/my_profile_app_manual.md +++ b/docs/my_profile_app_manual.md @@ -218,7 +218,7 @@ Opens a screen with five tables, each loading in the background: **Glossaries**, - `u` or `ctrl+s` – subscribe to the highlighted item. - `t` – search glossary terms. Back from the search (`g`) returns to Shop for Data. - `k` – bookmark the highlighted item; `ctrl+b` – open bookmarks with its GUID ready to add. -- `b` – back; `q` – quit the screen. +- `b` or `Escape` – back to the main screen; `q` – quit the screen. On the overview screen, `s` subscribes to the selected node, `b` goes back and `q` returns to the main screen. On a sample-data view, `s` subscribes, and `b`, `Escape` or `Enter` go back. @@ -282,7 +282,7 @@ See [Leaving Feedback](#leaving-feedback). | `a` / `d` / `Escape` | Edit screens | Add row / delete row / back | | `ctrl+a` / `q` | Add screens | Add / close (outside a text field) | | `ctrl+a` / `ctrl+r` / `q` | Comments screen | Add comment / show responses to highlighted comment / close | -| `s` / `u` or `ctrl+s` / `t` / `k` / `b` / `q` | Shop for Data | Sample / subscribe / search glossary terms / bookmark / back / close | +| `s` / `u` or `ctrl+s` / `t` / `k` / `b` or `Escape` / `q` | Shop for Data | Sample / subscribe / search glossary terms / bookmark / back / close | | `s` / `b` / `q` | Overview | Subscribe / back / back to main | | `c` / `q` | Create Subscription | Create / cancel | | `c` / `Enter` / `q` | Status screen | Copy GUID / close / close | diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py index 529cf4ce..b99c2937 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py @@ -11,22 +11,24 @@ from pyegeria import load_app_config, settings from textual import on from textual.app import ComposeResult +from textual.binding import Binding from textual.containers import ScrollableContainer from textual.events import Action from textual.screen import Screen -from textual.widgets import DataTable, Header, TextArea, Static, Footer +from textual.widgets import DataTable, Header, Static, Footer class ShopForDataScreen(Screen): """ Screen to Present a choice of different data sources to the user.""" BINDINGS = [("q", "dismiss(200)", "Quit"), + ("b", "back", "Go back"), + Binding("escape", "back", "Go back", show=False), ("s", "sample_data_source", "Sample data source"), ("u", "subscribe_to_data_source", "Subscribe"), ("ctrl+s", "subscribe_to_data_source", "Subscribe"), ("t", "search_for_term", "Search Glossary Terms"), ("k", "bookmark_row", "Bookmark"), - ("ctrl+b", "bookmarks", "Bookmarks"), - ("b", "back", "Go back")] + ("ctrl+b", "bookmarks", "Bookmarks")] CSS_PATH = "my_profile.tcss" @@ -86,7 +88,8 @@ def __init__( def compose(self) -> ComposeResult: """ Compose the UI components for the ShopForDataScreen screen.""" yield Header(show_clock=True) - yield TextArea(f"\n\n\nPlease click on a line to select a data source:") + # A Static, not a TextArea: a focused TextArea swallows the screen's letter key bindings (b, q, s, ...) + yield Static("\nPlease click on a line to select a data source, or press b or Escape to go back:") yield ScrollableContainer( Static("Glossary"), self.glossary_table) diff --git a/tests/micro-tests/my_profile/test_screens.py b/tests/micro-tests/my_profile/test_screens.py index 80d236b1..b2994cd5 100644 --- a/tests/micro-tests/my_profile/test_screens.py +++ b/tests/micro-tests/my_profile/test_screens.py @@ -776,6 +776,18 @@ async def test_shop_for_data_screen_default_config(self): await pilot.pause() assert app.dismissed_result == [210] + @pytest.mark.parametrize("key", ["b", "escape"]) + @pytest.mark.asyncio + async def test_shop_for_data_screen_go_back_key(self, key): + # Pressed as soon as the screen opens, without focusing a table first: nothing on the + # screen may take focus and swallow the key (a TextArea prompt used to) + app = ScreenTestHostApp(lambda: ShopForDataScreen()) + async with app.run_test() as pilot: + await pilot.pause() + await pilot.press(key) + await pilot.pause() + assert app.dismissed_result == [200] + @pytest.mark.asyncio async def test_shop_for_data_screen_subscribe_action(self): t2 = DataTable(id="digital_product_catalog_table") diff --git a/tests/micro-tests/my_profile/test_shop_for_data_handler.py b/tests/micro-tests/my_profile/test_shop_for_data_handler.py index c3720ae6..a04c3e14 100644 --- a/tests/micro-tests/my_profile/test_shop_for_data_handler.py +++ b/tests/micro-tests/my_profile/test_shop_for_data_handler.py @@ -164,6 +164,12 @@ async def test_shop_for_data_callback_exit_codes(self): assert res == 210 assert app.shown_main_screen is True + # go back (b / Escape) dismisses with [200] + app.shown_main_screen = False + res = await app.shop_for_data_callback([200]) + assert res == 200 + assert app.shown_main_screen is True + @pytest.mark.asyncio async def test_shop_for_data_callback_search_for_term(self): app = DummyShopApp() From 76e66928a21346028ed6238f0caa0408ed31afb6 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:30:27 -0500 Subject: [PATCH 13/19] feat(my_profile): use one key binding convention on every screen Every screen now follows the same rules: - escape goes back one level (every screen except Main) - q is the only plain-letter key: main menu on list screens, quit on Main; left off text-entry forms, where it would be typed into the Input - every other action is a ctrl key with one meaning app-wide: ctrl+s submit, ctrl+n add, ctrl+d delete, ctrl+o open, ctrl+k/ctrl+b bookmark row/bookmarks, ctrl+u subscribe, ctrl+l sample, ctrl+g glossary search, ctrl+y copy GUID, ctrl+r refresh (was r) Return codes are unchanged; only the keys moved. Also fixes keys that could never fire: ctrl+i (sent as Tab) and ctrl+c on Edit Profile, the "return" key name on Status and the sample-data view, and the Technology Types quit/go back mix-ups. Adds ctrl+s to Create/Edit Profile. MainScreen shows the user name as its subtitle; its config logging moves to on_mount, since self.log needs a running app. test_key_bindings.py enforces the convention; docs updated to match. Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- docs/developer_guide_pyegeria_textual.md | 4 +- docs/my_profile_app_manual.md | 118 ++++++++-------- .../DemoCode/My_Profile/AddCommentScreen.py | 5 +- .../My_Profile/AddToElementsScreens.py | 7 +- .../My_Profile/CreateProfileScreen.py | 7 +- .../CreateSubscriptionRequestScreen.py | 4 +- .../My_Profile/EditElementsScreens.py | 26 ++-- .../DemoCode/My_Profile/FeedbackScreens.py | 4 +- .../My_Profile/GenericDataViewScreen.py | 6 +- .../DemoCode/My_Profile/MainScreen.py | 20 ++- .../DemoCode/My_Profile/MyBookMarksScreen.py | 9 +- .../DemoCode/My_Profile/MyNetworkScreen.py | 7 +- .../DemoCode/My_Profile/MyTeamScreen.py | 4 +- .../My_Profile/SearchForTermScreen.py | 3 +- .../My_Profile/SelectionOverviewScreen.py | 6 +- .../DemoCode/My_Profile/ShopForDataScreen.py | 18 ++- .../DemoCode/My_Profile/ShowCommentsScreen.py | 15 +- .../DemoCode/My_Profile/StatusScreen.py | 9 +- .../My_Profile/TechnologyTypeScreens.py | 19 ++- .../My_Profile/UserIdentitiesScreen.py | 4 +- .../DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md | 10 +- .../My_Profile/ViewSubscriptionsScreen.py | 5 +- .../My_Profile/elements_crud_handler.py | 2 +- .../DemoCode/My_Profile/my_profile_app.py | 2 +- .../my_profile/test_key_bindings.py | 130 ++++++++++++++++++ tests/micro-tests/my_profile/test_screens.py | 4 +- 26 files changed, 301 insertions(+), 147 deletions(-) create mode 100644 tests/micro-tests/my_profile/test_key_bindings.py diff --git a/docs/developer_guide_pyegeria_textual.md b/docs/developer_guide_pyegeria_textual.md index 362462f2..6ae266a5 100644 --- a/docs/developer_guide_pyegeria_textual.md +++ b/docs/developer_guide_pyegeria_textual.md @@ -686,7 +686,7 @@ That is the second of the two standard worker patterns, so you now have both: ### 7.5 Acting on the selected row My Profile lets the user act on "the selected row" from several places: add a comment -(Ctrl+A), show comments (Ctrl+S), edit (Ctrl+T), bookmark it (Ctrl+K) and open bookmarks with +(Ctrl+N), show comments (Ctrl+O), edit (Ctrl+T), bookmark it (Ctrl+K) and open bookmarks with its GUID ready to add (Ctrl+B). Two problems need solving. **Which row is selected?** `MainScreen` records the table and row whenever the user @@ -737,7 +737,7 @@ qualified name. Some tables, such as Shop for Data's glossary table (built from Because `get_row_guid` takes the table itself, not a table name on one particular screen, any screen can use it, and each key binding is a few lines: -- **Ctrl+K** on the main screen, or **k** on Shop for Data, calls +- **Ctrl+K**, on the main screen or Shop for Data, calls `self.app.bookmark_table_row(table, row_key)`, which bookmarks the row straight away. - **Ctrl+B** calls `self.app.show_my_bookmarks(self.app.get_row_guid(table, row_key))`. The bookmark screen receives the GUID as `target_guid` and pre-fills its input with it, just as diff --git a/docs/my_profile_app_manual.md b/docs/my_profile_app_manual.md index 1065387c..b7033bce 100644 --- a/docs/my_profile_app_manual.md +++ b/docs/my_profile_app_manual.md @@ -134,18 +134,18 @@ The header shows your full name, user ID and **Karma Points** (your contribution | **User Identity** | Display Name, User ID, Distinguished Name, GUID | User identities linked to your profile | | **Projects** | Status or Type, Name, Description, GUID | Projects you are involved in | -Click or move into a table to select it; the highlighted row is the "selected row" used by the shortcuts below. Press `r` to reload everything from Egeria. The app also reloads automatically when you close an Add screen. +Click or move into a table to select it; the highlighted row is the "selected row" used by the shortcuts below. Press `ctrl+r` to reload everything from Egeria. The app also reloads automatically when you close an Add screen. Main screen shortcuts: | Key | Action | | :--- | :--- | | `ctrl+t` | **Edit Selected Table** – opens the edit screen for the selected table, where you add and delete rows | -| `ctrl+a` | **Add Comment** to the selected row | -| `ctrl+s` | **Show Comments** for the selected row | -| `ctrl+b` | **Manage Bookmarks** | +| `ctrl+o` | **Show Comments** for the selected row | +| `ctrl+n` | **Add Comment** to the selected row | | `ctrl+k` | **Bookmark** the element in the selected row | -| `r` | Refresh all data from Egeria | +| `ctrl+b` | **Manage Bookmarks** | +| `ctrl+r` | Refresh all data from Egeria | | `ctrl+f` | Leave feedback (works on every screen) | | `q` | Quit the app | @@ -155,8 +155,8 @@ Adding and deleting both go through the table's **edit screen**: 1. On the main screen, select a table and press `ctrl+t`. 2. The edit screen shows a copy of that table. Then: - - `a` – **Add Row**: opens the Add screen for that kind of element. - - `d` – **Delete Row**: asks you to confirm, then deletes the highlighted element from Egeria. The row is removed only if Egeria accepts the delete. You can delete one row at a time. + - `ctrl+n` – **Add Row**: opens the Add screen for that kind of element. + - `ctrl+d` – **Delete Row**: asks you to confirm, then deletes the highlighted element from Egeria. The row is removed only if Egeria accepts the delete. You can delete one row at a time. - `Escape` – back to the main screen. Edit screens add and delete; they do not edit a row's values in place. To change your own details use **Edit Profile** from the Other Functions menu. @@ -165,10 +165,10 @@ Edit screens add and delete; they do not edit a row's values in place. To change Every Add screen works the same way: - Fill in the fields. Those marked \* are required. -- Press **Add** (or `ctrl+a`). +- Press **Add** (or `ctrl+s`). - Some screens have a switch that links the new element to you. It is on by default. -If a required field is missing or a value is invalid, the screen tells you which one. After a successful add the form is cleared, so you can add another; after a failed add your input is kept so you can correct it. Press **Quit** (or `q` outside a text field) to close the screen. You go back to the main screen and the data is reloaded. +If a required field is missing or a value is invalid, the screen tells you which one. After a successful add the form is cleared, so you can add another; after a failed add your input is kept so you can correct it. Press **Quit** (or `Escape`) to close the screen. You go back to the main screen and the data is reloaded. | Table | Fields | Link switch | Egeria result | | :--- | :--- | :--- | :--- | @@ -187,15 +187,15 @@ If a required field is missing or a value is invalid, the screen tells you which Comments can be attached to any element in a main-screen table, and responses can be attached to comments to any depth. -**Add a comment from the main screen:** select a row, press `ctrl+a`, enter the comment and a comment type (`Question`, `Answer`, `Suggestion` or `Requirement`), then press **Submit**. +**Add a comment from the main screen:** select a row, press `ctrl+n`, enter the comment and a comment type (`Question`, `Answer`, `Suggestion` or `Requirement`), then press **Submit** (`ctrl+s`). -**View and reply:** select a row and press `ctrl+s`. The comments screen lists the comments on that element. +**View and reply:** select a row and press `ctrl+o`. The comments screen lists the comments on that element. -- `ctrl+a` adds a new comment to the element being shown. -- `ctrl+r` on a highlighted comment re-opens the screen focused on *that comment*, showing its responses. Here `ctrl+a` adds a response. Repeat to go deeper into a thread. -- `q` closes the screen. +- `ctrl+n` adds a new comment to the element being shown. +- `ctrl+o` on a highlighted comment re-opens the screen focused on *that comment*, showing its responses. Here `ctrl+n` adds a response. Repeat to go deeper into a thread. +- `Escape` or `q` closes the screen. -If an element has no comments yet, the screen tells you to use `ctrl+a`. +If an element has no comments yet, the screen tells you to use `ctrl+n`. ## Leaving Feedback @@ -214,21 +214,21 @@ A read-only table of your user identities: display name, category, description, Opens a screen with five tables, each loading in the background: **Glossaries**, **Digital Product Catalog**, **Data Dictionaries**, **Business Domains** and **Root Collections**. - **Enter** (or click) on a row opens an **overview** screen: a tree on the left and details on the right. Selecting a tree node shows its details – glossary terms, digital products (including sample data), dictionaries, business capabilities or collections. -- `s` – show sample data for the highlighted row. -- `u` or `ctrl+s` – subscribe to the highlighted item. -- `t` – search glossary terms. Back from the search (`g`) returns to Shop for Data. -- `k` – bookmark the highlighted item; `ctrl+b` – open bookmarks with its GUID ready to add. -- `b` or `Escape` – back to the main screen; `q` – quit the screen. +- `ctrl+l` – show sample data for the highlighted row. +- `ctrl+u` – subscribe to the highlighted item. +- `ctrl+g` – search glossary terms. `Escape` from the search returns to Shop for Data. +- `ctrl+k` – bookmark the highlighted item; `ctrl+b` – open bookmarks with its GUID ready to add. +- `Escape` or `q` – back to the main screen. -On the overview screen, `s` subscribes to the selected node, `b` goes back and `q` returns to the main screen. On a sample-data view, `s` subscribes, and `b`, `Escape` or `Enter` go back. +On the overview screen, `ctrl+u` subscribes to the selected node, `Escape` goes back and `q` returns to the main screen. On a sample-data view, `ctrl+u` subscribes, `Escape` goes back to Shop for Data and `q` returns to the main screen. -**Subscribing**, from any of these screens, opens the **Create Subscription Request** form. It has Display Name, Description, Identifier and Status (`DRAFT`, `PROPOSED` or `ACTIVE`; an invalid value becomes `DRAFT`). Press `c` to create the digital subscription in Egeria. The subscription is linked to the item you chose and to you as its subscriber. If either link can't be made, a warning says so; the subscription itself is still created. +**Subscribing**, from any of these screens, opens the **Create Subscription Request** form. It has Display Name, Description, Identifier and Status (`DRAFT`, `PROPOSED` or `ACTIVE`; an invalid value becomes `DRAFT`). Press `ctrl+s` to create the digital subscription in Egeria (`Escape` cancels). The subscription is linked to the item you chose and to you as its subscriber. If either link can't be made, a warning says so; the subscription itself is still created. ### Edit Profile -A form pre-filled with your profile details (courtesy title, job title, given names, surname, preferred name, pronouns, description, time zone, employee ID, preferred language, resident country). Press **Edit Profile** to save. The screen also has shortcuts that open the Communities (`ctrl+c`), Identities (`ctrl+i`), Roles (`ctrl+r`) and Teams (`ctrl+t`) edit screens. Press `q` to return. +A form pre-filled with your profile details (courtesy title, job title, given names, surname, preferred name, pronouns, description, time zone, employee ID, preferred language, resident country). Press **Edit Profile** (or `ctrl+s`) to save. The screen also has shortcuts that open the Communities (`ctrl+g`), Identities (`ctrl+l`), Roles (`ctrl+r`) and Teams (`ctrl+t`) edit screens. Press `Escape` to return. ### Subscriptions -A table of the digital subscriptions you created: name, status, description and GUID. Press `q` to return. +A table of the digital subscriptions you created: name, status, description and GUID. Press `Escape` or `q` to return. ### Technology Types Browse the technology type hierarchy as a tree. Selecting a type shows its **catalog templates** and **governance action processes**: @@ -236,27 +236,27 @@ Browse the technology type hierarchy as a tree. Selecting a type shows its **cat - **Templates:** pick one and press **Select Template**. Fill in its placeholder values and submit. Egeria creates a new element from the template, and a **status screen** shows the result. - **Processes:** pick one and press **Select Process**, then fill in its request parameters and submit. Only the parameters you fill in are sent. Egeria starts the governance action process, and a **status screen** shows its GUID. -On the status screen, `c` copies the GUID shown to your clipboard, and `Enter`/`q` closes it and returns you to the main screen. +On the status screen, `ctrl+y` copies the GUID shown to your clipboard, and `Enter`, `Escape` or `q` closes it and returns you to the main screen. ### User Bookmarks Opens the bookmarks screen (also `ctrl+b` on the main screen and on Shop for Data). It lists your bookmarks (name, type, GUID). -The easiest way to add a bookmark is from a table: select a row and press `ctrl+k` on the main screen, or highlight a row and press `k` on Shop for Data. Any row that has a GUID can be bookmarked, and so can a glossary row, which is identified by its qualified name. Bookmarking something that is already bookmarked just tells you so. +The easiest way to add a bookmark is from a table: select a row and press `ctrl+k` on the main screen, or highlight a row and press `ctrl+k` on Shop for Data. Any row that has a GUID can be bookmarked, and so can a glossary row, which is identified by its qualified name. Bookmarking something that is already bookmarked just tells you so. On the bookmarks screen: -- `ctrl+n` shows a field for the GUID of an element to bookmark. If you opened the screen with `ctrl+b` while a row was highlighted, that row's GUID is already filled in; otherwise copy a GUID from any table, or with `c` on a status screen. -- `d` removes the highlighted bookmark (the element itself is not deleted). -- `q` returns. +- `ctrl+n` shows a field for the GUID of an element to bookmark. If you opened the screen with `ctrl+b` while a row was highlighted, that row's GUID is already filled in; otherwise copy a GUID from any table, or with `ctrl+y` on a status screen. +- `ctrl+d` removes the highlighted bookmark (the element itself is not deleted). +- `Escape` or `q` returns. Your bookmarks are kept in Egeria as the members of your private **My Bookmarks** collection (qualified name `Bookmarks::`), and only that collection. It is created the first time you bookmark something, anchored to your profile and linked from it with a `ResourceList` relationship whose resource use is "Bookmarks". ### My Network Your network is the people linked to your profile in Egeria. The screen lists them (qualified name, display name, full name, pronouns, GUID and the description you gave the link). If your profile has not finished loading, the app tells you to wait and does not open the screen. -- **View details:** highlight a person and press `v` (or **Person Details**). A panel shows their **contact methods** and the **communities** they belong to. A message tells you if either is empty. +- **View details:** highlight a person and press `ctrl+o` (or **Person Details**). A panel shows their **contact methods** and the **communities** they belong to. A message tells you if either is empty. - **Add someone:** type all or part of a name in the search field and press `Enter`. Up to 10 matching people are listed, leaving out you and anyone already in your network. Select a person, optionally enter a **nickname** and a **description** of your relationship, then press **Add to Network**. **Cancel** clears the selection. -- **Remove someone:** highlight a person and press `d` (or **Remove from Network**), then confirm with **Remove**. **Cancel** or `Escape` keeps them. Only the link between you is removed; their profile is not changed. -- `q` returns to the main screen. +- **Remove someone:** highlight a person and press `ctrl+d` (or **Remove from Network**), then confirm with **Remove**. **Cancel** or `Escape` keeps them. Only the link between you is removed; their profile is not changed. +- `Escape` or `q` returns to the main screen. Each person in your network is linked to your profile with a `Peer` relationship. The nickname and description are stored on that relationship. @@ -265,31 +265,35 @@ See [Leaving Feedback](#leaving-feedback). ## Keyboard Reference -| Key | Where | Action | +Every screen follows the same rules: + +- `Escape` goes back one level, on every screen except the main screen. +- `q` returns to the main screen, or quits the app when you are on the main screen. Screens that are mostly text fields (Add screens, Add Comment, Create Subscription, Create/Edit Profile, Search for Term, Feedback, Template and Process forms) have no `q`, because it would be typed into the field; use `Escape`. +- Every other action is a `ctrl` key, and a key means the same thing wherever it appears. `ctrl` keys work even while a text field has focus. + +| Key | Action | Where | | :--- | :--- | :--- | -| `q` | Main screen | Quit the app | -| `r` | Main screen | Refresh all data from Egeria | -| `ctrl+f` | Any screen | Leave feedback | -| `F3` | Any screen (feedback owner only) | Show the feedback log | -| `ctrl+t` | Main screen | Edit the selected table | -| `ctrl+a` | Main screen | Comment on the selected row | -| `ctrl+s` | Main screen | Show comments for the selected row | -| `ctrl+b` | Main screen, Shop for Data | Bookmarks, with the highlighted row's GUID ready to add | -| `ctrl+k` | Main screen | Bookmark the selected row | -| `ctrl+n` / `d` / `q` | Bookmarks | Add by GUID / remove highlighted / close | -| `v` / `d` / `q` | My Network | Show highlighted person's details / remove highlighted person / close | -| `Enter` | My Network search field | Search for people to add | -| `a` / `d` / `Escape` | Edit screens | Add row / delete row / back | -| `ctrl+a` / `q` | Add screens | Add / close (outside a text field) | -| `ctrl+a` / `ctrl+r` / `q` | Comments screen | Add comment / show responses to highlighted comment / close | -| `s` / `u` or `ctrl+s` / `t` / `k` / `b` or `Escape` / `q` | Shop for Data | Sample / subscribe / search glossary terms / bookmark / back / close | -| `s` / `b` / `q` | Overview | Subscribe / back / back to main | -| `c` / `q` | Create Subscription | Create / cancel | -| `c` / `Enter` / `q` | Status screen | Copy GUID / close / close | -| `Escape` | Splash screen | Continue to the app | -| `ctrl+s` / `Escape` | Feedback | Submit / cancel | -| `Escape` | Confirm Delete | Cancel | -| `ctrl+c` | Anywhere | Force quit (terminal) | +| `Escape` | Back | Every screen except Main; continue on the splash screen; cancel on confirm dialogs | +| `q` | Main menu / quit the app | Every list screen / Main screen | +| `Enter` | Select the highlighted row; continue | Tables and trees; Status screen | +| `ctrl+s` | Save / submit the form | Add screens, Add Comment, Create Subscription, Create/Edit Profile, Feedback | +| `ctrl+n` | Add a new item | Edit screens (row), Bookmarks, Comments, Main screen (comment) | +| `ctrl+d` | Delete / remove the highlighted item | Edit screens, Bookmarks, My Network | +| `ctrl+o` | Open the highlighted item | Main screen (comments), Comments (responses), My Network (person details) | +| `ctrl+t` | Edit the selected table | Main screen | +| `ctrl+k` | Bookmark the highlighted row | Main screen, Shop for Data | +| `ctrl+b` | Bookmarks, with the highlighted row's GUID ready to add | Main screen, Shop for Data | +| `ctrl+u` | Subscribe | Shop for Data, Overview, sample-data view | +| `ctrl+l` | Show sample data | Shop for Data | +| `ctrl+g` | Search glossary terms | Shop for Data | +| `ctrl+e` | Expand / collapse the whole tree | Technology Types | +| `ctrl+y` | Copy the GUID shown | Status screen | +| `ctrl+x` | Close, flagging a bad result | Status screen | +| `ctrl+g` / `ctrl+l` / `ctrl+r` / `ctrl+t` | Edit Communities / Identities / Roles / Teams | Edit Profile | +| `ctrl+r` | Refresh all data from Egeria | Main screen | +| `ctrl+f` | Leave feedback | Any screen | +| `F3` | Show the feedback log | Any screen (feedback owner only) | +| `ctrl+c` | Force quit (terminal) | Anywhere | ## Exit and Return Codes @@ -322,7 +326,7 @@ The full list is in `RETURN_CODES.md` in the app folder. - the view server named by `EGERIA_VIEW_SERVER` is running; - `EGERIA_USER` and `EGERIA_USER_PASSWORD` are correct, and the user is allowed to use the view server. -**Something I added doesn't appear.** Press `r` on the main screen. Communities you create are not linked to you yet (see Known Limitations), so they don't appear in User Associations. +**Something I added doesn't appear.** Press `ctrl+r` on the main screen. Communities you create are not linked to you yet (see Known Limitations), so they don't appear in User Associations. **The app quit unexpectedly.** The app only exits by itself in a few cases: a startup problem (codes `402`, `403`, `430`), a failure fetching technology types (`416`), or an error looking up a team (`440`). diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py index 085f6529..4b57e75a 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py @@ -18,9 +18,8 @@ class AddCommentScreen(ModalScreen): """ Add a comment to an Egeria Element the user has access to """ BINDINGS = [ - ("a", "add_comment", "Add Comment"), - ("c", "cancel", "Cancel"), - ("q", "quit", "Quit") + ("escape", "cancel", "Back"), + ("ctrl+s", "add_comment", "Add Comment"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py index f013534e..2da63d28 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py @@ -30,8 +30,8 @@ class BaseAddScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), - ("ctrl+a", "add_element", "Add"), + ("escape", "quit", "Back"), + ("ctrl+s", "add_element", "Add"), ] CSS_PATH = "my_profile.tcss" @@ -202,7 +202,8 @@ class AddAssociationScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py index 7408c676..673e68f4 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py @@ -22,7 +22,8 @@ class CreateProfileScreen(ModalScreen[int]): 4xx on failure """ - BINDINGS = [("q", "dismiss(200)", "Quit")] + BINDINGS = [("escape", "quit", "Back"), + ("ctrl+s", "create_profile", "Create Profile")] CSS_PATH = "my_profile.tcss" def __init__(self, user: str | None = None, password: str | None = None, view_server: str | None = None, platform_url: str | None = None, *args, **kwargs): @@ -66,6 +67,10 @@ def compose(self) -> ComposeResult: yield Static() yield Footer() + def action_create_profile(self) -> None: + """ ctrl+s does the same as pressing the Create Profile button """ + self.create_profile() + @on(Button.Pressed, "#create_profile_btn") def create_profile(self) -> int: """Create a new profile in Egeria from data provided in Input fields.""" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py index 65304332..4f3ae925 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py @@ -16,8 +16,8 @@ class CreateSubscriptionRequestScreen(ModalScreen): BINDINGS = [ - ("q", "quit", "Quit"), - ("c", "create_subscription", "Create Subscription") + ("escape", "quit", "Back"), + ("ctrl+s", "create_subscription", "Create Subscription"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py index d38c7701..7e65ece1 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py @@ -96,9 +96,10 @@ class BaseEditScreen(ModalScreen): SOURCE_TABLE: str = "" BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("a", "add_row", "Add Row"), - ("d", "delete_row", "Delete Row"), + ("escape", "exit_screen", "Back"), + ("q", "exit_screen", "Main Menu"), + ("ctrl+n", "add_row", "Add Row"), + ("ctrl+d", "delete_row", "Delete Row"), ] CSS_PATH = "my_profile.tcss" @@ -120,7 +121,7 @@ def compose(self) -> ComposeResult: yield Header(show_clock=True) yield Static(self.TITLE_TEXT, classes="span-3", id=self.TITLE_ID) yield Static( - " Press 'a' to add a row, 'd' to delete the selected row, Esc to exit." + " Press Ctrl+N to add a row, Ctrl+D to delete the selected row, Esc to go back." " Please note you may only delete one row at a time!", classes="span-3", ) @@ -351,11 +352,14 @@ class EditProfileScreen(ModalScreen[Any]): 4xx on failure """ - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("ctrl+c", "Edit_communities", "Edit communities"), - ("ctrl+i", "Edit_identities", "Edit identities"), - ("ctrl+r", "Edit_roles", "Edit roles"), - ("ctrl+t", "Edit_teams", "Edit teams"), + # ctrl+c and ctrl+i can't be used here: ctrl+c is Textual's copy/quit key and a + # terminal sends ctrl+i as Tab. Input swallows ctrl+a/d/e/k/u, so these avoid those too. + BINDINGS = [("escape", "quit", "Back"), + ("ctrl+s", "create_profile", "Save Profile"), + ("ctrl+g", "Edit_communities", "Edit Communities"), + ("ctrl+l", "Edit_identities", "Edit Identities"), + ("ctrl+r", "Edit_roles", "Edit Roles"), + ("ctrl+t", "Edit_teams", "Edit Teams"), ] CSS_PATH = "my_profile.tcss" @@ -470,6 +474,10 @@ def compose(self) -> ComposeResult: yield Static() yield Footer() + def action_create_profile(self) -> None: + """ ctrl+s does the same as pressing the Edit Profile button """ + self.create_profile() + @on(Button.Pressed, "#edit_profile_btn") def create_profile(self) -> Any: """Update profile in Egeria from data provided in Input fields.""" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py index 01479a62..1c12adb8 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py @@ -22,8 +22,8 @@ class FeedbackScreen(ModalScreen): """ Collect feedback about the screen the user was on when they pressed the feedback key """ BINDINGS = [ + ("escape", "cancel", "Back"), ("ctrl+s", "submit", "Submit Feedback"), - ("escape", "cancel", "Cancel"), ] CSS_PATH = "my_profile.tcss" @@ -98,8 +98,8 @@ class FeedbackLogScreen(ModalScreen): """ Show the accumulated entries in the private feedback note log """ BINDINGS = [ - ("q", "quit", "Quit"), ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py index 95599ff2..453220c4 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py @@ -23,11 +23,9 @@ class GenericDataViewScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), - ("b", "back", "Go back"), ("escape", "back", "Back"), - ("return", "back", "Back"), - ("s", "subscribe", "Subscribe to Data Source"), + ("q", "quit", "Main Menu"), + ("ctrl+u", "subscribe", "Subscribe"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py index 9b183932..7615def5 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py @@ -13,17 +13,19 @@ from textual.widgets import DataTable, OptionList, Header, Static, Footer from textual.widgets._option_list import Option +from pyegeria import load_app_config, settings + class MainScreen(Screen): """Main Screen for My Profile App.""" BINDINGS = [ ("q", "app.quit", "Quit"), - ("ctrl+a", "add_comment","add comment to selected row"), - ("ctrl+b", "bookmarks", "Manage Bookmarks"), - ("ctrl+k", "bookmark_row", "Bookmark Selected Row"), - ("ctrl+s", "show_comments", "Show Comments for Selected Row"), ("ctrl+t", "edit_table", "Edit Selected Table"), + ("ctrl+o", "show_comments", "Show Comments"), + ("ctrl+n", "add_comment", "Add Comment"), + ("ctrl+k", "bookmark_row", "Bookmark Row"), + ("ctrl+b", "bookmarks", "Bookmarks"), ] CSS_PATH = "my_profile.tcss" @@ -31,10 +33,18 @@ class MainScreen(Screen): def __init__(self, *args, **kwargs): super().__init__(id="main_screen", *args, **kwargs) self.title = "Egeria" - self.sub_title = "My Profile" self.other_function_list: OptionList = OptionList(id="other_function_list") self.selected_table = None self.selected_row = None + load_app_config() + app_user = settings.User_Profile + self.user_name = app_user.user_name or "garygeeke" + self.sub_title = f"User: {self.user_name}" + + def on_mount(self) -> None: + # self.log needs a running app, so it can't be called from __init__ + self.log(f"Application Config: {settings.Environment}") + self.log(f"User Profile: {settings.User_Profile}") def compose(self) -> ComposeResult: # place widgets into grid on screen, note sequence determines position! diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py index 8d3a16f2..c29ea38d 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py @@ -20,9 +20,10 @@ class MyBookMarksScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), - ("ctrl+n", "new_bookmark", "Add New Bookmark"), - ("d", "remove_bookmark", "Remove Highlighted Bookmark"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+n", "new_bookmark", "Add Bookmark"), + ("ctrl+d", "remove_bookmark", "Remove Bookmark"), ] CSS_PATH = "my_profile.tcss" @@ -39,7 +40,7 @@ def __init__(self, my_bookmarks, *args, target_guid: str | None = None, **kwargs def compose(self) -> ComposeResult: yield Header(show_clock=True) yield ScrollableContainer( - Static("Your bookmarks. Ctrl+N adds one by GUID; 'd' removes the highlighted one."), + Static("Your bookmarks. Ctrl+N adds one by GUID; Ctrl+D removes the highlighted one."), DataTable(id="my_bookmark_table", zebra_stripes=True, cursor_type="row"), id="bookmarks_table_container", ) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py index b2f42a50..cf6c02eb 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py @@ -64,9 +64,10 @@ class MyNetworkScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), - ("v", "view_details", "View Highlighted Person"), - ("d", "remove_peer", "Remove Highlighted Person"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+o", "view_details", "Open Person"), + ("ctrl+d", "remove_peer", "Remove Person"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py index 6a01f8d4..8ac9674d 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py @@ -18,8 +18,8 @@ class MyTeam(ModalScreen): """ Display a list of team Members for a Team Leader """ BINDINGS = [ - ("q", "quit", "Quit"), - ("b", "back", "Go back") + ("escape", "back", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py index 9404b0bc..af2ef78a 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py @@ -17,8 +17,7 @@ class SearchForTermScreen(ModalScreen): - BINDINGS = [("q", "quit", "Quit"), - ("g", "go_back", "Go back"), + BINDINGS = [("escape", "go_back", "Back"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py index 41ee4a3f..1ef46122 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py @@ -21,9 +21,9 @@ class SelectionOverviewScreen(ModalScreen): """Screen to display the selection of the user's data sources.""" - BINDINGS = [("q", "quit", "Quit"), - ("s", "subscribe", "Subscribe to Data Source"), - ("b", "back", "Go back")] + BINDINGS = [("escape", "back", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+u", "subscribe", "Subscribe")] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py index b99c2937..35cd4e08 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py @@ -11,7 +11,6 @@ from pyegeria import load_app_config, settings from textual import on from textual.app import ComposeResult -from textual.binding import Binding from textual.containers import ScrollableContainer from textual.events import Action from textual.screen import Screen @@ -20,14 +19,13 @@ class ShopForDataScreen(Screen): """ Screen to Present a choice of different data sources to the user.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "back", "Go back"), - Binding("escape", "back", "Go back", show=False), - ("s", "sample_data_source", "Sample data source"), - ("u", "subscribe_to_data_source", "Subscribe"), - ("ctrl+s", "subscribe_to_data_source", "Subscribe"), - ("t", "search_for_term", "Search Glossary Terms"), - ("k", "bookmark_row", "Bookmark"), + BINDINGS = [("escape", "back", "Back"), + # Opened straight from the main screen, so back and main menu are the same place + ("q", "back", "Main Menu"), + ("ctrl+l", "sample_data_source", "Sample Data"), + ("ctrl+u", "subscribe_to_data_source", "Subscribe"), + ("ctrl+g", "search_for_term", "Search Glossary Terms"), + ("ctrl+k", "bookmark_row", "Bookmark Row"), ("ctrl+b", "bookmarks", "Bookmarks")] CSS_PATH = "my_profile.tcss" @@ -89,7 +87,7 @@ def compose(self) -> ComposeResult: """ Compose the UI components for the ShopForDataScreen screen.""" yield Header(show_clock=True) # A Static, not a TextArea: a focused TextArea swallows the screen's letter key bindings (b, q, s, ...) - yield Static("\nPlease click on a line to select a data source, or press b or Escape to go back:") + yield Static("\nPlease click on a line to select a data source, or press Escape to go back:") yield ScrollableContainer( Static("Glossary"), self.glossary_table) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py index c27df8d7..2a124655 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py @@ -21,9 +21,10 @@ class ShowCommentsScreen(ModalScreen): """Show any comments for an element Screen for My Profile App.""" BINDINGS = [ - ("q", "quit", "Quit"), - ("ctrl+a", "add_comment", "Add Comment"), - ("ctrl+r", "add_response", "Add Response"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+n", "add_comment", "Add Comment"), + ("ctrl+o", "add_response", "Open Responses"), ] CSS_PATH = "my_profile.tcss" @@ -40,7 +41,7 @@ def __init__( load_app_config() # The comment whose responses this screen shows (None = show an element's comments) self.selected_comment_guid = selected_comment_guid or None - # The comment currently highlighted in the DataTable (target for Ctrl+R) + # The comment currently highlighted in the DataTable (target for Ctrl+O) self.highlighted_comment_guid: str | None = None app_config = settings.Environment app_user = settings.User_Profile @@ -116,15 +117,15 @@ def load_comments(self, element_guid: str) -> None: if comment_table.row_count == 0: if self.selected_comment_guid: - self.notify("This comment has no responses yet - use Ctrl+A to add one", timeout=10) + self.notify("This comment has no responses yet - use Ctrl+N to add one", timeout=10) else: - self.notify("Selected element does not have any comments - use Ctrl+A to add one", timeout=10) + self.notify("Selected element does not have any comments - use Ctrl+N to add one", timeout=10) return # Mount the table once, after all rows have been added container = self.query_one("#show_comments_container", ScrollableContainer) container.mount(comment_table) - container.mount(Static("Ctrl+R on a highlighted comment to view and add responses to it")) + container.mount(Static("Ctrl+O on a highlighted comment to view and add responses to it")) def extract_backend_identifier(self, table_name, row_key): try: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py index 90ca4695..f7c6a000 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py @@ -18,10 +18,11 @@ class StatusScreen(ModalScreen): """Modal screen to display the status of the application.""" - BINDINGS = [("q", "quit", "Quit"), - ("return", "successful", "Continue"), - ("b", "unsuccessful", "Bad result"), - ("c", "copy_guid_to_clipboard", "Copy GUID to clipboard"), + BINDINGS = [("enter", "successful", "Continue"), + ("escape", "successful", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+y", "copy_guid_to_clipboard", "Copy GUID"), + ("ctrl+x", "unsuccessful", "Bad Result"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py index fd5c4309..9a53c545 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py @@ -21,9 +21,8 @@ class TechnologyTypeOptionsScreen(ModalScreen): """ Modal screen to display a technology type's templates and processes.""" - BINDINGS = [("q", "dismiss", "Quit"), - ("b", "go_back", "Go_Back"), - # ("ctl+s", "Select", "tech_type_option_select") + BINDINGS = [("escape", "go_back", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" @@ -239,7 +238,7 @@ def action_go_back(self) -> None: """ Handle the back key press.""" self.dismiss("back") - def action_Quit(self) -> None: + def action_quit(self) -> None: """ Handle the quit key press""" self.dismiss(200) @@ -261,8 +260,7 @@ def handle_process_selected(self, event: Button.Pressed) -> None: class TechnologyTypeProcessesScreen(ModalScreen[Any]): """Modal screen to display technology type processes in Egeria.""" - BINDINGS = [("q", "quit", "Quit"), - ("b", "quit", "Go back")] + BINDINGS = [("escape", "quit", "Back")] CSS_PATH = "my_profile.tcss" @@ -406,9 +404,9 @@ def handle_input_changed(self, event: Input.Changed) -> None: class TechnologyTypesScreen(ModalScreen): """Modal screen to display technology types in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("g", "go_back", "Back"), - ("ctrl+e", "expand", "Toggle Twisties")] + BINDINGS = [("escape", "go_back", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+e", "expand", "Expand/Collapse All")] CSS_PATH = "my_profile.tcss" @@ -543,8 +541,7 @@ def render_tech_type_hierarchy_to_tree(self, class TechnologyTypeTemplatesScreen(ModalScreen[Any]): """Modal screen to display technology type templates in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "quit", "Go back")] + BINDINGS = [("escape", "quit", "Back")] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py index 4ff72ede..7888e651 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py @@ -19,8 +19,8 @@ class UserIdentitiesScreen(ModalScreen): """ Modal Screen to display the user identities as stored in Egeria """ BINDINGS = [ - ("escape", "app.pop_screen", "Exit Screen"), - ("q", "app.pop_screen", "Exit Screen"), + ("escape", "app.pop_screen", "Back"), + ("q", "app.pop_screen", "Main Menu"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md index 4109a9c7..d84ec809 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md @@ -49,17 +49,17 @@ > This panel is my working life: my **blog**, my **journal**, and my **to-dos**. > Down here is my **user identity**, which is how Egeria recognises me across the tools I use. -**[ON-SCREEN CALLOUT]** Point out the keyboard shortcuts in the footer: `Ctrl+T` Edit, `Ctrl+S` Comments, `Ctrl+B` Bookmarks. +**[ON-SCREEN CALLOUT]** Point out the keyboard shortcuts in the footer: `Ctrl+T` Edit, `Ctrl+O` Comments, `Ctrl+B` Bookmarks. --- ## 4. Keeping it current (1:45 – 2:20) -**[VISUAL]** Select the To-Dos table, press `Ctrl+T`, and add a to-do: *"Review clinical trial data quality report"*. Back on the main screen the new row appears. Then highlight a row and press `Ctrl+S` to show and add a comment. +**[VISUAL]** Select the To-Dos table, press `Ctrl+T`, and add a to-do: *"Review clinical trial data quality report"*. Back on the main screen the new row appears. Then highlight a row and press `Ctrl+O` to show and add a comment. **CALLIE:** > The profile is only useful if it's accurate. I pick any table and press **Ctrl+T** to edit it: add a row, update one, or remove something that's no longer true. Here I'm adding a to-do from this morning's stand-up. -> **Ctrl+S** opens the comments on whatever row I've selected, so a conversation stays attached to the thing it's about, not lost in email. +> **Ctrl+O** opens the comments on whatever row I've selected, so a conversation stays attached to the thing it's about, not lost in email. **[OPTIONAL]** Quick shot of **Edit Profile** from the Other Functions list, updating the job title. @@ -67,7 +67,7 @@ ## 5. Shopping for data (2:20 – 3:05) ← *the "hero" moment* -**[VISUAL]** Other Functions → **Catalogs / Shop for Data**. Show the Glossary, Digital Product Catalog, Data Dictionary, Business Domain, and Root Collection panels. Select a data product, press `s` to **sample** it, then `u` to **subscribe**. Fill in the subscription request and submit. Then open **Subscriptions** to show it listed. +**[VISUAL]** Other Functions → **Catalogs / Shop for Data**. Show the Glossary, Digital Product Catalog, Data Dictionary, Business Domain, and Root Collection panels. Select a data product, press `Ctrl+L` to **sample** it, then `Ctrl+U` to **subscribe**. Fill in the subscription request and submit. Then open **Subscriptions** to show it listed. **CALLIE:** > This is the part I use most. **Shop for Data** brings together our glossary, the digital product catalog, data dictionaries, and business domains in one place. @@ -110,7 +110,7 @@ - **Tone:** first person, warm, practical, a little wry. It's one colleague showing another, not a feature tour. Each feature should be tied to a job Callie actually needs to get done. - **Screen capture:** use a large terminal font (≥18pt) and a dark theme matching `my_profile.tcss`. Record at 1920×1080 and zoom in on the panel being discussed. Pause about half a second before each keypress so viewers can follow. - **Demo data:** record against the Coco Pharmaceuticals demo data, so the teams, roles, and data products shown match the rest of the series. Pre-create a few to-dos, blog and journal entries, and one subscribable data product. -- **Captions:** burn in the keyboard shortcuts (`Ctrl+T`, `Ctrl+S`, `Ctrl+B`, `s`, `u`) as they're used. +- **Captions:** burn in the keyboard shortcuts (`Ctrl+T`, `Ctrl+O`, `Ctrl+B`, `Ctrl+L`, `Ctrl+U`) as they're used. - **Before recording:** check every step against the current build, especially Change User, Create Profile, and Subscribe, since they've changed recently. - **LinkedIn post (draft):** > Meet My Profile 👋 A terminal app for Egeria users that shows your teams, roles, projects, and to-dos in one place, and lets you shop for governed data and subscribe to it in a few keystrokes. Watch Callie Quartile from Coco Pharmaceuticals take it for a spin: [YouTube link] #Egeria #OpenMetadata #DataGovernance #LFAIData diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py index 0b12122a..ed8784f1 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py @@ -19,7 +19,8 @@ class ViewSubscriptionsScreen(ModalScreen): """View the digital subscriptions created by the current user.""" BINDINGS = [ - ("q", "quit", "Quit"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" @@ -37,7 +38,7 @@ def __init__(self, *args, **kwargs): def compose(self) -> ComposeResult: yield Header(show_clock=True) yield ScrollableContainer( - Static("Your digital subscriptions. Press q to return.", id="view_subsciptions_static"), + Static("Your digital subscriptions. Press Escape to go back.", id="view_subsciptions_static"), DataTable(id="subscriptions_table", zebra_stripes=True, cursor_type="row"), id="view-subscriptions_container", ) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py index 336e1dbf..281e6cb0 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py @@ -242,7 +242,7 @@ def show_comments(self, table_name: str | None = None, row_k: Any = None, """Show comments for the selected table row, or the responses to a selected comment. When selected_comment_guid is given, the screen shows the comments attached to that - comment (its responses) and Ctrl+A adds a new response to it. + comment (its responses) and Ctrl+N adds a new response to it. """ self.log(f"Showing comments for table: {table_name}, row: {row_k}, comment: {selected_comment_guid}") self.push_screen( diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py index 2b3674bf..79710c50 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py @@ -104,7 +104,7 @@ class MyProfileApp(App, TechTypesMixin, ShopForDataMixin, TeamRolesMixin, Elemen # means ctrl+f wins over the Input/TextArea "delete word right" binding on the same key. BINDINGS = [ ("q", "quit", "Quit"), - ("r", "refresh", "Refresh Data"), + ("ctrl+r", "refresh", "Refresh Data"), Binding("ctrl+f", "feedback", "Feedback", priority=True), Binding("f3", "view_feedback_log", "Feedback Log", priority=True), ] diff --git a/tests/micro-tests/my_profile/test_key_bindings.py b/tests/micro-tests/my_profile/test_key_bindings.py new file mode 100644 index 00000000..d71692e2 --- /dev/null +++ b/tests/micro-tests/my_profile/test_key_bindings.py @@ -0,0 +1,130 @@ +""" + PDX-License-Identifier: Apache-2.0 + Copyright Contributors to the ODPi Egeria project. + + Guards the app-wide key binding convention so every My Profile screen behaves the same: + escape back one level (every screen except Main) + q the only plain-letter key: Main Menu on sub-screens, Quit on Main. + Left off text-entry forms, where it would be typed into the Input instead. + ctrl+* every other action, one meaning per key (see docs/my_profile_app_manual.md) +""" + +import inspect + +import pytest +from textual.app import App +from textual.binding import Binding +from textual.screen import Screen +from textual.widgets import Input + +import AddCommentScreen +import AddToElementsScreens +import CreateProfileScreen +import CreateSubscriptionRequestScreen +import EditElementsScreens +import FeedbackScreens +import GenericDataViewScreen +import MainScreen +import MyBookMarksScreen +import MyNetworkScreen +import MyTeamScreen +import SearchForTermScreen +import SelectionOverviewScreen +import ShopForDataScreen +import ShowCommentsScreen +import SplashScreen +import StatusScreen +import TechnologyTypeScreens +import UserIdentitiesScreen +import ViewSubscriptionsScreen +from my_profile_app import MyProfileApp + +MODULES = [ + AddCommentScreen, AddToElementsScreens, CreateProfileScreen, CreateSubscriptionRequestScreen, + EditElementsScreens, FeedbackScreens, GenericDataViewScreen, MainScreen, MyBookMarksScreen, + MyNetworkScreen, MyTeamScreen, SearchForTermScreen, SelectionOverviewScreen, ShopForDataScreen, + ShowCommentsScreen, SplashScreen, StatusScreen, TechnologyTypeScreens, UserIdentitiesScreen, + ViewSubscriptionsScreen, +] + +SCREENS = sorted( + { + cls + for module in MODULES + for _, cls in inspect.getmembers(module, inspect.isclass) + if issubclass(cls, Screen) and cls.__module__ == module.__name__ + }, + key=lambda c: c.__name__, +) + +# ctrl+c is Textual's copy/quit, a terminal sends ctrl+i as Tab, ctrl+m/ctrl+j as Enter and +# ctrl+h as Backspace, and ctrl+p opens the command palette. "return" is not a Textual key name. +UNUSABLE_KEYS = {"ctrl+c", "ctrl+i", "ctrl+m", "ctrl+j", "ctrl+h", "ctrl+p", "return"} + + +def _bindings(cls): + for b in cls.BINDINGS: + key, action = (b.key, b.action) if isinstance(b, Binding) else (b[0], b[1]) + for k in key.split(","): + yield k.strip(), action + + +@pytest.mark.parametrize("screen_cls", SCREENS, ids=lambda c: c.__name__) +def test_only_q_is_a_plain_letter(screen_cls): + letters = [k for k, _ in _bindings(screen_cls) if len(k) == 1 and k != "q"] + assert not letters, f"{screen_cls.__name__} binds plain letter(s) {letters}; use a ctrl key" + + +@pytest.mark.parametrize("screen_cls", [s for s in SCREENS if s is not MainScreen.MainScreen], + ids=lambda c: c.__name__) +def test_escape_goes_back(screen_cls): + assert "escape" in dict(_bindings(screen_cls)), f"{screen_cls.__name__} has no escape binding" + + +@pytest.mark.parametrize("screen_cls", SCREENS + [MyProfileApp], ids=lambda c: c.__name__) +def test_no_unusable_keys(screen_cls): + bad = [k for k, _ in _bindings(screen_cls) if k in UNUSABLE_KEYS] + assert not bad, f"{screen_cls.__name__} binds {bad}, which never reach the app" + + +@pytest.mark.parametrize("screen_cls", SCREENS, ids=lambda c: c.__name__) +def test_every_binding_has_an_action(screen_cls): + for key, action in _bindings(screen_cls): + if action.startswith("app."): + continue + name = action.split("(")[0] + assert hasattr(screen_cls, f"action_{name}"), f"{screen_cls.__name__}: {key} -> missing action_{name}" + + +class _Host(App): + """Pushes one screen and records what it dismisses with.""" + + def __init__(self, screen): + super().__init__() + self._screen = screen + self.dismissed = "not dismissed" + + def on_mount(self): + self.push_screen(self._screen, callback=lambda r: setattr(self, "dismissed", r)) + + +@pytest.mark.parametrize("key, expected", [("ctrl+g", "community"), ("ctrl+l", "identity"), + ("ctrl+r", "role"), ("ctrl+t", "team"), ("escape", 200)]) +async def test_edit_profile_keys_work_while_typing(key, expected): + app = _Host(EditElementsScreens.EditProfileScreen(user_profile={"displayName": "Gary"}, user_GUID="g-1")) + async with app.run_test() as pilot: + await pilot.pause() + app.screen.query_one("#user_job_title", Input).focus() + await pilot.press(key) + await pilot.pause() + assert app.dismissed == expected + + +@pytest.mark.parametrize("key", ["enter", "escape", "q"]) +async def test_status_screen_close_keys(key): + app = _Host(StatusScreen.StatusScreen("Created element 'guid-1'")) + async with app.run_test() as pilot: + await pilot.pause() + await pilot.press(key) + await pilot.pause() + assert app.dismissed == 200 diff --git a/tests/micro-tests/my_profile/test_screens.py b/tests/micro-tests/my_profile/test_screens.py index b2994cd5..6caad2aa 100644 --- a/tests/micro-tests/my_profile/test_screens.py +++ b/tests/micro-tests/my_profile/test_screens.py @@ -764,7 +764,7 @@ async def test_shop_for_data_screen_bookmark_highlighted_row(self): target.focus() target.move_cursor(row=0) await pilot.pause() - await pilot.press("k") + await pilot.press("ctrl+k") await pilot.pause() app.bookmark_table_row.assert_called_once_with(target, row_key) @@ -776,7 +776,7 @@ async def test_shop_for_data_screen_default_config(self): await pilot.pause() assert app.dismissed_result == [210] - @pytest.mark.parametrize("key", ["b", "escape"]) + @pytest.mark.parametrize("key", ["escape", "q"]) @pytest.mark.asyncio async def test_shop_for_data_screen_go_back_key(self, key): # Pressed as soon as the screen opens, without focusing a table first: nothing on the From 0682b9cfebf1cdcd775a2067d2143f8d0d136326 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:28:04 -0500 Subject: [PATCH 14/19] feat(my_profile): make the Roles -> My Team view discoverable, changes to User Identities Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- docs/my_profile_app_manual.md | 8 ++++-- .../DemoCode/My_Profile/MainScreen.py | 4 +-- .../DemoCode/My_Profile/RETURN_CODES.md | 4 +-- .../DemoCode/My_Profile/team_roles_handler.py | 11 +++++++- .../my_profile/test_team_roles_handler.py | 28 +++++++++++++++++-- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/my_profile_app_manual.md b/docs/my_profile_app_manual.md index b7033bce..a5f2187e 100644 --- a/docs/my_profile_app_manual.md +++ b/docs/my_profile_app_manual.md @@ -126,7 +126,7 @@ The header shows your full name, user ID and **Karma Points** (your contribution | **User Associations** | Status or Type, Name, Description, GUID | The communities you belong to | | **My Collections** | Collection Name, Collection Description, Collection GUID | Collections you created (your bookmarks collection is not listed here) | | **Other Functions** | – | A menu of further features ([below](#other-functions-menu)) | -| **Roles** | Role Name, Role Type, Description, GUID | Roles you have been appointed to | +| **Roles** | Role Name, Role Type, Description, GUID | Roles you have been appointed to. Press `Enter` on a TeamLeader or TeamMember role (or click it a second time) to see the other people in that team | | **Teams** | Assignment Type, Team Name, Description, GUID | Teams you are a member of | | **Blogs** | Blog Title, Date, Text, GUID | Your blog entries | | **Journal** | Journal Entry, Date, Text, GUID | Your journal entries | @@ -136,6 +136,8 @@ The header shows your full name, user ID and **Karma Points** (your contribution Click or move into a table to select it; the highlighted row is the "selected row" used by the shortcuts below. Press `ctrl+r` to reload everything from Egeria. The app also reloads automatically when you close an Add screen. +**Seeing your team.** Selecting a TeamLeader or TeamMember role in the Roles table (for example `TeamLeader::Department::0010`) opens the **My Team** screen, which lists the team's members and their assignment types; press `Escape` to return. Selecting any other role shows a warning that it is not a team role. If Egeria returns no members for the team, or the lookup fails, a message says so. + Main screen shortcuts: | Key | Action | @@ -313,7 +315,7 @@ When the app exits it returns a code; screens also use codes to talk to the app. | `412` / `413` | Error / empty result reloading a newly created profile | | `416` | Technology types could not be fetched | | `430` | Incompatible Python or pydantic version | -| `440` | Team, role or glossary lookup error | +| `440` | Glossary term lookup error | The full list is in `RETURN_CODES.md` in the app folder. @@ -328,7 +330,7 @@ The full list is in `RETURN_CODES.md` in the app folder. **Something I added doesn't appear.** Press `ctrl+r` on the main screen. Communities you create are not linked to you yet (see Known Limitations), so they don't appear in User Associations. -**The app quit unexpectedly.** The app only exits by itself in a few cases: a startup problem (codes `402`, `403`, `430`), a failure fetching technology types (`416`), or an error looking up a team (`440`). +**The app quit unexpectedly.** The app only exits by itself in a few cases: a startup problem (codes `402`, `403`, `430`), a failure fetching technology types (`416`), or an error looking up a glossary term (`440`). A failed team lookup is reported as a message instead. ## Known Limitations diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py index 7615def5..aff78958 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py @@ -64,7 +64,7 @@ def compose(self) -> ComposeResult: Static(f"Other Functions"), Static(f"[b]Select a function[/b]"), OptionList( - Option("[] User Identities"), + # Option("[] User Identities"), Option("[] Catalogs/Shop for Data"), Option("[] Edit Profile"), Option("[] Subscriptions"), @@ -78,7 +78,7 @@ def compose(self) -> ComposeResult: id="other_function_container" ) yield ScrollableContainer( - Static("Roles"), + Static("Roles [dim](Enter on a TeamLeader/TeamMember role: show team)[/dim]"), DataTable(id="roles_table"), id="main_roles_container" ) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md b/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md index de3e376e..080c92e5 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md @@ -48,7 +48,7 @@ In accordance with application conventions: | **`423`** | `BUSINESS_DOMAIN_SPEC_ERROR` | Error executing BusinessCapabilities report specification in Shop for Data handler. | `shop_for_data_handler.py` | | **`429`** | `SAMPLE_DATA_RETRIEVAL_ERROR` | Failed to retrieve or parse sample data from Egeria Asset Catalog / Data Engine in Shop for Data handler. | `shop_for_data_handler.py` | | **`430`** | `INCOMPATIBLE_ENVIRONMENT` | App started under a Python whose pydantic is older than pyegeria requires, so request bodies would reach Egeria with their parameters silently missing. The app exits at startup with a message naming the interpreter and how to run from the project venv instead. | `my_profile_app.py`, `profile_utils.py` | -| **`440`** | `TEAM_ROLES_OR_GLOSSARY_ERROR` | Error communicating with Egeria or retrieving team members, user roles, or glossary term details. | `team_roles_handler.py` | +| **`440`** | `TEAM_ROLES_OR_GLOSSARY_ERROR` | Error communicating with Egeria or retrieving glossary term details. (A failed team members lookup is now shown as an error notification and does not exit the app.) | `team_roles_handler.py` | --- @@ -59,4 +59,4 @@ In accordance with application conventions: - **410–419**: Tree resolution errors in selection overview, and technology type data extraction / callback payload errors. - **420–429**: Data retrieval exceptions, report specification execution failures, and template instantiation failures across Shop for Data and Technology Types. - **430**: Incompatible Python environment detected at startup. -- **440**: Team roles and glossary term query communication errors. +- **440**: Glossary term query communication errors. diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py index 0b050795..e9d3fdc3 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py @@ -70,8 +70,17 @@ def handle_roles_table_row_selection(self, event: DataTable.RowSelected) -> Any: if not isinstance(result, (tuple, list)) or len(result) != 5: return team_members_list, team_display_name, team_qualified_name, team_category, team_description = result + if team_display_name is None: + # find_team_members has already told the user why there is no team to show + return else: self.log(f"Selected role is not a TeamLeader or TeamMember: {selected_role_name}") + self.notify( + f"'{selected_role_name}' is not a team role, so there are no team members to show. " + "Select a TeamLeader or TeamMember role.", + timeout=5, + severity="warning", + ) return 201 self.log(f"team_members_list: {team_members_list}") @@ -131,7 +140,7 @@ def find_team_members(self, role_name: str) -> list[Any]: except PyegeriaException as e: print_basic_exception(e) self.log(f"Error retrieving team members: {e!s}") - self.exit(440) + self.notify(f"Could not retrieve the team members from Egeria: {e!s}", timeout=20, severity="error") return [[], None, None, None, None] self.log(f"team_member_data: {team_member_data}") diff --git a/tests/micro-tests/my_profile/test_team_roles_handler.py b/tests/micro-tests/my_profile/test_team_roles_handler.py index 35221cd5..04cfcb76 100644 --- a/tests/micro-tests/my_profile/test_team_roles_handler.py +++ b/tests/micro-tests/my_profile/test_team_roles_handler.py @@ -24,6 +24,7 @@ class DummyTeamRolesApp(TeamRolesMixin): def __init__(self, backend=None): self.pushed_screens = [] + self.notifications = [] self.exit_code = None self.shown_main_screen = False self.log_messages = [] @@ -49,6 +50,9 @@ def push_screen(self, screen, callback=None): def exit(self, code): self.exit_code = code + def notify(self, message, **kwargs): + self.notifications.append((message, kwargs.get("severity"))) + def status_callback(self, *args, **kwargs): pass @@ -76,6 +80,22 @@ def test_handle_roles_table_row_selection_non_team(self): res = app.handle_roles_table_row_selection(mock_event) assert res == 201 assert len(app.pushed_screens) == 0 + # The user is told why nothing opened + assert len(app.notifications) == 1 + message, severity = app.notifications[0] + assert "DataSteward" in message and "not a team role" in message + assert severity == "warning" + + def test_handle_roles_table_row_selection_failed_lookup_opens_no_team_screen(self, backend): + # Always faked: an 'empty' result. Only the StatusScreen explaining it is shown, not MyTeam too. + app = DummyTeamRolesApp(backend) + backend.always_fake("team_roles_handler.exec_report_spec", returns={"kind": "empty"}) + mock_event = MagicMock() + mock_event.data_table.get_row.return_value = ["TeamLeader::Department::101", "PersonRole", "Desc", "g1"] + + app.handle_roles_table_row_selection(mock_event) + + assert [type(screen) for screen, _ in app.pushed_screens] == [StatusScreen] @pytest.mark.live_capable def test_handle_roles_table_row_selection_team_leader( @@ -138,8 +158,12 @@ def test_find_team_members_pyegeria_exception(self, backend): backend.always_fake("team_roles_handler.exec_report_spec", side_effect=PyegeriaException("API Error")) members, dname, qname, cat, desc = app.find_team_members("Department::101::TeamLeader") - assert members == [] - assert app.exit_code == 440 + assert members == [] and dname is None + # Reported to the user; the app keeps running + assert app.exit_code is None + assert len(app.notifications) == 1 + message, severity = app.notifications[0] + assert "API Error" in message and severity == "error" def test_find_team_members_empty_kind(self, backend): # Always faked: an 'empty' result is not something a live server can be asked for. From 3a017a8fde7c7e12e48f3464ab2f7e1e32f400cb Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:05:15 -0500 Subject: [PATCH 15/19] Fix failing github scorecard check Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 7b144fc9..6372c75b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@dc50aa6d9966401f441295b8d21c2302195df180 # v2.4.0 + uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 with: results_file: results.sarif results_format: sarif From fb209970674864d2bbb1c015a6b7a4b603580dc2 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:44:54 -0500 Subject: [PATCH 16/19] Updates to fix bugs found in testing Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../My_Profile/AddToElementsScreens.py | 13 ++++++++----- .../My_Profile/elements_crud_handler.py | 2 +- .../DemoCode/My_Profile/my_profile.tcss | 15 ++++++++++++++- tests/micro-tests/my_profile/test_screens.py | 19 ++++++++++++++++++- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py index 2da63d28..2cce1be9 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py @@ -71,7 +71,8 @@ def compose(self) -> ComposeResult: if self.LINK_LABEL is not None: widgets.append(Horizontal( Static(self.LINK_LABEL), - Switch(value=True, id="link_to_profile"), + Switch(value=self.link_to_profile, id="link_to_profile"), + classes="link_row", )) widgets.append(Horizontal( Button(f"Add {self.ELEMENT_NAME}", id="add_button", variant="primary"), @@ -344,7 +345,7 @@ class AddProjectScreen(BaseAddScreen): ("project_start_date", "Start Date (YYYY-MM-DD)", False), ("project_end_date", "Planned End Date (YYYY-MM-DD)", False), ] - LINK_LABEL = "Add yourself to the project team? Default = True" + LINK_LABEL = "Add yourself to the project team?" def validate(self, values: dict[str, str]) -> str | None: kind = values["project_classification"] @@ -383,7 +384,7 @@ class AddRoleScreen(BaseAddScreen): ("role_name", "Name of Role", True), ("role_description", "Description of Role", True), ] - LINK_LABEL = "Appoint yourself to this role? Default = True" + LINK_LABEL = "Appoint yourself to this role?" def create_element(self, client: Egeria, values: dict[str, str]) -> str: name = values["role_name"] @@ -401,6 +402,8 @@ def create_element(self, client: Egeria, values: dict[str, str]) -> str: return client.create_actor_role(body=body) def link_element(self, client: Egeria, guid: str) -> None: + self.log(f"link person role, GUID: {guid}") + self.log(f"link person role, user GUID: {self.user_guid}") client.link_person_role_to_profile(person_role_guid=guid, person_profile_guid=self.user_guid) @@ -414,7 +417,7 @@ class AddTeamScreen(BaseAddScreen): ("team_name", "Name of Team", True), ("team_description", "Description of Team", True), ] - LINK_LABEL = "Join this team as a member? Default = True" + LINK_LABEL = "Join this team as a member?" def create_element(self, client: Egeria, values: dict[str, str]) -> str: name = values["team_name"] @@ -483,7 +486,7 @@ class AddUserIdentityScreen(BaseAddScreen): ("user_identity_display_name", "Display Name", False), ("user_identity_distinguished_name", "Distinguished Name", False), ] - LINK_LABEL = "Link User Identity to your profile? True or False, Default = True" + LINK_LABEL = "Link this User Identity to your profile?" def create_element(self, client: Egeria, values: dict[str, str]) -> str: user_id = values["user_identity_user_id"] diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py index 281e6cb0..dca318de 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py @@ -285,7 +285,7 @@ async def add_to_tables(self, selected_table: str, selected_row: Any) -> None: callback=getattr(self, callback_name)) def _after_add(self, kind: str, result: Any) -> None: - """Return to the main screen once an Add screen closes, and re-read the profile. + """ Return to the main screen once an Add screen closes, and re-read the profile. Popping back to main (rather than returning to the edit screen the Add screen was opened from) also drops that edit screen's now-stale copy of the rows. diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss index 705c8252..dca325bf 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss @@ -283,4 +283,17 @@ Input { } .empty { background: transparent; /* Invisible structural spacer */ - } \ No newline at end of file + } +/* "Link to my profile" row on the Add screens: without these the label takes the + full row width and the Switch is clipped off the right edge */ +#add_input_container .link_row { + height: auto; +} + +#add_input_container .link_row Static { + width: 1fr; +} + +#add_input_container .link_row Switch { + width: auto; +} diff --git a/tests/micro-tests/my_profile/test_screens.py b/tests/micro-tests/my_profile/test_screens.py index 6caad2aa..646fa5fa 100644 --- a/tests/micro-tests/my_profile/test_screens.py +++ b/tests/micro-tests/my_profile/test_screens.py @@ -9,7 +9,7 @@ from unittest.mock import MagicMock, AsyncMock, patch import pytest from textual.app import App, ComposeResult -from textual.widgets import Input, Button, Markdown, DataTable, Static, Tree +from textual.widgets import Input, Button, Markdown, DataTable, Static, Switch, Tree from StatusScreen import StatusScreen from UserIdentitiesScreen import UserIdentitiesScreen @@ -392,6 +392,23 @@ async def test_add_user_identity_without_link(self, mock_egeria): client.create_user_identity.assert_called_once() client.link_identity_to_profile.assert_not_called() + @pytest.mark.asyncio + @pytest.mark.parametrize("screen_cls", [AddRoleScreen, AddProjectScreen, AddTeamScreen, + AddUserIdentityScreen]) + async def test_link_switch_is_visible_and_toggles(self, mock_egeria, screen_cls): + # Regression: the label used to take the whole row, clipping the Switch off its right + # edge, so users saw only the question and could never turn the link off + app = ScreenTestHostApp(lambda: screen_cls("a_table", "user-guid-123")) + async with app.run_test(size=(120, 40)) as pilot: + screen = app.target_screen + switch = screen.query_one("#link_to_profile", Switch) + row = switch.parent.region + assert row.x <= switch.region.x and switch.region.right <= row.right + assert screen.link_to_profile is True + await pilot.click("#link_to_profile") + await pilot.pause() + assert switch.value is False and screen.link_to_profile is False + @pytest.mark.asyncio async def test_failed_create_keeps_form(self, mock_egeria): from pyegeria import PyegeriaException From 26d2bbe3b468f29acc0b3a8b1858db4842ab0131 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:20:49 -0500 Subject: [PATCH 17/19] Updates to make switches more visible on screens that contain the textual switch widget Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss index dca325bf..896f028d 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss @@ -296,4 +296,9 @@ Input { #add_input_container .link_row Switch { width: auto; + border: solid white; +} +#add_input_container .link_row Switch--slider { + color: chartreuse; + background: cornsilk; } From 4797a950374387fc25e9210c9222d3a7b8a247b8 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:42:03 -0500 Subject: [PATCH 18/19] Renamed my_profile_app.py to my_egeria.py and deleted and otherwise tidied up the code files for the project Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- PYEGERIA_ISSUES.md | 2 +- docs/My-Egeria-Doc.md | 4 +- docs/developer_guide_pyegeria_textual.md | 8 +- docs/my_profile_app_manual.md | 4 +- .../Deprecated/AddAssociationScreen.py | 28 - .../DemoCode/Deprecated/AddBlogEntryScreen.py | 28 - .../DemoCode/Deprecated/AddCommentScreen.py | 58 - .../DemoCode/Deprecated/AddCommunityScreen.py | 28 - .../Deprecated/AddJournalEntryScreen.py | 28 - .../DemoCode/Deprecated/AddProjectScreen.py | 28 - .../DemoCode/Deprecated/AddRoleScreen.py | 28 - .../DemoCode/Deprecated/AddTeamScreen.py | 28 - .../DemoCode/Deprecated/AddTodoScreen.py | 124 - .../Deprecated/EditAssociationsScreen.py | 91 - .../DemoCode/Deprecated/EditBlogsScreen.py | 91 - .../Deprecated/EditCommunitiesScreen.py | 91 - .../Deprecated/EditIdentitiesScreen.py | 91 - .../DemoCode/Deprecated/EditJournalScreen.py | 91 - .../DemoCode/Deprecated/EditProfileScreen.py | 235 -- .../DemoCode/Deprecated/EditProjectsScreen.py | 91 - .../DemoCode/Deprecated/EditRolesScreen.py | 98 - .../DemoCode/Deprecated/EditTeamsScreen.py | 100 - .../DemoCode/Deprecated/EditTodosScreen.py | 91 - .../Deprecated/My_Profile_monolithic/.envrc | 1 - .../AddToElementsScreens.py | 1275 -------- .../CreateProfileScreen.py | 115 - .../CreateSubscriptionRequestScreen.py | 76 - .../EditElementsScreens.py | 935 ------ .../My_Profile_monolithic/MainScreen.py | 168 -- .../My_Profile_monolithic/MyTeamScreen.py | 72 - .../SearchForTermScreen.py | 115 - .../SelectionOverviewScreen.py | 364 --- .../ShopForDataScreen.py | 164 - .../ShowCommentsScreen.py | 226 -- .../My_Profile_monolithic/StatusScreen.py | 68 - .../TechnologyTypeScreens.py | 661 ----- .../UserIdentitiesScreen.py | 102 - .../My_Profile_monolithic/__init__.py | 7 - .../catalog_api_calls.md | 103 - .../My_Profile_monolithic/my_profile.tcss | 169 -- .../my_profile_app-orig.py | 1924 ------------ .../My_Profile_monolithic/my_profile_app.py | 2217 -------------- .../My_Profile_monolithic/sample-data | 126 - .../Add_to_Elements_Screens.py | 644 ---- .../EditElementsScreens.py | 992 ------- .../My_Profile_parked_aug26/MainScreen.py | 163 - .../My_Profile_parked_aug26/README.md | 46 - .../ShowCommentsScreen.py | 210 -- .../TechnologyTypeScreens.py | 662 ----- .../My_Profile_parked_aug26/__init__.py | 0 .../My_Profile_parked_aug26/my_profile_app.py | 2468 --------------- .../Deprecated/ShopForDataScreenOld.py | 164 - .../Deprecated/TechnologyTypeOptionsScreen.py | 258 -- .../TechnologyTypeProcessesScreen.py | 153 - .../TechnologyTypeTemplatesScreen.py | 148 - .../Deprecated/TechnologyTypesScreen.py | 147 - .../Technology_Type_Demo/__init__.py | 7 - .../Technology_Type_Demo/my_connectors.py | 368 --- .../Technology_Type_Demo/my_connectors.tcss | 149 - .../my_connectors_splash_screen.py | 119 - .../DemoCode/Deprecated/Temp Data Display | 733 ----- .../DemoCode/Deprecated/my_connectors.py | 595 ---- .../Deprecated/my_profile_app-orig.py | 1924 ------------ .../DemoCode/Deprecated/my_profile_tui.py | 2199 -------------- .../DemoCode/Deprecated/report_spec.py | 177 -- .../Deprecated/report_spec_details.py | 192 -- .../Deprecated/report_spec_splash_screen.py | 119 - .../DemoCode/Deprecated/report_specs.tcss | 73 - .../Deprecated/report_specs_splash.tcss | 45 - .../DemoCode/Deprecated/tech_type.tcss | 72 - .../DemoCode/Deprecated/tech_type_details.py | 72 - .../DemoCode/Deprecated/tech_type_splash.tcss | 48 - .../Deprecated/tech_type_splash_screen.py | 119 - .../my_egeria/DemoCode/Deprecated/test1.py | 30 - .../DemoCode/Deprecated/test_my_profile.py | 2639 ----------------- .../DemoCode/Deprecated/testing.yaml | 1 - ...ying_DataTables_between_screens.example.py | 111 - .../Examople_Initiate_GAP.java | 33 - ...xmaple_of_returning_data_in_a_product.java | 22 - .../DemoCode/Experimental Code/experiment1.py | 290 -- .../Experimental Code/experiment1.tcss | 134 - .../experiment2-tree-rotated.py | 573 ---- .../Experimental Code/experiment2-tree.py | 435 --- .../DemoCode/Experimental Code/experiment2.py | 331 --- .../DemoCode/Experimental Code/experiment3.py | 293 -- .../DemoCode/Experimental Code/my_network.py | 584 ---- .../My_Profile/AddToElementsScreens.py | 8 +- .../DemoCode/My_Profile/RETURN_CODES.md | 8 +- .../DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md | 2 +- .../DemoCode/My_Profile/catalog_api_calls.md | 2 +- .../{my_profile_app.py => my_egeria.py} | 0 .../DemoCode/My_Profile/my_profile.tcss | 9 +- .../DemoCode/My_Profile/profile_utils.py | 2 +- 93 files changed, 26 insertions(+), 28901 deletions(-) delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddAssociationScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddBlogEntryScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddCommentScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddCommunityScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddJournalEntryScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddProjectScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddRoleScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddTeamScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/AddTodoScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditAssociationsScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditBlogsScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditCommunitiesScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditIdentitiesScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditJournalScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditProfileScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditProjectsScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditRolesScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditTeamsScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/EditTodosScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/.envrc delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/AddToElementsScreens.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/CreateProfileScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/CreateSubscriptionRequestScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/EditElementsScreens.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/MainScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/MyTeamScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/SearchForTermScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/SelectionOverviewScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/ShopForDataScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/ShowCommentsScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/StatusScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/TechnologyTypeScreens.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/UserIdentitiesScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/__init__.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/catalog_api_calls.md delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile.tcss delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile_app-orig.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile_app.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/sample-data delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/Add_to_Elements_Screens.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/EditElementsScreens.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/MainScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/README.md delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/ShowCommentsScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/TechnologyTypeScreens.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/__init__.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/my_profile_app.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/ShopForDataScreenOld.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeOptionsScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeProcessesScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeTemplatesScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypesScreen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/__init__.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors.tcss delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors_splash_screen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/Temp Data Display delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/my_connectors.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/my_profile_app-orig.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/my_profile_tui.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/report_spec.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/report_spec_details.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/report_spec_splash_screen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/report_specs.tcss delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/report_specs_splash.tcss delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/tech_type.tcss delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/tech_type_details.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/tech_type_splash.tcss delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/tech_type_splash_screen.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/test1.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/test_my_profile.py delete mode 100644 my_egeria/my_egeria/DemoCode/Deprecated/testing.yaml delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/Copying_DataTables_between_screens.example.py delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/Examople_Initiate_GAP.java delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/Exmaple_of_returning_data_in_a_product.java delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/experiment1.py delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/experiment1.tcss delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/experiment2-tree-rotated.py delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/experiment2-tree.py delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/experiment2.py delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/experiment3.py delete mode 100644 my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py rename my_egeria/my_egeria/DemoCode/My_Profile/{my_profile_app.py => my_egeria.py} (100%) diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index 02bfbd5f..48972027 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -2879,7 +2879,7 @@ member) shipped as `site-packages/my_egeria/my_egeria/...`. The four console scr point at `my_egeria.main`, `my_egeria.serve`, `my_egeria.DemoCode...`, and the app's own code imports `from my_egeria.`, so none of it resolved outside this repo's dev venv (where the workspace member is installed editable). The portal survived only -because its own script launched `my_profile_app.py` by **file path**, found with a glob +because its own script launched `my_egeria.py` by **file path**, found with a glob for the nested layout. **Fixed:** `where = [".", "my_egeria"]` with `namespaces = false`, so the inner diff --git a/docs/My-Egeria-Doc.md b/docs/My-Egeria-Doc.md index b51df45a..d4a0dffa 100644 --- a/docs/My-Egeria-Doc.md +++ b/docs/My-Egeria-Doc.md @@ -3,7 +3,7 @@ # My Profile Application: Reference Guide -This guide explains how the **My Profile Application** (`my_profile_app.py`) is put together: its screens, the handler mixins behind them, the Egeria calls each flow makes, and the codes screens use to talk to the app. To learn how to *use* the app (configuration, starting it, keys), see the [My Profile App User Manual](my_profile_app_manual.md). +This guide explains how the **My Profile Application** (`my_egeria.py`) is put together: its screens, the handler mixins behind them, the Egeria calls each flow makes, and the codes screens use to talk to the app. To learn how to *use* the app (configuration, starting it, keys), see the [My Profile App User Manual](my_profile_app_manual.md). All source files named here are in `my_egeria/my_egeria/DemoCode/My_Profile/`. @@ -13,7 +13,7 @@ All source files named here are in `my_egeria/my_egeria/DemoCode/My_Profile/`. | File | Contents | | :--- | :--- | -| `my_profile_app.py` | `MyProfileApp`: startup, profile loading, table population, menu dispatch, comments-from-main | +| `my_egeria.py` | `MyProfileApp`: startup, profile loading, table population, menu dispatch, comments-from-main | | `MainScreen.py` | The dashboard: tables, Other Functions menu, row-selection tracking | | `SplashScreen.py` | Welcome card and Change User | | `CreateProfileScreen.py` | First-time profile creation | diff --git a/docs/developer_guide_pyegeria_textual.md b/docs/developer_guide_pyegeria_textual.md index 6ae266a5..2450179c 100644 --- a/docs/developer_guide_pyegeria_textual.md +++ b/docs/developer_guide_pyegeria_textual.md @@ -525,7 +525,7 @@ application, so it shows how the patterns from section 6 hold up at scale. ```bash cd my_egeria/my_egeria/DemoCode/My_Profile -python my_profile_app.py +python my_egeria.py ``` When developing, run it under the Textual dev tools so you can see its log output. Start the @@ -533,7 +533,7 @@ console in one terminal and the app in another: ```bash textual console # terminal 1: shows self.log(...) output -textual run --dev my_profile_app.py # terminal 2 +textual run --dev my_egeria.py # terminal 2 ``` > **Pitfall:** make sure the `textual` command is the one from your project's `.venv` @@ -557,7 +557,7 @@ All files are in | File(s) | Role | |---|---| -| `my_profile_app.py` | The `App` subclass, `MyProfileApp`: startup, loading the profile, filling the main tables, and shared actions such as comments. | +| `my_egeria.py` | The `App` subclass, `MyProfileApp`: startup, loading the profile, filling the main tables, and shared actions such as comments. | | `MainScreen.py` | The main dashboard screen: its layout, and keeping track of which table and row are selected. | | `*Screen.py`, `*Screens.py` | One class per screen or dialog: `SplashScreen`, `ShopForDataScreen`, `MyBookMarksScreen`, `AddCommentScreen`, the `Edit*`/`Add*` screens, and so on. | | `*_handler.py` | **Mixins**, each holding one feature area's logic: `shop_for_data_handler.py`, `tech_types_handler.py`, `team_roles_handler.py`, `elements_crud_handler.py`, `feedback_handler.py`, `bookmarks_handler.py`. | @@ -565,7 +565,7 @@ All files are in | `my_profile.tcss` | All the Textual CSS for the app. | | `RETURN_CODES.md` | The catalogue of numeric codes that screens pass back through `dismiss()`. | -Two design choices keep `my_profile_app.py` manageable. +Two design choices keep `my_egeria.py` manageable. **Feature logic lives in mixins.** The app class is assembled from them: diff --git a/docs/my_profile_app_manual.md b/docs/my_profile_app_manual.md index a5f2187e..fb9ea008 100644 --- a/docs/my_profile_app_manual.md +++ b/docs/my_profile_app_manual.md @@ -75,9 +75,9 @@ From the app folder, inside the project's virtual environment: my_profile # installed console script, runs from any directory # or, from the app folder: cd my_egeria/my_egeria/DemoCode/My_Profile -python my_profile_app.py +python my_egeria.py # or, with the Textual developer console (needs textual-dev, a dev dependency): -textual run --dev my_profile_app.py +textual run --dev my_egeria.py ``` Use Python 3.13: Textual currently fails on Python 3.14 (an assertion in `textual/rlock.py` at startup). diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddAssociationScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddAssociationScreen.py deleted file mode 100644 index 398e7629..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddAssociationScreen.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import pwd -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class AddAssociationScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_role", "Add New Role") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, selected_table, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.selected_table = selected_table \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddBlogEntryScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddBlogEntryScreen.py deleted file mode 100644 index 150b2b99..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddBlogEntryScreen.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import pwd -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class AddBlogEntryScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_role", "Add New Role") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, selected_table, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.selected_table = selected_table \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddCommentScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddCommentScreen.py deleted file mode 100644 index fd47e3cb..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddCommentScreen.py +++ /dev/null @@ -1,58 +0,0 @@ -"""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a screen for a user to add a comment to a community for my_egeria. - -""" - -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import Header, Static, TextArea, Footer -from pyegeria import Egeria - -class AddCommentScreen(ModalScreen): - """ Add a comment to a community the user belongs to """ - - BINDINGS = [ - ("s", "save_comment", "Save Comment"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, community_GUID): - super().__init__() - self.community_GUID = community_GUID - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add Comment to a Community" - - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddCommentScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer( - Static("Enter your comment in the text area."), - Static("When complete use Save Comment to save current commment and add another"), - Static("Use Cancel to return to the previous screen"), - TextArea(id="community_comment-textarea"), - id="comment_textarea_container" - ) - yield Footer() - - def action_save_comment(self): - comment = self.query_one("#community_comment-textarea", TextArea) - if comment: - user_comment = comment.text - self.log(f"User comment: {user_comment}") - self.dismiss([self.community_GUID, user_comment]) - else: - container =self.query_one("#comment_textarea_container", ScrollableContainer) - container.mount(Static(f"[@b aquamarine]Please enter a comment prior to selecting Save Comment[/]")) - container.refresh() - - def action_cancel(self): - self.dismiss(200) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddCommunityScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddCommunityScreen.py deleted file mode 100644 index c4566878..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddCommunityScreen.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import pwd -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class AddCommunityScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_role", "Add New Role") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, selected_table, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.selected_table = selected_table \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddJournalEntryScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddJournalEntryScreen.py deleted file mode 100644 index 92855665..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddJournalEntryScreen.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import pwd -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class AddJournalEntryScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_role", "Add New Role") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, selected_table, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.selected_table = selected_table \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddProjectScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddProjectScreen.py deleted file mode 100644 index a399dbbd..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddProjectScreen.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import pwd -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class AddProjectScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_role", "Add New Role") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, selected_table, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.selected_table = selected_table \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddRoleScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddRoleScreen.py deleted file mode 100644 index b82d285b..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddRoleScreen.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import pwd -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class AddRoleScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_role", "Add New Role") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, selected_table, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.selected_table = selected_table \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddTeamScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddTeamScreen.py deleted file mode 100644 index 0b4e33a0..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddTeamScreen.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import pwd -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class AddTeamScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_role", "Add New Role") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, selected_table, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.selected_table = selected_table \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/AddTodoScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/AddTodoScreen.py deleted file mode 100644 index 7e18a761..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/AddTodoScreen.py +++ /dev/null @@ -1,124 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a user screen to allow the user to add todos to my_egeria. - -""" -import pwd - -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Horizontal -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer, Input, Button -from textual.widgets._option_list import Option - -from pyegeria import Egeria, PyegeriaException, load_app_config, settings - - -class AddTodoScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_todo", "Add New Todo") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, selected_table, *args, **kwargs): - super().__init__(id="add_todo_screen", *args, **kwargs) - self.selected_table = selected_table - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.todo_name = "" - self.todo_description = "" - self.todo_priority = "" - self.todo_guid = "" - - def on_mount(self): - main_screen = self.app.get_screen("main") - - self.todos_table = main_screen.query_one("#todos_table", DataTable) - assert self.todos_table is not None - - self.todos_table.zebra_stripes = True - self.todos_table.cursor_type = "row" - self.todos_table.focus() - - def compose(self) -> ComposeResult: - yield Static("Add Todo Screen") - yield ScrollableContainer( - Static("This screen is intended for the user who wants to add a small number of Todos\n" - "Please ensure that you have filled in all fields before clicking 'Add Todo'\n" - "For bulk additions please use Dr_Egeria instead."), - Input("Name of Todo", id="todo_name"), - Input("Description of Todo", id="todo_description"), - Input("Priority of Todo", id="todo_priority"), - Static("Status will be automatically set to 'REQUESTED'"), - Horizontal( - Button("Add Todo", id="add_todo_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning") - )) - - def action_add_new_todo(self): - """ Call Egeria to add the new todo """ - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - - try: - tclient.create_egeria_bearer_token() - todo_guid = tclient.create_my_todo( - todo_name=self.todo_name, - description=self.todo_description, - priority=self.todo_priority, - activity_status="REQUESTED" - ) - self.log(f"Created ToDo assigned to the current user: {todo_guid}") - except PyegeriaException as e: - self.notify(f"Add todo failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.todo_name = "" - self.todo_description = "" - self.todo_priority = "" - self.todo_guid = "" - self.query_one("#todo_name", Input).clear() - self.query_one("#todo_description", Input).clear() - self.query_one("#todo_priority", Input).clear() - return - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "todo_name": - self.todo_name = event.input.value - if event.input.id == "todo_description": - self.todo_description = event.input.value - if event.input.id == "todo_priority": - self.todo_priority = event.input.value - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#add_todo_button") - def handle_add_todo_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.todo_name and self.todo_description: - self.action_add_new_todo() - else: - self.notify("Please enter at least anew todo name and description", timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.action_quit() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditAssociationsScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditAssociationsScreen.py deleted file mode 100644 index c9321fee..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditAssociationsScreen.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder - -class EditAssociationsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the communities - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_communities_table: DataTable = DataTable(id="communities_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.communities_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Communities", classes="span-3", id="edit_communities_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_communities_container") - yield Footer() - - async def on_mount(self): - self.log("Edit communities screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Communities" - # Populate DataTable - self.my_communities_table.clear(columns=True) - self.my_communities_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_communities_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit communities container in the screen composition objects - self.communities_container = self.query_one("#edit_communities_container", ScrollableContainer) - self.log(f"Communities container found: {self.communities_container}") - # Mount the table into the container - await self.communities_container.mount(self.my_communities_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.communities_container.refresh(layout=True) - self.my_communities_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_communities_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit communities container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#communities_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_communities_table.rows: - rows_with_keys.append((row_key.value, self.my_communities_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_communities_table.remove_row(self.row_key) - self.my_communities_table.refresh() - self.communities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.communities_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.communities_container.refresh(layout=True) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditBlogsScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditBlogsScreen.py deleted file mode 100644 index d352af1f..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditBlogsScreen.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder - -class EditBlogsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the blogs - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_blogs_table: DataTable = DataTable(id="blogs_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.blogs_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Blogs", classes="span-3", id="edit_blogs_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_blogs_container") - yield Footer() - - async def on_mount(self): - self.log("Edit blogs screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Blogs" - # Populate DataTable - self.my_blogs_table.clear(columns=True) - self.my_blogs_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_blogs_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit blogs container in the screen composition objects - self.blogs_container = self.query_one("#edit_blogs_container", ScrollableContainer) - self.log(f"Blogs container found: {self.blogs_container}") - # Mount the table into the container - await self.blogs_container.mount(self.my_blogs_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.blogs_container.refresh(layout=True) - self.my_blogs_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_blogs_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit blogs container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#blogs_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_blogs_table.rows: - rows_with_keys.append((row_key.value, self.my_blogs_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_blogs_table.remove_row(self.row_key) - self.my_blogs_table.refresh() - self.blogs_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.blogs_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.blogs_container.refresh(layout=True) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditCommunitiesScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditCommunitiesScreen.py deleted file mode 100644 index 6da47d4e..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditCommunitiesScreen.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder - -class EditCommunitiesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the communities - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_communities_table: DataTable = DataTable(id="communities_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.communities_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Communities", classes="span-3", id="edit_communities_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_communities_container") - yield Footer() - - async def on_mount(self): - self.log("Edit communities screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Communities" - # Populate DataTable - self.my_communities_table.clear(columns=True) - self.my_communities_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_communities_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit communities container in the screen composition objects - self.communities_container = self.query_one("#edit_communities_container", ScrollableContainer) - self.log(f"Communities container found: {self.communities_container}") - # Mount the table into the container - await self.communities_container.mount(self.my_communities_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.communities_container.refresh(layout=True) - self.my_communities_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_communities_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit communities container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#communities_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_communities_table.rows: - rows_with_keys.append((row_key.value, self.my_communities_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_communities_table.remove_row(self.row_key) - self.my_communities_table.refresh() - self.communities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.communities_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.communities_container.refresh(layout=True) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditIdentitiesScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditIdentitiesScreen.py deleted file mode 100644 index 9dbe08b8..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditIdentitiesScreen.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder - -class EditIdentitiesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the identities - they have in Egeria.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_identities_table: DataTable = DataTable(id="identities_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.identities_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Identities", classes="span-3", id="edit_identities_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_identities_container") - yield Footer() - - async def on_mount(self): - self.log("Edit identities screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Identities" - # Populate DataTable - self.my_identities_table.clear(columns=True) - self.my_identities_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_identities_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit identities container in the screen composition objects - self.identities_container = self.query_one("#edit_identities_container", ScrollableContainer) - self.log(f"Identities container found: {self.identities_container}") - # Mount the table into the container - await self.identities_container.mount(self.my_identities_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.identities_container.refresh(layout=True) - self.my_identities_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_identities_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit identities container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#identities_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_identities_table.rows: - rows_with_keys.append((row_key.value, self.my_identities_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_identities_table.remove_row(self.row_key) - self.my_identities_table.refresh() - self.identities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.identities_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.identities_container.refresh(layout=True) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditJournalScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditJournalScreen.py deleted file mode 100644 index f6067778..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditJournalScreen.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder - -class EditJournalScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the journal - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_journal_table: DataTable = DataTable(id="journal_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.journal_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Journal", classes="span-3", id="edit_journal_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_journal_container") - yield Footer() - - async def on_mount(self): - self.log("Edit journal screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Journal" - # Populate DataTable - self.my_journal_table.clear(columns=True) - self.my_journal_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_journal_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit journal container in the screen composition objects - self.journal_container = self.query_one("#edit_journal_container", ScrollableContainer) - self.log(f"Journal container found: {self.journal_container}") - # Mount the table into the container - await self.journal_container.mount(self.my_journal_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.journal_container.refresh(layout=True) - self.my_journal_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_journal_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit journal container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#journal_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_journal_table.rows: - rows_with_keys.append((row_key.value, self.my_journal_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_journal_table.remove_row(self.row_key) - self.my_journal_table.refresh() - self.journal_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.journal_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.journal_container.refresh(layout=True) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditProfileScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditProfileScreen.py deleted file mode 100644 index 77322b1d..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditProfileScreen.py +++ /dev/null @@ -1,235 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from typing import Any - -from md_processing.v2 import actor_manager - -from pyegeria import Egeria -from pyegeria import load_app_config, settings, MyProfile, PyegeriaException -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Horizontal -from textual.screen import ModalScreen -from textual.widgets import Header, Static, Input, Button, Footer - - -class EditProfileScreen(ModalScreen[Any]): - """Modal screen to create a new user profile in Egeria. - - Dismisses with: - 200 on success or a code for further editing of related collections - 4xx on failure - """ - - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("ctrl+c", "Edit_communities", "Edit communities"), - ("ctrl+i", "Edit_identities", "Edit identities"), - ("ctrl+r", "Edit_roles", "Edit roles"), - ("ctrl+t", "Edit_teams", "Edit teams"), - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, user, - password, - view_server, - platform_url, - karma_points, - user_profile, - user_GUID - ): - super().__init__() - load_app_config() - # config_logging() - self.user_name = user - self.user_password = password - self.view_server = view_server - self.platform_url = platform_url - self.karma_points = karma_points - self.user_profile = user_profile - self.user_GUID = user_GUID - print("Platform:", self.platform_url) - print("View Server:", self.view_server) - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Edit Egeria Profile for user: {self.user_name}" - # retrieve person profile details - - - def compose(self) -> ComposeResult: - self.job_title = str(self.user_profile.get("jobTitle", self.user_profile.get("Job Title", ""))) - self.log(f"Job Title: {self.job_title}") - self.given_names = str(self.user_profile.get("givenNames", "")) - self.log(f"Given Names: {self.given_names}") - self.surname = str(self.user_profile.get("surname", "")) - self.log(f"Surname: {self.surname}") - self.display_name = str(self.user_profile.get("displayName", "")) - self.log(f"Display Name: {self.display_name}") - self.pronouns = str(self.user_profile.get("pronouns", "")) - self.log(f"Pronouns: {self.pronouns}") - self.description = str(self.user_profile.get("description", "")) - self.log(f"Description: {self.description}") - self.employee_id = str(self.user_profile.get("employeeNumber", self.user_profile.get("employeeId", self.user_profile.get("Employee Number", "")))) - self.log(f"Employee ID: {self.employee_id}") - self.preferred_language = str(self.user_profile.get("preferredLanguage", "")) - self.log(f"Preferred Language: {self.preferred_language}") - self.resident_country = str(self.user_profile.get("residentCountry", "")) - self.log(f"Resident Country: {self.resident_country}") - self.time_zone = str(self.user_profile.get("timeZone", "")) - self.log(f"Time Zone: {self.time_zone}") - self.courtesy_title = str(self.user_profile.get("courtesyTitle", "")) - - yield Header(show_clock=True) - yield Static("Update your profile in Egeria", classes="span-3", id="edit_profile_title") - yield Static() - yield ScrollableContainer( - Static("Edit your information as needed:"), - Horizontal( - Static("Courtesy Title:"), - Input(value=self.courtesy_title, placeholder="Courtesy Title", id="user_title")), - Horizontal( - Static("Job Title:"), - Input(value=self.job_title, placeholder="Job Title", id="user_job_title")), - Horizontal( - Static("Given Names:"), - Input(value=self.given_names, placeholder="Given Names", id="user_given_names")), - Horizontal( - Static("Family/Surname:"), - Input(value=self.surname, placeholder="Surname", id="user_family_name")), - Horizontal( - Static("Preferred Name:"), - Input(value=self.display_name, placeholder="Preferred Name", id="user_preferred_name")), - Horizontal( - Static("Pronouns:"), - Input(value=self.pronouns, placeholder="Pronouns", id="user_pronouns")), - Horizontal( - Static("Description:"), - Input(value=self.description, placeholder="Description", id="user_description")), - Horizontal( - Static("Time Zone:"), - Input(value=self.time_zone, placeholder="Time Zone", id="user_time_zone")), - Horizontal( - Static("Employee ID:"), - Input(value=self.employee_id, placeholder="Employee ID", id="user_employee_id")), - Horizontal( - Static("Preferred Language:"), - Input(value=self.preferred_language, placeholder="Preferred Language", id="user_preferred_language")), - Horizontal( - Static("Resident Country:"), - Input(value=self.resident_country, placeholder="Resident Country", id="user_resident_country")), - Button("Edit Profile", id="edit_profile_btn"), - id="edit_profile_form", - ) - yield Static() - yield Footer() - - @on(Button.Pressed, "#edit_profile_btn") - def create_profile(self) -> Any: - """Update profile in Egeria from data provided in Input fields.""" - - input_q_name = "Person" + self.query_one("#user_employee_id", Input).value + \ - self.query_one("#user_resident_country", Input).value + \ - self.query_one("#user_given_names", Input).value + \ - self.query_one("#user_family_name", Input).value - - input_d_name = self.query_one("#user_preferred_name", Input).value - - input_c_title = self.query_one("#user_title", Input).value - - input_g_name = self.query_one("#user_given_names", Input).value - - input_s_name = self.query_one("#user_family_name", Input).value - - input_f_name = self.query_one("#user_preferred_name", Input).value - - input_pronouns = self.query_one("#user_pronouns", Input).value - - input_j_title = self.query_one("#user_job_title", Input).value - - input_e_num = self.query_one("#user_employee_id", Input).value - - input_p_lang = self.query_one("#user_preferred_language", Input).value - - input_r_country = self.query_one("#user_resident_country", Input).value - - input_t_z = self.query_one("#user_time_zone", Input).value - - input_desc = self.query_one("#user_description", Input).value - - self.log(f"Input q_name:{input_q_name}") - self.log(f"Input d_name:{input_d_name}") - self.log(f"Input j_title:{input_j_title}") - self.log(f"Input e_num:{input_e_num}") - self.log(f"Input p_lang:{input_p_lang}") - self.log(f"Input r_country:{input_r_country}") - self.log(f"Input t_z:{input_t_z}") - self.log(f"Input desc:{input_desc}") - - props: dict = {} - props.update({"class": "PersonProperties"}) - if input_q_name: - props.update({"qualifiedName": input_q_name}) - if input_d_name: - props.update({"displayName": input_d_name}) - if input_c_title: - props.update({"courtesyTitle": input_c_title}) - if input_g_name: - props.update({"givenNames": input_g_name}) - if input_s_name: - props.update({"surname": input_s_name}) - if input_f_name: - props.update({"fullName": input_f_name}) - if input_pronouns: - props.update({"pronouns": input_pronouns}) - if input_j_title: - props.update({"jobTitle": input_j_title}) - if input_e_num: - props.update({"employeeNumber": input_e_num}) - if input_p_lang: - props.update({"preferredLanguage": input_p_lang}) - if input_r_country: - props.update({"residentCountry": input_r_country}) - if input_t_z: - props.update({"timeZone": input_t_z}) - if input_desc: - props.update({"description": input_desc}) - - self.log(f"Props: {props}") - - self.update_element_request_body: dict = {"class": "UpdateElementRequestBody", - "isOwnAnchor": True, - "properties": props - } - try: - new_profile_inst = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) - new_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) - new_profile_inst.update_actor_profile(self.user_GUID, self.update_element_request_body) - self.log(f"Profile updated for GUID: {self.user_GUID}") - self.dismiss(200) - return (200) - except PyegeriaException as e: - self.log(f"Error creating profile: {e!s} | request={self.update_element_request_body}") - self.dismiss(401) - return (401) - - def action_quit(self) -> int: - self.dismiss(200) - return(200) - - def action_Edit_communities(self) -> str: - self.dismiss("community") - - def action_Edit_identities(self) -> str: - self.dismiss("identity") - - def action_Edit_roles(self) -> str: - self.dismiss("role") - - def action_Edit_teams(self) -> str: - self.dismiss("team") \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditProjectsScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditProjectsScreen.py deleted file mode 100644 index 1d2e6741..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditProjectsScreen.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder - -class EditProjectsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the projects - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_projects_table: DataTable = DataTable(id="projects_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.projects_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Projects", classes="span-3", id="edit_projects_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_projects_container") - yield Footer() - - async def on_mount(self): - self.log("Edit projects screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Projects" - # Populate DataTable - self.my_projects_table.clear(columns=True) - self.my_projects_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_projects_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit projects container in the screen composition objects - self.projects_container = self.query_one("#edit_projects_container", ScrollableContainer) - self.log(f"Projects container found: {self.projects_container}") - # Mount the table into the container - await self.projects_container.mount(self.my_projects_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.projects_container.refresh(layout=True) - self.my_projects_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_projects_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit projects container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#projects_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_projects_table.rows: - rows_with_keys.append((row_key.value, self.my_projects_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_projects_table.remove_row(self.row_key) - self.my_projects_table.refresh() - self.projects_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.projects_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.projects_container.refresh(layout=True) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditRolesScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditRolesScreen.py deleted file mode 100644 index b828b469..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditRolesScreen.py +++ /dev/null @@ -1,98 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder - -class EditRolesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the roles - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("ctrl+a", "add_role", "Add Role"), - ("ctrl+d", "delete_role", "Delete Role"), - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_roles_table: DataTable = DataTable(id="roles_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.roles_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Roles", classes="span-3", id="edit_roles_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_roles_container") - yield Footer() - - async def on_mount(self): - self.log("Edit roles screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Roles" - # Populate DataTable - self.my_roles_table.clear(columns=True) - self.my_roles_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_roles_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit roles container in the screen composition objects - self.roles_container = self.query_one("#edit_roles_container", ScrollableContainer) - self.log(f"Roles container found: {self.roles_container}") - # Mount the table into the container - await self.roles_container.mount(self.my_roles_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.roles_container.refresh(layout=True) - self.my_roles_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_roles_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit roles container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#roles_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_roles_table.rows: - rows_with_keys.append((row_key.value, self.my_roles_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_role(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_roles_table.remove_row(self.row_key) - self.my_roles_table.refresh() - self.roles_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.roles_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.roles_container.refresh(layout=True) - - def action_add_role(self): - """ The user has selected the add role option """ - self.log("Add role selected") - self.roles_container.mount(Static("Add role functionality not yet implemented!")) - self.roles_container.refresh(layout=True) \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditTeamsScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditTeamsScreen.py deleted file mode 100644 index b9cc0c6f..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditTeamsScreen.py +++ /dev/null @@ -1,100 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder -from textual.widgets._data_table import RowKey - -class EditTeamsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the teams - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("ctrl+t", "add_team", "Add Team"), - ("ctrl+r", "delete_team", "Delete Team") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_teams_table: DataTable = DataTable(id="teams_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.teams_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Teams", classes="span-3", id="edit_teams_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_teams_container") - yield Footer() - - async def on_mount(self): - self.log("Edit teams screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Teams" - # Polulate DataTable - self.my_teams_table.clear(columns=True) - self.my_teams_table.add_columns(*self.columns) - self.my_teams_table.cursor_type="row" - for key_str, cell_values in self.rows_with_keys: - self.my_teams_table.add_row(*cell_values, key=key_str) - try: - # Access the edit teams container in the screen composition objects - self.teams_container = self.query_one("#edit_teams_container", ScrollableContainer) - self.log(f"Teams container found: {self.teams_container}") - # Mount the table into the container - await self.teams_container.mount(self.my_teams_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.teams_container.refresh(layout=True) - self.my_teams_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_teams_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit teams container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#teams_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - self.table = event.data_table - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_teams_table.rows: - rows_with_keys.append((row_key.value, self.my_teams_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_team(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_teams_table.remove_row(self.row_key) - self.my_teams_table.refresh() - self.teams_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.teams_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.teams_container.refresh(layout=True) - - def action_add_team(self): - """ The user has selected the add team option """ - self.log("Add team selected") - self.teams_container.mount(Static("Add team functionality not yet implemented!")) - self.teams_container.refresh(layout=True) \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/EditTodosScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/EditTodosScreen.py deleted file mode 100644 index 2ba5bdd4..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/EditTodosScreen.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder - -class EditTodosScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the todos - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_todos_table: DataTable = DataTable(id="todos_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.todos_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Todos", classes="span-3", id="edit_todos_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_todos_container") - yield Footer() - - async def on_mount(self): - self.log("Edit todos screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Todos" - # Populate DataTable - self.my_todos_table.clear(columns=True) - self.my_todos_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_todos_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit todos container in the screen composition objects - self.todos_container = self.query_one("#edit_todos_container", ScrollableContainer) - self.log(f"Todos container found: {self.todos_container}") - # Mount the table into the container - await self.todos_container.mount(self.my_todos_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.todos_container.refresh(layout=True) - self.my_todos_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_todos_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit todos container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#todos_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_todos_table.rows: - rows_with_keys.append((row_key.value, self.my_todos_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_todos_table.remove_row(self.row_key) - self.my_todos_table.refresh() - self.todos_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.todos_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.todos_container.refresh(layout=True) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/.envrc b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/.envrc deleted file mode 100644 index 175de894..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/.envrc +++ /dev/null @@ -1 +0,0 @@ -layout python diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/AddToElementsScreens.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/AddToElementsScreens.py deleted file mode 100644 index d1fed5bd..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/AddToElementsScreens.py +++ /dev/null @@ -1,1275 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a user screen to allow the user to add todos to my_egeria. - -""" - -import pwd -from datetime import datetime - -import optional -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Horizontal -from textual.screen import ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer, Input, Button, Switch -from textual.widgets._option_list import Option - -from pyegeria import Egeria, PyegeriaException, load_app_config, settings, print_basic_exception - - -class AddTodoScreen(ModalScreen): - """Add Todo Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_todo", "Add New Todo") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, selected_table, user_GUID, *args, **kwargs): - super().__init__(id="add_todo_screen", *args, **kwargs) - self.selected_table = selected_table - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.todo_name = "" - self.todo_description = "" - self.todo_priority = "" - self.todo_guid = "" - self.user_guid = user_GUID - self.link_todo_to_profile = True - - def on_mount(self): - main_screen = self.app.get_screen("main") - - self.todos_table = main_screen.query_one("#todos_table", DataTable) - assert self.todos_table is not None - - self.todos_table.zebra_stripes = True - self.todos_table.cursor_type = "row" - self.todos_table.focus() - - def compose(self) -> ComposeResult: - yield Static("Add Todo Screen") - yield ScrollableContainer( - Static("This screen is intended for the user who wants to add a small number of Todos\n" - "Please ensure that you have filled in all fields before clicking 'Add Todo'\n" - "For bulk additions please use Dr_Egeria instead."), - Input("Name of Todo", id="todo_name"), - Input("Description of Todo", id="todo_description"), - Input("Priority of Todo", id="todo_priority"), - Static("Status will be automatically set to 'REQUESTED'"), - Horizontal( - Static("Link Todo to your profile? True or False, Default = True"), - Switch(value=True, id="link_todo_to_profile") - ), - Horizontal( - Button("Add Todo", id="add_todo_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning") - )) - yield Footer() - - def action_add_new_todo(self): - """ Call Egeria to add the new todo """ - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - - try: - token = tclient.create_egeria_bearer_token(self.user_name, self.user_password) - todo_guid = tclient.create_my_todo( - todo_name=self.todo_name, - description=self.todo_description, - priority=self.todo_priority, - activity_status="REQUESTED" - ) - self.log(f"Created ToDo assigned to the current user: {todo_guid}") - if self.link_todo_to_profile is True: - try: - self.todo_link_guid = tclient.link_todo_to_profile( - todo_guid=todo_guid, - profile_guid=self.user_guid - ) - self.notify(f"Linked ToDo to profile: {self.todo_link_guid}", timeout=10, severity="information") - except PyegeriaException as e: - self.log(f"Link todo to profile failed with return: {e}") - self.notify(f"Link todo to profile failed with return: {e}", timeout=10, severity="error") - except PyegeriaException as e: - self.notify(f"Add todo failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.todo_name = "" - self.todo_description = "" - self.todo_priority = "" - self.todo_guid = "" - self.todo_link_guid = "" - self.query_one("#todo_name", Input).clear() - self.query_one("#todo_description", Input).clear() - self.query_one("#todo_priority", Input).clear() - return - - @on(Switch.Changed, "#link_todo_to_profile") - def handle_link_todo_to_profile_changed(self, event: Switch.Changed): - self.link_todo_to_profile = event.switch.value - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "todo_name": - self.todo_name = event.input.value - if event.input.id == "todo_description": - self.todo_description = event.input.value - if event.input.id == "todo_priority": - self.todo_priority = event.input.value - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#add_todo_button") - def handle_add_todo_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.todo_name and self.todo_description: - self.action_add_new_todo() - else: - self.notify("Please enter at least anew todo name and description", timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.action_quit() - - -class AddAssociationScreen(ModalScreen): - """Add Association (Projects or Communities) Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_association", "Add New Association") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, selected_table, user_GUID, *args, **kwargs): - super().__init__(id="add_association_screen", *args, **kwargs) - self.selected_table = selected_table - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_guid = user_GUID - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.project_name = "" - self.project_description = "" - self.project_classification = "" - self.project_identifier = "" - self.project_start_date = "" - self.project_end_date = "" - self.community_name = "" - self.community_description = "" - self.community_guid = "" - self.community_link_guid = "" - self.project_link_guid = "" - self.link_community_to_profile = True - self.link_project_to_profile = True - - def on_mount(self): - main_screen = self.app.get_screen("main") - - def compose(self) -> ComposeResult: - yield Static("Add Association Screen") - yield ScrollableContainer( - Static("This screen is intended for the user who wants to add a small number of new Projects or Communities to Egeria\n" - "First please select which element type you want to add, Project or Community, and the screen will change accordingly.\n" - "Please ensure that you have filled in all fields before clicking 'Add Association'\n" - "For bulk additions please use Dr_Egeria instead."), - Input("Element type to be added:", id="element_type"), - Horizontal( - Button("Select Element Type", id="select_element_type_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning"), - id="add_association_button_container" - ), - id="element_type_input", - ) - yield Footer() - - def display_add_new_project_screen(self): - input_container = self.query_one("#element_type_input", ScrollableContainer) - input_container.remove_children() - input_container.border_title = "Add New Project" - input_container.mount(Static("Please fill in all input fields before clicking 'Add Project'"), - Input("Name of the project:", id="project_name"), - Input("Description of the project:", id="project_description"), - Static("Classification: Campaign, StudyProject, Task, PersonalProject or Project"), - Input("Project Classification:", id="project_classification"), - Input("Project Identifier:", id="project_identifier"), - Input("Start Date (mm/dd/yyyy):", id="project_start_date"), - Input("Planned End Date (mm/dd/yyyy):", id="project_end_date"), - Horizontal( - Static("Link Project to your profile? True or False, Default = True"), - Switch(value=True, id="link_project_to_profile") - ), - Horizontal( - Button("Add Project", id="add_project_button"), - Button("Quit", id="quit_button") - ) - ) - - def display_add_new_community_screen(self): - input_container = self.query_one("#element_type_input", ScrollableContainer) - input_container.remove_children() - input_container.border_title = "Add New Community" - input_container.mount(Static("Please fill in all input fields before clicking 'Add Project'"), - Input("Name of the community:", id="community_name"), - Input("Description of the community:", id="community_description"), - Horizontal( - Static("Link Community to your profile? True or False, Default = True"), - Switch(value=True, id="link_community_to_profile") - ), - Horizontal( - Button("Add Community", id="add_community_button"), - Button("Quit", id="quit_button") - ) - ) - - def action_add_new_community(self): - """ Call Egeria to add the new community """ - - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - - try: - token = tclient.create_egeria_bearer_token(self.user_name, self.user_password) - - community_body = { - "class": "NewElementRequestBody", - "typeName": "Community", # community type - "initialStatus": "ACTIVE", # initial status of the new element - "properties": { # properties for a Community instance - "class": "CommunityProperties", - "qualifiedName": tclient.__create_qualified_name__("Community", self.community_name), - "displayName": self.community_name, # community name to be displayed in UI - "description": self.community_description, # description of the new element (optional) - } - } - community_guid = tclient.create_community( - body=community_body - ) - self.log(f"Created Community: {community_guid}") - if self.link_community_to_profile is True: - try: - self.community_link_guid = tclient.link_community_to_profile( - community_guid=community_guid, - profile_guid=self.user_guid - ) - self.notify(f"Linked Community to profile: {self.community_link_guid}", timeout=10, severity="information") - except PyegeriaException as e: - self.log(f"Link community to profile failed with return: {e}") - except PyegeriaException as e: - self.notify(f"Add todo failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.community_name = "" - self.community_description = "" - self.community_guid = "" - self.query_one("#community_name", Input).clear() - self.query_one("#community_description", Input).clear() - return - - def action_add_new_project(self): - """ Call Egeria to add the new project """ - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - try: - token = tclient.create_egeria_bearer_token(self.user_name, self.user_password) - project_body = { - "class": "NewElementRequestBody", - "properties": { - "classificationName": "Campaign", # type of project - "displayName": self.project_name, # display name - "description": self.project_description, # description - "identifier": self.project_identifier # business identifier for the project - } - } - project_guid = tclient.create_project( - anchor_guid=None, # The identity of the anchor element for the project. - parent_guid=None, # The identity of the parent element for the project. - parent_relationship_type_name=None,# The type of relationship to the parent element. - parent_at_end1=False, # True if the parent is at end 1 of the relationship. - display_name=self.project_name, # The display name of the project. - description=self.project_description,# A description of the project. - classification_name=self.project_classification, # The type of project - Campaign, StudyProject, Task, PersonalProject or Project. - identifier=self.project_identifier, # A business identifier for the project. - is_own_anchor=False, # True if the project is its own anchor. - status=None, # The project status. - phase=None, # The project phase. - health=None, # The project health. - start_date=self.project_start_date, # The start date of the project. - planned_end_date=self.project_end_date, # The planned completion date of the project. - body=project_body # A dict representing the details of the project to create. - ) - self.log(f"Created project: {project_guid}") - if self.link_project_to_profile is True: - try: - self.project_link_guid = tclient.link_project_to_profile( - profile_guid=self.user_guid, - project_guid=project_guid - ) - self.log(f"Linked project: {self.project_link_guid}") - self.notify(f"Linked project: {self.project_link_guid}", timeout=10, severity="information") - except PyegeriaException as e: - self.notify(f"Link project to profile failed with return: {e}", timeout=10, severity="error") - except PyegeriaException as e: - self.notify(f"Add project failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.project_name = "" - self.project_description = "" - self.project_classification = "" - self.project_identifier = "" - self.project_start_date = "" - self.project_end_date = "" - self.query_one("#project_name", Input).clear() - self.query_one("#project_description", Input).clear() - self.query_one("#project_classification", Input).clear() - self.query_one("#project_identifier", Input).clear() - self.query_one("#project_start_date", Input).clear() - self.query_one("#project_end_date", Input).clear() - return - - @on(Switch.Changed, "#link_project_to_profile") - def handle_link_project_to_profile_changed(self, event: Switch.Changed): - self.link_project_to_profile = event.switch.value - - @on(Switch.Changed, "#link_community_to_profile") - def handle_link_community_to_profile_changed(self, event: Switch.Changed): - self.link_community_to_profile = event.switch.value - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "select_element_type": - self.selected_element_type = event.input.value - if self.selected_element_type == "Project": - self.log.info("Selected element type is Project") - elif self.selected_element_type == "Community": - self.log.info("Selected element type is Community") - else: - self.notify("Invalid element type selected, Only 'Project' or 'Community' are allowed.", severity="error", timeout=15) - elif event.input.id == "project_name": - self.project_name = event.input.value - elif event.input.id == "project_description": - self.project_description = event.input.value - elif event.input.id == "project classification": - self.project_classification = event.input.value - elif event.input.id == "project_identifier": - self.project_identifier = event.input.value - elif event.input.id == "project_start_date": - self.project_start_date = event.input.value - elif event.input.id == "project_end_date": - self.project_end_date = event.input.value - elif event.input.id == "community_name": - self.community_name = event.input.value - elif event.input.id == "community_description": - self.community_description = event.input.value - else: - self.notify("Invalid input field selected.", severity="error", timeout=10) - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#select_element_type_button") - def handle_select_element_type_button(self, event: Button.Pressed): - self.selected_element_type = event.button.id - if self.selected_element_type == "Project": - self.display_add_new_project_screen() - elif self.selected_element_type == "Community": - self.display_add_new_community_screen() - else: - self.notify("Invalid element type selected, Only 'Project' or 'Community' are allowed.", severity="error", timeout=15) - - @on(Button.Pressed, "#add_community_button") - def handle_add_community_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.community_name and self.community_description: - self.action_add_new_community() - else: - self.notify("Please enter new community name and description before selecting Add Community button", timeout=10, severity="error") - - @on(Button.Pressed, "#add_project_button") - def handle_add_project_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.project_name and self.project_description: - self.action_add_new_project() - else: - self.notify("Please enter new community name and description before selecting Add Community button", - timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.action_quit() - -class AddBlogEntryScreen(ModalScreen): - """Add Blog Entry Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_blog", "Add New Blog Entry") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, selected_table, user_GUID, *args, **kwargs): - super().__init__(id="add_blog_screen", *args, **kwargs) - self.selected_table = selected_table - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_guid = user_GUID - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.blog_entry_name = "" - self.blog_entry_description = "" - self.blog_entry_priority = "" - self.blog_entry_guid = "" - self.link_blog_entry_to_profile = True - - def on_mount(self): - main_screen = self.app.get_screen("main") - - self.blogs_table = main_screen.query_one("#blogs_table", DataTable) - assert self.blogs_table is not None - - self.blogs_table.zebra_stripes = True - self.blogs_table.cursor_type = "row" - self.blogs_table.focus() - - def compose(self) -> ComposeResult: - yield Static("Add Blog Entry Screen") - yield ScrollableContainer( - Static("This screen is intended for the user who wants to add a small number of blog entries\n" - "Please ensure that you have filled in all fields before clicking 'Add Blog Entry'\n" - "For bulk additions please use Dr_Egeria instead."), - Static("Name"), - Input("Name of Blog Entry", id="blog_entry_name"), - Static("Text"), - Input("Text of Entry", id="blog_entry_text"), - Static("Situation"), - Input("Situation", id="blog_entry_situation"), - Horizontal( - Static("Link Blog Entry to your profile? True or False, Default = True"), - Switch(value=True, id="link_blog_entry_to_profile") - ), - Horizontal( - Button("Add Blog Entry", id="add_entry_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning") - )) - yield Footer() - - def action_add_new_blog(self): - """ Call Egeria to add the new blog entry """ - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - try: - token = tclient.create_egeria_bearer_token(self.user_name, self.user_password) - body = { - "class": "NewAttachmentRequestBody", - "properties": { - "class": "BlogEntryProperties", - "qualifiedName": f"Blog::Blog-{datetime.now().isoformat()}", - "displayName": self.blog_entry_name, - "situation": self.blog_entry_situation, - "description": self.blog_entry_text, - } - } - blog_entry_response = tclient.blog_my_activity(body=body) - assert isinstance(blog_entry_response, str) - blog_entry_guid = blog_entry_response - self.log(f"Created Blog Entry assigned to the current user: {blog_entry_guid}") - if self.link_blog_entry_to_profile is True: - try: - tclient.link_element_to_profile( - element_guid=self.user_guid, - linked_element_guid=blog_entry_guid, - relationship_type="BlogEntryToUser", - relationship_properties={"class": "BlogEntryToUserProperties"}, - ) - self.notify(f"Linked Blog Entry to Profile: {blog_entry_guid}", timeout=10, severity="information") - except PyegeriaException as e: - self.notify(f"Link Blog Entry to Profile failed with return: {e}", timeout=10, severity="error") - except PyegeriaException as e: - self.notify(f"Add blog entry failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.blog_entry_name = "" - self.blog_entry_description = "" - self.blog_entry_situation = "" - self.blog_entry_guid = "" - self.query_one("#blog_entry_name", Input).clear() - self.query_one("#blog_entry_text", Input).clear() - self.query_one("#blog_entry_situation", Input).clear() - return - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "blog_entry_name": - self.blog_entry_name = event.input.value - if event.input.id == "blog_entry_text": - self.blog_entry_text = event.input.value - if event.input.id == "blog_entry_situation": - self.blog_entry_situation = event.input.value - - @on(Switch.Changed, "#link_blog_entry_to_profile") - def handle_link_blog_entry_to_profile_changed(self, event: Switch.Changed): - self.link_blog_entry_to_profile = event.switch.value - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#add_entry_button") - def handle_add_entry_button(self, event: Button.Pressed): - """ Handle the add button press """ - self.log(f"Blog entry name: {self.blog_entry_name}, blog entry text: {self.blog_entry_text}, blog entry situation: {self.blog_entry_situation}") - if self.blog_entry_name: - self.action_add_new_blog() - else: - self.notify("Please enter at least a new blog name and text", timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.action_quit() - - -class AddCommunityScreen(ModalScreen): - """ Add Community Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_community", "Add New Community") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, selected_table, user_GUID, *args, **kwargs): - super().__init__(id="add_community_screen", *args, **kwargs) - self.selected_table = selected_table - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_guid = user_GUID - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.community_description = "" - self.community_name = "" - self.link_community_to_profile = True - # - def on_mount(self): - main_screen = self.app.get_screen("main") - - def compose(self) -> ComposeResult: - yield Static("Add Community Screen") - yield ScrollableContainer( - Static("This screen is intended for the user who wants to add a small number of Communities\n" - "Please ensure that you have filled in all fields before clicking 'Add Community'\n" - "For bulk additions please use Dr_Egeria instead.\n" - "Following the add, please use the Refresh hot key on the main screen to display the updated data" - ), - Input("Name of Community", id="community_display_name"), - Input("Description of Community", id="community_description"), - Static("Domain Identifier will be automatically set to '0 - All Domains'"), - - Horizontal( - Static("Link Community to your profile? True or False, Default = True"), - Switch(value=True, id="link_community_to_profile") - ), - Horizontal( - Button("Add Community", id="add_community_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning") - ) - ) - yield Footer() - - def action_add_new_community(self): - """ Call Egeria to add the new todo """ - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - - try: - token = tclient.create_egeria_bearer_token(self.user_name, self.user_password) - body = { - "class": "Community", - "properties": { - "typeName": "Community", # the actual Egeria type - "qualifiedName": tclient.__create_qualified_name__("Community", self.community_name), - "displayName": self.community_name, - "description": self.community_description, - "domainIdentifier": 0, # 0 = all domains - } - } - response = tclient.create_governance_definition(body) - if isinstance(response, dict): - self.log(f"Created community with ID {response['guid']}") - if self.link_community_to_profile is True: - try: - tclient.link_element_to_profile( - element_guid=self.user_guid, - linked_element_guid=response["guid"], - relationship_type="CommunityToUser", - relationship_properties={"class": "CommunityToUserProperties"}, - ) - self.notify(f"Linked Community to Profile: {response['guid']}", timeout=10, severity="information") - except PyegeriaException as e: - self.notify(f"Link Community to Profile failed with return: {e}", timeout=10, severity="error") - else: - self.log(f"Error creating community: {response}") - except PyegeriaException as e: - print_basic_exception(e) - finally: - tclient.close_session() - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "community_display_name": - self.community_name = event.input.value - if event.input.id == "community_description": - self.community_description = event.input.value - - @on(Switch.Changed, "#link_community_to_profile") - def handle_link_community_to_profile_changed(self, event: Switch.Changed): - self.link_community_to_profile = event.switch.value - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#add_community_button") - def handle_add_todo_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.community_name and self.community_description: - self.action_add_new_community() - else: - self.notify("Please enter a community name and description", timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.dismiss(200) - -class AddJournalEntryScreen(ModalScreen): - """ Add Journal Entry Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_journal_entry", "Add New Journal Entry") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, selected_table, user_GUID, *args, **kwargs): - super().__init__(id="add_journal_entry_screen", *args, **kwargs) - self.selected_table = selected_table - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_guid = user_GUID - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.journal_entry_title = "" - self.journal_entry_text = "" - self.journal_entry_qualified_name = "" - self.journal_entry_situation = "" - self.link_journal_entry_to_profile = True - - def on_mount(self): - main_screen = self.app.get_screen("main") - - self.journal_table = main_screen.query_one("#journal_table", DataTable) - self.journal_table.zebra_stripes = True - self.journal_table.cursor_type = "row" - self.journal_table.focus() - - def compose(self) -> ComposeResult: - yield Static("Add Todo Screen") - yield ScrollableContainer( - Static("Please ensure that you have filled in all fields before clicking 'Add Journal Entry' Button\n" - "For bulk additions please use Dr_Egeria instead."), - Static("Title"), - Input("Title of Entry", id="journal_entry_title"), - Static("Text"), - Input("Text of Entry", id="journal_entry_text"), - Static("Situation"), - Input("Situation of Entry", id="journal_entry_situation"), - Horizontal( - Button("Add Journal Entry", id="add_journal_entry_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning") - )) - yield Footer() - - def action_add_new_journal_entry(self): - """ Call Egeria to add the new entry """ - - self.qualified_name = f"{self.user_name}:{self.journal_entry_title}:JournalEntry:{datetime.now().isoformat()}" - body = { - "class": "NewAttachmentRequestBody", - "properties": { - "class": "JournalEntryProperties", - "qualifiedName": self.qualified_name, - "displayName": self.journal_entry_title, - "situation": self.journal_entry_situation, - "description": self.journal_entry_text - } - } - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - try: - token = tclient.create_egeria_bearer_token(self.user_name, self.user_password) - journal_entry_response = tclient.journal_my_activity(body=body) - assert isinstance(journal_entry_response, str) - self.log(f"Created Journal Entry for the current user: {journal_entry_response}") - except PyegeriaException as e: - self.notify(f"Add journal entry failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.journal_entry_title = "" - self.journal_entry_text = "" - self.journal_entry_situation = "" - self.qualified_name = "" - self.query_one("#journal_entry_title", Input).clear() - self.query_one("#journal_entry_text", Input).clear() - self.query_one("#journal_entry_situation", Input).clear() - return - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "journal_entry_title": - self.journal_entry_title = event.input.value - if event.input.id == "journal_entry_text": - self.journal_entry_text = event.input.value - if event.input.id == "journal_entry_situation": - self.journal_entry_situation = event.input.value - - @on(Switch.Changed, "#link_journal_entry_to_profile") - def handle_link_journal_entry_to_profile_changed(self, event: Switch.Changed): - self.link_journal_entry_to_profile = event.switch.value - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#add_journal_entry_button") - def handle_add_journal_entry_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.journal_entry_title and self.journal_entry_text: - self.action_add_new_journal_entry() - else: - self.notify("Please enter at least a new title and text for the entry", timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.action_quit() - - -class AddProjectScreen(ModalScreen): - """Add project Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_project", "Add New Project") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, selected_table, user_GUID, *args, **kwargs): - super().__init__(id="add_project_screen", *args, **kwargs) - self.selected_table = selected_table - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.project_name = "" - self.project_description = "" - self.project_identifier = "" - - def on_mount(self): - main_screen = self.app.get_screen("main") - - def compose(self) -> ComposeResult: - yield Static("Add Project Screen") - yield ScrollableContainer( - Static("This screen is intended for the user who wants to add a small number of Projects\n" - "Please ensure that you have filled in all fields before clicking 'Add Project'\n" - "For bulk additions please use Dr_Egeria instead."), - Input("Name of Project", id="project_name"), - Input("Description of project", id="project_description"), - Input("Poject Identifier", id="project_identifier"), - Horizontal( - Static("Link Project to your profile? True or False, Default = True"), - Switch(value=True, id="link_project_to_profile") - ), - Horizontal( - Button("Add Project", id="add_project_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning") - )) - yield Footer() - - def action_add_new_project(self): - """ Call Egeria to add the new project """ - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - body = { - "class": "NewElementRequestBody", - "properties": { - "classificationName": "Project", # The type of project - "displayName": self.project_name, # Display name for the new element. - "description": self.project_description, # Description for the new element. - "identifier": self.project_identifier, # A business identifier for the element (e.g., a unique code). - } - } - - try: - token = tclient.create_egeria_bearer_token(self.user_name, self.user_password) - project_guid = tclient.create_project( - anchor_guid=None, # The identity of the anchor element for the project - parent_guid=None, # The identity of the parent element for the project - parent_relationship_type_name="Project", # The type of relationship to the parent element. - parent_at_end1=False, - display_name=body["properties"]["displayName"], # Display name of the new element. - description=body["properties"]["description"], # Description of the new element (optional). - classification_name=body["properties"]["classificationName"], - identifier=body["properties"]["identifier"], - is_own_anchor=True, # True if this project is its own anchor. - status=None, - phase=None, - health=None, - start_date=None, - planned_end_date=None, - body=body - ) - self.log(f"Created Project assigned to the current user: {project_guid}") - if self.link_project_to_profile is True: - try: - link_project_body = { - "class": "NewRelationshipRequestBody", - "properties": { - "relationshipType": "ProjectLinkedToUser" - } - } - - project_link_guid = tclient.add_to_project_team( - project_guid="PROJECT_GUID", # identity of the project to update - actor_guid=tclient.__create_egeria_user_id__(), - # identity of the actor to add (current logged in user) - assignment_type=None, # Name of the role the actor plays in the project. - description="", # Date at which the actor becomes active in the project. - body=body - ) - self.notify(f"Project linked to current user, response {project_link_guid}") - except PyegeriaException as e: - print_basic_exception(e) - self.notify(f"Link project to user failed with return: {e}", timeout=10, severity="error") - except PyegeriaException as e: - self.notify(f"Add project failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.project_name = "" - self.project_description = "" - self.project_identifier = "" - self.project_guid = "" - self.query_one("#project_name", Input).clear() - self.query_one("#project_description", Input).clear() - self.query_one("#project_identifier", Input).clear() - return - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "project_name": - self.project_name = event.input.value - if event.input.id == "project_description": - self.project_description = event.input.value - if event.input.id == "project_identifier": - self.project_identifier = event.input.value - - @on(Switch.Changed, "#link_project_to_profile") - def handle_link_project_to_profile_changed(self, event: Switch.Changed): - self.link_project_to_profile = event.switch.value - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#add_todo_button") - def handle_add_project_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.project_name and self.project_description and self.project_identifier: - self.action_add_new_project() - else: - self.notify("Please enter a project name, description and identifier", timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.action_quit() - - -class AddRoleScreen(ModalScreen): - """ Add Role Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_role", "Add New Role") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, selected_table, user_GUID, *args, **kwargs): - super().__init__(id="add_role_screen", *args, **kwargs) - self.user_GUID = user_GUID - self.selected_table = selected_table - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.role_name = "" - self.role_description = "" - self.link_self_to_role = True - - def on_mount(self): - main_screen = self.app.get_screen("main") - - def compose(self) -> ComposeResult: - yield Static("Add Role Screen") - yield ScrollableContainer( - Static("This screen is intended for the user who wants to add a small number of Roles\n" - "Please ensure that you have filled in all fields before clicking 'Add Role'\n" - "For bulk additions please use Dr_Egeria instead." - "Once additions are complete Quit and use the Refresh hot key on the main screen to update the display."), - Input("Name of role", id="role_name"), - Input("Description of role", id="role_description"), - Horizontal( - Static("Link to this role in your profile? (True or False, default is True)"), - Switch(value=True, id="link_self_to_role"), - ), - Horizontal( - Button("Add Role", id="add_role_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning") - )) - yield Footer() - - def action_add_new_role(self): - """ Call Egeria to add the new role """ - - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - - token = tclient.create_egeria_bearer_token(self.user_name, self.user_password) - - role_body = { - "class": "NewElementRequestBody", - "isOwnAnchor": True, - "effectiveFrom": "{{$isoTimestamp}}", - "effectiveTo": "{{$isoTimestamp}}", - "properties": { - "class": "ActorRoleProperties", - "typeName": "Role", - "actorProfileGroups": [], - "qualifiedName": tclient.__create_qualified_name__("Role:", self.role_name), - "displayName": self.role_name, - "description": self.role_description, - "scope": "0", - "additionalProperties": { - }, - "extendedProperties": { - } - } - } - - try: - self.role_guid = tclient.create_actor_role(body=role_body) - self.log(f"Created Role by the current user: {self.role_guid}") - if self.link_self_to_role is True: - link_body = { - "class": "NewRelationshipRequestBody", - "externalSourceGUID": "add guid here", - "externalSourceName": "add qualified name here", - "effectiveTime": "{{$isoTimestamp}}", - "forLineage": False, - "forDuplicateProcessing": False, - "properties": { - "class": "PersonRoleAppointmentProperties", - "effectiveFrom": "{{$isoTimestamp}}", - "effectiveTo": "" - } - } - try: - self.link_guid = tclient.link_person_role_to_profile({ - self.role_guid: str, - self.user_GUID: str, - "body": link_body, - }) - except(PyegeriaException) as e: - self.notify(f"Linking role failed with return: {e}", timeout=10, severity="error") - print_basic_exception(e) - else: - self.link_guid = "" - self.log(f"Link to profile not requested: {self.link_self_to_role}") - except PyegeriaException as e: - self.notify(f"Add role failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.role_name = "" - self.role_description = "" - self.role_guid = "" - self.query_one("#role_name", Input).clear() - self.query_one("#role_description", Input).clear() - return - - @on(Switch.Changed, "#link_self_to_role") - def handle_link_self_to_role_changed(self, event: Switch.Changed): - self.link_self_to_role = event.switch.value - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "role_name": - self.role_name = event.input.value - if event.input.id == "role_description": - self.role_description = event.input.value - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#add_role_button") - def handle_add_role_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.role_name and self.role_description: - self.action_add_new_role() - else: - self.notify("Please enter at least anew todo name and description", timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.action_quit() - -class AddTeamScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+a", "add_new_team", "Add New Team") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, selected_table, user_GUID, *args, **kwargs): - super().__init__(id="add_team_screen", *args, **kwargs) - self.selected_table = selected_table - self.user_guid = user_GUID - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.team_name = "" - self.team_description = "" - self.team_identifier = "" - self.team_status = "" - self.team_phase = "" - self.team_health = "" - - def on_mount(self): - main_screen = self.app.get_screen("main") - - def compose(self) -> ComposeResult: - yield Static("Add Team Screen") - yield ScrollableContainer( - Static("This screen is intended for the user who wants to add a small number of new Teams\n" - "Please ensure that you have filled in all fields before clicking 'Add Team'\n" - "For bulk additions please use Dr_Egeria instead.\n" - "Once new teams have been added use the Refresh hot key on the main screen to update the list of teams"), - Input("Name of Team", id="team_name"), - Input("Description of Team", id="team_description"), - Input("Identifier of Team", id="team_identifier"), - Input("Status of Team, 'Active' or 'Deleted'", id="team_status"), - Input("Phase of Team, 'planning', 'in_progress', 'completed'", id="team_phase"), - Input("Health of Team, 'green', 'yellow', 'red'", id="team_health"), - Static("Domain will be automatically set to '0' - all domains"), - Horizontal( - Static("Link Team to your profile? True or False, Default = True"), - Switch(value=True, id="link_team_to_profile") - ), - Horizontal( - Button("Add Team", id="add_team_button", variant="primary"), - Button("Quit", id="quit_button", variant="warning") - )) - - def action_add_new_team(self): - """ Call Egeria to add the new team """ - tclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - - team_body = { - "class": "NewElementRequestBody", - "properties": { - "classificationName": "Team", # classification type - "displayName": self.team_name, - "description": self.team_description, - "identifier": self.team_identifier, - "isOwnAnchor": False, - "status": self.team_status, # project status (e.g. 'active') - "phase": self.team_phase, # project phase (e.g. 'planning', 'in_progress') - "health": self.team_health # project health (e.g. 'green', 'yellow', 'red') - } - } - tclient.create_egeria_bearer_token(self.user_name, self.user_password) - self.link_team_to_user = self.query_one("#link_team_to_profile") - try: - team_guid = tclient.create_team( - anchor_guid=None, - parent_guid=None, - parent_relationship_type_name=None, # optional - parent_at_end1=False, # False by default; indicates the relationship is at end-2 (project) - display_name=self.team_name, # project name - description=self.team_description, # project description - classification_name="Team", # team type - identifier=None, - is_own_anchor=True, # True if this team is its own anchor element - status="Active", # team status (e.g. 'Active' or 'Deleted') - phase=self.team_phase, # team phase (e.g. 'planning', 'in_progress') - health=self.team_health, # team health (e.g. 'green', 'yellow', 'red') - start_date=None, - planned_end_date=None, - body=team_body - ) - self.log(f"Created Team: {team_guid}") - self.notify(f"Team created, guid {team_guid}") - if self.link_team_to_user == True: - try: - # Add the new team GUID to the actor's profile - team_link_body = { - "class": "NewClassificationRequestBody", - "properties": { - "class": "SecurityGroupMembershipProperties", - "groups": [team_guid], - "effectiveFrom": "{{$isoTimestamp}}", - "effectiveTo": "{{$isoTimestamp}}" - }, - "externalSourceGUID": self.user_guid, - "externalSourceName": "", # e.g. 'My Actor Profile' - } - tclient.add_security_group_membershipdef( - user_identity_guid="current_user_GUID", # Replace with actual GUID - security_groups=["new_team_GUID"], - body=team_link_body, # Add the new team to the actor's profile - ) - except PyegeriaException as e: - print_basic_exception(e) - self.notify(f"Add team link to profile failed with return: {e}", timeout=10, severity="error") - except PyegeriaException as e: - self.notify(f"Add team failed with return: {e}", timeout=10, severity="error") - finally: - tclient.close_session() - self.team_name = "" - self.team_description = "" - self.team_identifier = "" - self.team_status = "" - self.team_phase = "" - self.team_health = "" - self.team_guid = "" - self.query_one("#team_name", Input).clear() - self.query_one("#team_description", Input).clear() - self.query_one("#team_identifier", Input).clear() - self.query_one("#team_status", Input).clear() - self.query_one("#team_phase", Input).clear() - self.query_one("#team_health", Input).clear() - return - - @on(Input.Changed) - def handle_input_changed(self, event: Input.Changed): - if event.input.id == "team_name": - self.team_name = event.input.value - if event.input.id == "team_description": - self.team_description = event.input.value - if event.input.id == "team_identifier": - self.team_identifier = event.input.value - if event.input.id == "team_status": - self.team_status = event.input.value - if event.input.id == "team_phase": - self.team_phase = event.input.value - if event.input.id == "team_health": - self.team_health = event.input.value - - @on(Switch.Changed, "#link_team_to_profile") - def handle_link_team_to_profile_changed(self, event: Switch.Changed): - self.link_team_to_profile = event.switch.value - - def action_quit(self): - self.dismiss(200) - - @on(Button.Pressed, "#add_team_button") - def handle_add_team_button(self, event: Button.Pressed): - """ Handle the add button press """ - if self.team_name and self.team_description: - self.action_add_new_team() - else: - self.notify("Please enter all the required fields before pressing the 'Add Team' button", timeout=10, severity="error") - - @on(Button.Pressed, "#quit_button") - def handle_quit_button(self, event: Button.Pressed): - """ Handle the quit button press """ - self.action_quit() diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/CreateProfileScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/CreateProfileScreen.py deleted file mode 100644 index 118560a2..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/CreateProfileScreen.py +++ /dev/null @@ -1,115 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from pyegeria import load_app_config, settings, MyProfile, PyegeriaException -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import Header, Static, Input, Button, Footer - - -class CreateProfileScreen(ModalScreen[int]): - """Modal screen to create a new user profile in Egeria. - - Dismisses with: - 200 on success - 4xx on failure - """ - - BINDINGS = [("q", "dismiss(200)", "Quit")] - CSS_PATH = "my_profile.tcss" - - def __init__(self, user, password, view_server, platform_url): - super().__init__() - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - self.karma_points = 0 - - self.user_name = user - self.user_password = password - self.view_server = view_server - self.platform_url = platform_url - print("Platform:", self.platform_url) - print("View Server:", self.view_server) - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Create Egeria Profile for user: {self.user_name}" - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Create a new profile in Egeria", classes="span-3", id="create_profile_title") - yield Static() - yield ScrollableContainer( - Static("Fill in your information below:"), - Input(placeholder="Courtesy Title", id="user_title"), - Input(placeholder="Given Names", id="user_given_names"), - Input(placeholder="Family Name", id="user_family_name"), - Input(placeholder="Preferred Name", id="user_preferred_name"), - Input(placeholder="Pronouns", id="user_pronouns"), - Input(placeholder="Job Title", id="user_job_title"), - Input(placeholder="Description", id="user_description"), - Input(placeholder="Employee ID", id="user_employee_id"), - Input(placeholder="Preferred Language", id="user_preferred_language"), - Input(placeholder="Resident Country", id="user_resident_country"), - Input(placeholder="Time Zone", id="user_time_zone"), - Button("Create Profile", id="create_profile_btn"), - id="create_profile_form", - ) - yield Static() - yield Footer() - - @on(Button.Pressed, "#create_profile_btn") - def create_profile(self) -> int: - """Create a new profile in Egeria from data provided in Input fields.""" - - self.new_element_request_body:dict = {"class": "NewElementRequestBody", - "isOwnAnchor": True, - "properties": { - "class": "PersonProperties", - "qualifiedName": "Person" + self.query_one("#user_employee_id", - Input).value + - self.query_one("#user_resident_country", - Input).value + - self.query_one("#user_given_names", Input).value + - self.query_one("#user_family_name", Input).value, - "displayName": self.query_one("#user_preferred_name", Input).value, - "courtesyTitle": self.query_one("#user_title", Input).value, - "initials": "PAT", - "givenNames": self.query_one("#user_given_names", Input).value, - "surname": self.query_one("#user_family_name", Input).value, - "fullName": self.query_one("#user_preferred_name", Input).value, - "pronouns": self.query_one("#user_pronouns", Input).value, - "jobTitle": self.query_one("#user_job_title", Input).value, - "employeeNumber": self.query_one("#user_employee_id", Input).value, - "employeeType": "Full-Time", - "preferredLanguage": self.query_one("#user_preferred_language", - Input).value, - "residentCountry": self.query_one("#user_resident_country", - Input).value, - "timeZone": self.query_one("#user_time_zone", Input).value, - "description": self.query_one("#user_description", Input).value, - } - } - try: - new_profile_inst = MyProfile(self.app.view_server, self.app.platform_url, self.app.user, self.app.password) - new_profile_guid = new_profile_inst.add_my_profile(self.new_element_request_body) - self.log(f"Profile created with GUID: {new_profile_guid}") - self.dismiss(200) - return(200) - except PyegeriaException as e: - self.log(f"Error creating profile: {e!s} | request={self.new_element_request_body}") - self.dismiss(401) - return(401) - - def action_quit(self) -> int: - self.dismiss(200) - return(200) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/CreateSubscriptionRequestScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/CreateSubscriptionRequestScreen.py deleted file mode 100644 index 9ba93e37..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/CreateSubscriptionRequestScreen.py +++ /dev/null @@ -1,76 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a Screen Layout and support functions for supporting a user - creating a subscription to a digital data asset. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import Header, Static, Input, Footer - - -class CreateSubscriptionRequestScreen(ModalScreen): - - BINDINGS = [ - ("q", "quit", "Quit"), - ("c", "create_subscription", "Create Subscription") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.valid_status = ["DRAFT", - "PROPOSED", - "ACTIVE", - "SUSPENDED", - "CANCELLED", - "EXPIRED", - "PREPARED", - "APPROVED", - "DISABLED", - "DEPRECATED", - "OTHER"] - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield ScrollableContainer( - Static(f"Please provide the following input as appropriate:"), - Static("Display Name should be short but descriptive, it will also be used to create the Qualified Name as well"), - Input("Display Name for the subscription", id = "sub_display_name"), - Static("By default the user defined status for this subscription will be set to DRAFT, you can override"), - Input("User defined Status - optional", id = "sub_status"), - Static("When all input is completed please click the 'Create Subscription' (c) key"), - id = "subscription_input_container") - yield Footer() - - def on_mount(self) -> None: - self.title = "Create Subscription Request" - self.subtitle = "Enter details for the subscription request" - - def action_quit(self): - self.log("Quitting Create Subscription Request screen") - self.dismiss(200) - - @on(Input.Changed, "#sub_display_name") - def handle_subscription_name_change(self, event): - self.display_name = event.value - - @on(Input.Changed, "#sub_status") - def handle_subscription_status_change(self, event): - self.status = event.value - if self.status not in self.valid_status: - self.status = "DRAFT" - - def action_create_subscription(self): - self.log("Creating subscription request") - self.response = [] - if self.display_name and self.display_name != None and self.display_name != "": - self.response.append(self.display_name) - if self.status and self.status != None and self.status != "": - self.response.append(f"Status: {self.status}") - return self.response \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/EditElementsScreens.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/EditElementsScreens.py deleted file mode 100644 index af34d2a9..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/EditElementsScreens.py +++ /dev/null @@ -1,935 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from typing import Any - -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Horizontal -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder, Input, Button - -from pyegeria import PyegeriaException, Egeria, load_app_config - - -class EditAssociationsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the communities and projects - they belong too.""" - - BINDINGS = [ - ("e", "exit_screen", "Exit"), - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_associations_screen") - self.main_screen = self.query_screen("main_screen") - self.my_communities_table = self.main_screen.query_one("#communities_table", DataTable) - self.my_projects_table = self.main_screen.query_one("#projects_table", DataTable) - self.my_communities_table.id="my_communities_table" - self.my_projects_table.id="my_projects_table" - self.my_communities_table.cursor_type = "row" - self.my_projects_table.cursor_type = "row" - self.my_communities_table.zebra_stripes = True - self.my_projects_table.zebra_stripes = True - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Communities", classes="span-3", id="edit_communities_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer( - DataTable(id="my_communities_table"), - Horizontal( - Button("Remove", id="remove_community", variant="error"), - Button("Edit Values for selected row", id="edit_community", variant="warning"), - ), - id="edit_communities_container") - yield ScrollableContainer( - DataTable(id="my_projects_table"), - Horizontal( - Button("Remove", id="remove_project",variant="error"), - Button("Edit Values for selected row", id="edit_project", variant="warning"), - ), - id="edit_projects_container") - yield Horizontal( - Button("Exit", id="exit_screen", variant="primary"), - id="bottom_buttons_container") - yield Footer() - - async def on_mount(self): - self.log("Edit communities screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Communities" - - @on(DataTable.RowSelected, "#communities_table") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - @on(DataTable.RowSelected, "#projects_table") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - self.dismiss(200) - - def action_remove_community(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_communities_table.remove_row(self.row_key) - self.my_communities_table.refresh() - # self.communities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.communities_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.communities_container.refresh(layout=True) - -class EditBlogsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the blogs - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_blogs_screen") - self.my_blogs_table: DataTable = DataTable(id="blogs_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.blogs_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Blogs", classes="span-3", id="edit_blogs_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_blogs_container") - yield Footer() - - async def on_mount(self): - self.log("Edit blogs screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Blogs" - # Populate DataTable - self.my_blogs_table.clear(columns=True) - self.my_blogs_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_blogs_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit blogs container in the screen composition objects - self.blogs_container = self.query_one("#edit_blogs_container", ScrollableContainer) - self.log(f"Blogs container found: {self.blogs_container}") - # Mount the table into the container - await self.blogs_container.mount(self.my_blogs_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.blogs_container.refresh(layout=True) - self.my_blogs_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_blogs_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit blogs container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#blogs_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_blogs_table.rows: - rows_with_keys.append((row_key.value, self.my_blogs_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_blogs_table.remove_row(self.row_key) - self.my_blogs_table.refresh() - self.blogs_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.blogs_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.blogs_container.refresh(layout=True) - -class EditCommunitiesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the communities - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_communities_screen") - self.my_communities_table: DataTable = DataTable(id="communities_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.communities_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Communities", classes="span-3", id="edit_communities_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_communities_container") - yield Footer() - - async def on_mount(self): - self.log("Edit communities screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Communities" - # Populate DataTable - self.my_communities_table.clear(columns=True) - self.my_communities_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_communities_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit communities container in the screen composition objects - self.communities_container = self.query_one("#edit_communities_container", ScrollableContainer) - self.log(f"Communities container found: {self.communities_container}") - # Mount the table into the container - await self.communities_container.mount(self.my_communities_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.communities_container.refresh(layout=True) - self.my_communities_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_communities_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit communities container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#communities_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_communities_table.rows: - rows_with_keys.append((row_key.value, self.my_communities_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_communities_table.remove_row(self.row_key) - self.my_communities_table.refresh() - self.communities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.communities_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.communities_container.refresh(layout=True) - -class EditIdentitiesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the identities - they have in Egeria.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_identities_screen") - self.my_identities_table: DataTable = DataTable(id="identities_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.identities_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Identities", classes="span-3", id="edit_identities_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_identities_container") - yield Footer() - - async def on_mount(self): - self.log("Edit identities screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Identities" - # Populate DataTable - self.my_identities_table.clear(columns=True) - self.my_identities_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_identities_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit identities container in the screen composition objects - self.identities_container = self.query_one("#edit_identities_container", ScrollableContainer) - self.log(f"Identities container found: {self.identities_container}") - # Mount the table into the container - await self.identities_container.mount(self.my_identities_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.identities_container.refresh(layout=True) - self.my_identities_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_identities_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit identities container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#identities_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_identities_table.rows: - rows_with_keys.append((row_key.value, self.my_identities_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_identities_table.remove_row(self.row_key) - self.my_identities_table.refresh() - self.identities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.identities_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.identities_container.refresh(layout=True) - -class EditJournalScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the journal - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_journal_screen") - self.my_journal_table: DataTable = DataTable(id="journal_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.journal_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Journal", classes="span-3", id="edit_journal_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_journal_container") - yield Footer() - - async def on_mount(self): - self.log("Edit journal screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Journal" - # Populate DataTable - self.my_journal_table.clear(columns=True) - self.my_journal_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_journal_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit journal container in the screen composition objects - self.journal_container = self.query_one("#edit_journal_container", ScrollableContainer) - self.log(f"Journal container found: {self.journal_container}") - # Mount the table into the container - await self.journal_container.mount(self.my_journal_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.journal_container.refresh(layout=True) - self.my_journal_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_journal_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit journal container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#journal_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_journal_table.rows: - rows_with_keys.append((row_key.value, self.my_journal_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_journal_table.remove_row(self.row_key) - self.my_journal_table.refresh() - self.journal_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.journal_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.journal_container.refresh(layout=True) - -class EditProfileScreen(ModalScreen[Any]): - """Modal screen to create a new user profile in Egeria. - - Dismisses with: - 200 on success or a code for further editing of related collections - 4xx on failure - """ - - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("ctrl+c", "Edit_communities", "Edit communities"), - ("ctrl+i", "Edit_identities", "Edit identities"), - ("ctrl+r", "Edit_roles", "Edit roles"), - ("ctrl+t", "Edit_teams", "Edit teams"), - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, user, - password, - view_server, - platform_url, - karma_points, - user_profile, - user_GUID - ): - super().__init__(id="edit_profile_screen") - load_app_config() - self.user_name = user - self.user_password = password - self.view_server = view_server - self.platform_url = platform_url - self.karma_points = karma_points - self.user_profile = user_profile - self.user_GUID = user_GUID - print("Platform:", self.platform_url) - print("View Server:", self.view_server) - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Edit Egeria Profile for user: {self.user_name}" - # retrieve person profile details - - - def compose(self) -> ComposeResult: - self.job_title = str(self.user_profile.get("jobTitle", self.user_profile.get("Job Title", ""))) - self.log(f"Job Title: {self.job_title}") - self.given_names = str(self.user_profile.get("givenNames", "")) - self.log(f"Given Names: {self.given_names}") - self.surname = str(self.user_profile.get("surname", "")) - self.log(f"Surname: {self.surname}") - self.display_name = str(self.user_profile.get("displayName", "")) - self.log(f"Display Name: {self.display_name}") - self.pronouns = str(self.user_profile.get("pronouns", "")) - self.log(f"Pronouns: {self.pronouns}") - self.description = str(self.user_profile.get("description", "")) - self.log(f"Description: {self.description}") - self.employee_id = str(self.user_profile.get("employeeNumber", self.user_profile.get("employeeId", self.user_profile.get("Employee Number", "")))) - self.log(f"Employee ID: {self.employee_id}") - self.preferred_language = str(self.user_profile.get("preferredLanguage", "")) - self.log(f"Preferred Language: {self.preferred_language}") - self.resident_country = str(self.user_profile.get("residentCountry", "")) - self.log(f"Resident Country: {self.resident_country}") - self.time_zone = str(self.user_profile.get("timeZone", "")) - self.log(f"Time Zone: {self.time_zone}") - self.courtesy_title = str(self.user_profile.get("courtesyTitle", "")) - - yield Header(show_clock=True) - yield Static("Update your profile in Egeria", classes="span-3", id="edit_profile_title") - yield Static() - yield ScrollableContainer( - Static("Edit your information as needed:"), - Horizontal( - Static("Courtesy Title:"), - Input(value=self.courtesy_title, placeholder="Courtesy Title", id="user_title")), - Horizontal( - Static("Job Title:"), - Input(value=self.job_title, placeholder="Job Title", id="user_job_title")), - Horizontal( - Static("Given Names:"), - Input(value=self.given_names, placeholder="Given Names", id="user_given_names")), - Horizontal( - Static("Family/Surname:"), - Input(value=self.surname, placeholder="Surname", id="user_family_name")), - Horizontal( - Static("Preferred Name:"), - Input(value=self.display_name, placeholder="Preferred Name", id="user_preferred_name")), - Horizontal( - Static("Pronouns:"), - Input(value=self.pronouns, placeholder="Pronouns", id="user_pronouns")), - Horizontal( - Static("Description:"), - Input(value=self.description, placeholder="Description", id="user_description")), - Horizontal( - Static("Time Zone:"), - Input(value=self.time_zone, placeholder="Time Zone", id="user_time_zone")), - Horizontal( - Static("Employee ID:"), - Input(value=self.employee_id, placeholder="Employee ID", id="user_employee_id")), - Horizontal( - Static("Preferred Language:"), - Input(value=self.preferred_language, placeholder="Preferred Language", id="user_preferred_language")), - Horizontal( - Static("Resident Country:"), - Input(value=self.resident_country, placeholder="Resident Country", id="user_resident_country")), - Button("Edit Profile", id="edit_profile_btn"), - id="edit_profile_form", - ) - yield Static() - yield Footer() - - @on(Button.Pressed, "#edit_profile_btn") - def create_profile(self) -> Any: - """Update profile in Egeria from data provided in Input fields.""" - - input_q_name = "Person" + self.query_one("#user_employee_id", Input).value + \ - self.query_one("#user_resident_country", Input).value + \ - self.query_one("#user_given_names", Input).value + \ - self.query_one("#user_family_name", Input).value - - input_d_name = self.query_one("#user_preferred_name", Input).value - - input_c_title = self.query_one("#user_title", Input).value - - input_g_name = self.query_one("#user_given_names", Input).value - - input_s_name = self.query_one("#user_family_name", Input).value - - input_f_name = self.query_one("#user_preferred_name", Input).value - - input_pronouns = self.query_one("#user_pronouns", Input).value - - input_j_title = self.query_one("#user_job_title", Input).value - - input_e_num = self.query_one("#user_employee_id", Input).value - - input_p_lang = self.query_one("#user_preferred_language", Input).value - - input_r_country = self.query_one("#user_resident_country", Input).value - - input_t_z = self.query_one("#user_time_zone", Input).value - - input_desc = self.query_one("#user_description", Input).value - - self.log(f"Input q_name:{input_q_name}") - self.log(f"Input d_name:{input_d_name}") - self.log(f"Input j_title:{input_j_title}") - self.log(f"Input e_num:{input_e_num}") - self.log(f"Input p_lang:{input_p_lang}") - self.log(f"Input r_country:{input_r_country}") - self.log(f"Input t_z:{input_t_z}") - self.log(f"Input desc:{input_desc}") - - props: dict = {} - props.update({"class": "PersonProperties"}) - if input_q_name: - props.update({"qualifiedName": input_q_name}) - if input_d_name: - props.update({"displayName": input_d_name}) - if input_c_title: - props.update({"courtesyTitle": input_c_title}) - if input_g_name: - props.update({"givenNames": input_g_name}) - if input_s_name: - props.update({"surname": input_s_name}) - if input_f_name: - props.update({"fullName": input_f_name}) - if input_pronouns: - props.update({"pronouns": input_pronouns}) - if input_j_title: - props.update({"jobTitle": input_j_title}) - if input_e_num: - props.update({"employeeNumber": input_e_num}) - if input_p_lang: - props.update({"preferredLanguage": input_p_lang}) - if input_r_country: - props.update({"residentCountry": input_r_country}) - if input_t_z: - props.update({"timeZone": input_t_z}) - if input_desc: - props.update({"description": input_desc}) - - self.log(f"Props: {props}") - - self.update_element_request_body: dict = {"class": "UpdateElementRequestBody", - "isOwnAnchor": True, - "properties": props - } - try: - new_profile_inst = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) - new_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) - new_profile_inst.update_actor_profile(self.user_GUID, self.update_element_request_body) - self.log(f"Profile updated for GUID: {self.user_GUID}") - self.dismiss(200) - return (200) - except PyegeriaException as e: - self.log(f"Error creating profile: {e!s} | request={self.update_element_request_body}") - self.dismiss(401) - return (401) - - def action_quit(self) -> int: - self.dismiss(200) - return(200) - - def action_Edit_communities(self) -> str: - self.dismiss("community") - - def action_Edit_identities(self) -> str: - self.dismiss("identity") - - def action_Edit_roles(self) -> str: - self.dismiss("role") - - def action_Edit_teams(self) -> str: - self.dismiss("team") - - -class EditProjectsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the projects - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_projects_screen") - self.my_projects_table: DataTable = DataTable(id="projects_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.projects_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Projects", classes="span-3", id="edit_projects_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_projects_container") - yield Footer() - - async def on_mount(self): - self.log("Edit projects screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Projects" - # Populate DataTable - self.my_projects_table.clear(columns=True) - self.my_projects_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_projects_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit projects container in the screen composition objects - self.projects_container = self.query_one("#edit_projects_container", ScrollableContainer) - self.log(f"Projects container found: {self.projects_container}") - # Mount the table into the container - await self.projects_container.mount(self.my_projects_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.projects_container.refresh(layout=True) - self.my_projects_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_projects_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit projects container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#projects_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_projects_table.rows: - rows_with_keys.append((row_key.value, self.my_projects_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_projects_table.remove_row(self.row_key) - self.my_projects_table.refresh() - self.projects_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.projects_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.projects_container.refresh(layout=True) - -class EditRolesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the roles - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_roles_screen") - self.my_roles_table: DataTable = DataTable(id="roles_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.roles_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Roles", classes="span-3", id="edit_roles_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_roles_container") - yield Footer() - - async def on_mount(self): - self.log("Edit roles screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Roles" - # Populate DataTable - self.my_roles_table.clear(columns=True) - self.my_roles_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_roles_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit roles container in the screen composition objects - self.roles_container = self.query_one("#edit_roles_container", ScrollableContainer) - self.log(f"Roles container found: {self.roles_container}") - # Mount the table into the container - await self.roles_container.mount(self.my_roles_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.roles_container.refresh(layout=True) - self.my_roles_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_roles_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit roles container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#roles_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_roles_table.rows: - rows_with_keys.append((row_key.value, self.my_roles_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_roles_table.remove_row(self.row_key) - self.my_roles_table.refresh() - self.roles_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.roles_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.roles_container.refresh(layout=True) - -class EditTeamsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the teams - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_teams_screen") - self.my_teams_table: DataTable = DataTable(id="teams_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.teams_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Teams", classes="span-3", id="edit_teams_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_teams_container") - yield Footer() - - async def on_mount(self): - self.log("Edit teams screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Teams" - # Polulate DataTable - self.my_teams_table.clear(columns=True) - self.my_teams_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_teams_table.add_row(*cell_values, key=key_str) - try: - # Access the edit teams container in the screen composition objects - self.teams_container = self.query_one("#edit_teams_container", ScrollableContainer) - self.log(f"Teams container found: {self.teams_container}") - # Mount the table into the container - await self.teams_container.mount(self.my_teams_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.teams_container.refresh(layout=True) - self.my_teams_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_teams_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit teams container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#teams_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_teams_table.rows: - rows_with_keys.append((row_key.value, self.my_teams_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_teams_table.remove_row(self.row_key) - self.my_teams_table.refresh() - self.teams_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.teams_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.teams_container.refresh(layout=True) - -class EditTodosScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the todos - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs, id="edit_todos_screen") - self.my_todos_table: DataTable = DataTable(id="todos_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.todos_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Todos", classes="span-3", id="edit_todos_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_todos_container") - yield Footer() - - async def on_mount(self): - self.log("Edit todos screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Todos" - # Populate DataTable - self.my_todos_table.clear(columns=True) - self.my_todos_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_todos_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit todos container in the screen composition objects - self.todos_container = self.query_one("#edit_todos_container", ScrollableContainer) - self.log(f"Todos container found: {self.todos_container}") - # Mount the table into the container - await self.todos_container.mount(self.my_todos_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.todos_container.refresh(layout=True) - self.my_todos_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_todos_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit todos container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#todos_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_todos_table.rows: - rows_with_keys.append((row_key.value, self.my_todos_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_todos_table.remove_row(self.row_key) - self.my_todos_table.refresh() - self.todos_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.todos_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.todos_container.refresh(layout=True) - diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/MainScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/MainScreen.py deleted file mode 100644 index af208299..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/MainScreen.py +++ /dev/null @@ -1,168 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import pwd -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import Screen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class MainScreen(Screen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+s", "show_comments", "Show Comments for Selected Row"), - ("ctrl+t", "add_todos", "Add Todos"), - ("ctrl+j", "add_journals", "Add Journals"), - ("ctrl+b", "add_blogs", "Add Blogs"), - ("ctrl+c", "add_association", "Add Association"), - ("ctrl+r", "add_role", "Add Role"), - ("ctrl+g", "add_team", "Add Team"), - ("ctrl+m", "add_my_collections", "Add My Collections"), - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.title = "Egeria" - self.sub_title = "My Profile" - self.other_function_list: OptionList = OptionList(id="other_function_list") - self.selected_table = None - self.selected_row = None - - def compose(self) -> ComposeResult: - # place widgets into grid on screen, note sequence determines position! - yield Header(show_clock=True, id="main_header") - - yield ScrollableContainer( - Static("User Associations"), - DataTable(id="associations_table"), - id="main_associations_container" - ) - yield ScrollableContainer( - Static(f"My Collections"), - DataTable(id=("my_collections_table")), - id="my_collections_container" - ) - yield ScrollableContainer( - Static(f"Other Functions"), - Static(f"[b]Select a function[/b]"), - OptionList( - Option("User Identities"), - Option("Catalogs/Shop for Data"), - Option("Edit Profile"), - Option("Subscriptions", disabled=True), - Option("Technology Types"), - Option("User Bookmarks", disabled=True), - id="other_function_list" - ), - id="other_function_container" - ) - yield ScrollableContainer( - Static("Roles"), - DataTable(id="roles_table"), - id="main_roles_container" - ) - yield ScrollableContainer( - Static("Teams"), - DataTable(id="teams_table"), - id="main_teams_container" - ) - yield ScrollableContainer( - Static("Blogs"), - DataTable(id="blogs_table"), - Static("Journal"), - DataTable(id="journal_table"), - Static("To-Dos"), - DataTable(id="todos_table"), - id="main_activities_container" - ) - - yield ScrollableContainer( - Static("User Identity"), - DataTable(id="user_identity_table"), - id="main_identities_container" - ) - - yield Footer(id="main_footer") - - def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted): - """ Collect row and table when the user highlights a row in a table """ - self.selected_row = event.row_key - self.selected_table = event.data_table.id - - def on_data_table_row_selected(self, event: DataTable.RowSelected): - """ Collect row and table when the user selects a row in a table """ - self.selected_row = event.row_key - self.selected_table = event.data_table.id - - async def action_edit_table(self): - """ Edit the selected table """ - if self.selected_table and self.selected_row: - await self.app.edit_tables(self.selected_table, self.selected_row) - else: - self.notify("Please select a row and table to edit.", timeout=5, severity="warning") - - async def action_show_comments(self): - """ Show comments for the selected table """ - if self.selected_table and self.selected_row: - self.app.show_comments(self.selected_table, self.selected_row) - else: - self.notify("Please select a row and table to show comments.", timeout=5, severity="warning") - - async def action_add_to_table(self): - """ Add to the selected table """ - if self.selected_table and self.selected_row: - await self.app.add_to_tables(self.selected_table, self.selected_row) - else: - self.notify("Please select a row and table to add to.", timeout=5, severity="warning") - - async def action_add_todos(self): - """ Add to the selected table """ - self.selected_table = "todos_table" - self.selected_row = 0 - await self.app.add_to_tables(self.selected_table, self.selected_row) - - async def action_add_journals(self): - """ Add to the selected table """ - self.selected_table = "journal_table" - self.selected_row = 0 - await self.app.add_to_tables(self.selected_table, self.selected_row) - - async def action_add_blogs(self): - """ Add to the selected table """ - self.selected_table = "blogs_table" - self.selected_row = 0 - await self.app.add_to_tables(self.selected_table, self.selected_row) - - async def action_add_my_collections(self): - """ Add to the selected table """ - self.selected_table = "my_collections_table" - self.selected_row = 0 - await self.app.add_to_tables(self.selected_table, self.selected_row) - - async def action_add_association(self): - """ Add to the selected table """ - self.selected_table = "associations_table" - self.selected_row = 0 - await self.app.add_to_tables(self.selected_table, self.selected_row) - - async def action_add_role(self): - """ Add to the selected table """ - self.selected_table = "roles_table" - self.selected_row = 0 - await self.app.add_to_tables(self.selected_table, self.selected_row) - - async def action_add_team(self): - """ Add to the selected table """ - self.selected_table = "my_team_table" - self.selected_row = 0 - await self.app.add_to_tables(self.selected_table, self.selected_row) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/MyTeamScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/MyTeamScreen.py deleted file mode 100644 index 06779490..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/MyTeamScreen.py +++ /dev/null @@ -1,72 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, Static, DataTable - - -class MyTeam(ModalScreen): - """ Display a list of team Members for a Team Leader """ - - BINDINGS = [ - ("q", "quit", "Quit"), - ("b", "back", "Go back") - ] - CSS_PATH = "my_profile.tcss" - - def __init__(self, my_team, my_team_properties, leader_name): - super().__init__() - self.title = "My Team" - self.my_team = my_team - self.my_team_display_name = my_team_properties[0] - self.my_team_qualified_name = my_team_properties[1] - self.my_team_category = my_team_properties[2] - self.my_team_description = my_team_properties[3] - self.leader_name = leader_name - self.team_table: DataTable = DataTable(id = "team_table") - - def on_mount(self) -> None: - if self.my_team: - if "Selection Error" in self.my_team[0][0]: - self.team_table.add_column("Error") - self.team_table.add_column("Description") - self.team_table.add_row(self.my_team[0][0], self.my_team[0][1]) - else: - self.team_table.add_column("Name") - self.team_table.add_column("Role") - self.team_table.add_column("GUID") - self.team_table.border_title = f"{self.leader_name}: My Team " - self.team_table.border = True - self.team_table.zebra_stripes = True - self.team_table.cursor = "row" - for member in self.my_team: - self.team_table.add_row(member[0], member[1], member[2]) - else: - self.log(f"No team member data for MyTeam Screen", { "my_team": self.my_team}) - self.team_table.add_column("Error") - self.team_table.add_column("Description") - self.team_table.add_row("No team members found", "No team members returned from Egeria for this team") - - self.team_table.refresh() - - def compose(self) -> ComposeResult: - yield Header() - yield ScrollableContainer( - Static(f"Team Members of: {self.my_team_display_name}, {self.my_team_qualified_name}, {self.my_team_category}, {self.my_team_description}"), - self.team_table - ) - yield Footer() - - def action_quit(self) -> None: - self.dismiss("200") - - def action_back(self) -> None: - self.dismiss( - "201") \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/SearchForTermScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/SearchForTermScreen.py deleted file mode 100644 index 49ca584d..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/SearchForTermScreen.py +++ /dev/null @@ -1,115 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import os - -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Horizontal -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, Static, Input, Placeholder, Button, Markdown -from pyegeria import load_app_config, settings, PyegeriaException, exec_report_spec - - -class SearchForTermScreen(ModalScreen): - - BINDINGS = [("q", "quit", "Quit"), - ("g", "go back", "Go back"), - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, user, password, view_server, platform_url): - super().__init__() - self.search_term = "" - self.view_server = view_server - self.platform_url = platform_url - self.user = user - self.password = password - - def on_mount(self): - self.title = "Egeria" - self.sub_title = "Search for Term" - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield (Horizontal - ( - ScrollableContainer - ( - Static("Search for term, please key in term to search for:"), - Input(id="search_term_input"), - Button("Search", id="search_term_btn"), - id="search_term_input_container" - ), - ScrollableContainer - ( - Static("Result:", id="search_for_term_result_header"), - Placeholder(id="search_term_result"), - id="search_term_result_container" - ))) - yield Footer() - - def action_quit(self) -> None: - self.log("User requested quit") - self.dismiss(200) - - def action_go_back(self) -> None: - self.log("User requested go back") - self.dismiss(201) - - @on(Input.Changed, "#search_term_input") - def handle_search_term_input_changed(self, event: Input.Changed) -> None: - self.search_term = event.value - self.log(f"Search term input changed: {self.search_term}") - - @on(Button.Pressed, "#search_term_btn") - async def handle_search_term(self) -> None: - self.log("User requested search term") - if self.search_term: - # search for the term - search_result = self.search_for_the_term(self.search_term) - self.log(f"Search result: {search_result}") - my_placeholder = self.query_one("#search_term_result") - await my_placeholder.remove() - markdown = Markdown(search_result, id="search_term_result") - markdown.code_indent_guides = False - await self.query_one("#search_term_result_container", ScrollableContainer).mount(markdown) - else: - self.log("No search term provided") - my_placeholder = self.query_one("#search_term_result") - await my_placeholder.remove() - markdown = Markdown(f"# No Content Found for: {self.search_term}", id="search_term_result") - markdown.code_indent_guides = False - await self.query_one("#search_term_result_container", ScrollableContainer).mount(markdown) - - def search_for_the_term(self, search_term): - """ Invoke egeria function to search Egeria for the term """ - try: - self.search_result = exec_report_spec(format_set_name="Glossary-Terms", - output_format="MD", - params={"search_string": search_term, "filter_string": search_term}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password, ) - if isinstance(self.search_result, dict): - if self.search_result.get("kind") == "text": - if self.search_result.get("mimeType") == "text/markdown": - self.search_result = self.search_result.get("content") - else: - self.search_result = "# No Markdown content found for this glossary term" - else: - self.search_result = "# No Markdown content found for this glossary term" - else: - self.search_result = "# No Markdown content found for this glossary term" - - return self.search_result - except PyegeriaException as e: - self.log(f"Error searching for term: {e}") - self.search_result = f"# Error searching for term: {e}" - return self.search_result diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/SelectionOverviewScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/SelectionOverviewScreen.py deleted file mode 100644 index ee72bd36..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/SelectionOverviewScreen.py +++ /dev/null @@ -1,364 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from typing import Any - -from pyegeria import exec_report_spec, PyegeriaException, print_basic_exception -from textual import app, on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Container -from textual.css.query import NoMatches -from textual.screen import Screen, ModalScreen -from textual.widgets import Tree, Header, Static, Placeholder, Footer, TextArea, Markdown - -from StatusScreen import StatusScreen - - -class SelectionOverviewScreen(ModalScreen): - """Screen to display the selection of the user's data sources.""" - BINDINGS = [("q", "quit", "Quit"), - ("s", "subscribe", "Subscribe to Data Source"), - ("b", "back", "Go back")] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, category, view_server, url, user, pwd, data_tree=None, data_samples=None): - self.category = category - self.view_server = view_server - self.platform_url = url - self.user_name = user - self.user_password = pwd - self.data_tree = data_tree - self.sample_data = data_samples - self.glossary_term_var_list: list = [] - - if self.data_tree is None: - if self.category == "glossary": - try: - self.data_tree: Tree = self.app.query_one("#glossary_details_tree", Tree) - except NoMatches: - self.dismiss(411) - return - elif self.category == "catalog": - try: - self.data_tree: Tree = self.app.query_one("#digital_product_catalog_tree", Tree) - except NoMatches: - self.dismiss(412) - return - elif self.category == "dictionary": - try: - self.data_tree: Tree = self.app.query_one("#data_dictionary_tree", Tree) - except NoMatches: - self.dismiss(413) - return - elif self.category == "domain": - try: - self.data_tree: Tree = self.app.query_one("#business_domain_tree", Tree) - except NoMatches: - self.dismiss(414) - return - elif self.category == "collection": - try: - self.data_tree: Tree = self.app.query_one("#root_collection_tree", Tree) - except NoMatches: - self.dismiss(415) - return - else: - # unknown category - self.dismiss(410) - super().__init__() - - def compose(self) -> ComposeResult: - """ Compose the UI components for the SelectionOverviewScreen screen.""" - self.title = f"Shopping for Data, Data Selection:" - self.sub_title = f"Category: {self.category}" - yield Header(show_clock=True) - yield Static("Please select an item from the tree [blink]:[/]", id="instruction_static") - yield ScrollableContainer( - self.data_tree, - id="data_tree_container" - ) - yield Container( - ScrollableContainer( - Placeholder(id="data_details_placeholder"), - id="data_details_placeholder_container" - ), - ScrollableContainer( - Placeholder(id="data_sample_placeholder"), - id="data_sample_placeholder_container" - ) - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss(210) - - def action_back(self) -> None: - """ The back option in the footer has been selected. Dismiss the screen.""" - self.dismiss(200) - - @on(Tree.NodeSelected) - def handle_tree_node_selected(self, event: Tree.NodeSelected): - # Modify to handle for each tree? - self.node_selected = event.node - self.tree_selected = event.node.parent - self.log(f"Tree selected: {self.tree_selected}") - self.log(f"Node selected: {self.node_selected}") - self.node_label = self.node_selected.label - self.node_data = self.node_selected.data - # the provided id can be either a GUID or a qualified name! - # the variable is labeled guid but it could contain a qualified name, both guid and qualified name are strings. - self.node_GUID = self.node_selected.data - self.log(f"Node label: {self.node_label}, GUID: {self.node_GUID}") - if self.category == "glossary": - self.display_selected_term_details(self.node_GUID) - elif self.category == "catalog": - self.display_selected_digital_product(self.node_GUID) - elif self.category == "dictionary": - self.display_selected_data_dictionary(self.node_GUID) - elif self.category == "domain": - self.display_selected_business_domain(self.node_GUID) - elif self.category == "collection": - self.display_selected_root_collection(self.node_GUID) - # elif self.category == "specification": - # self.display_selected_data_specification(self.node_GUID) - - def display_selected_term_details(self, term_GUID) -> Any: - """ The user has selected a glossary term, build a display of the term details, - and show along side the glossary tree """ - self.term_GUID = term_GUID - self.log(f"Selected glossary term GUID: {self.term_GUID}") - try: - self.glossary_term_data = exec_report_spec(format_set_name="Glossary-Terms", - output_format="MD", - params={"search_string": self.term_GUID, "filter_string": self.term_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving glossary details: {e!s}") - self.glossary_term_data = [] - if isinstance(self.glossary_term_data, dict): - if self.glossary_term_data.get("kind") == "text": - if self.glossary_term_data.get("mime") == "text/markdown": - self.glossary_term_data = self.glossary_term_data.get("content") - else: - self.glossary_term_data = "No Markdown content found for this glossary term" - else: - self.glossary_term_data = "No Markdown content found for this glossary term" - else: - self.glossary_term_data = "No Markdown content found for this glossary term" - container = self.query_one("#data_details_placeholder_container") - container.remove_children() - - # if not self.glossary_term_data or self.glossary_term_data == [] or self.glossary_term_data == None or self.glossary_term_data.get("kind") == "empty": - if not self.glossary_term_data or self.glossary_term_data == [] or self.glossary_term_data == None: - self.log(f"No glossary term data returned for GUID: {self.term_GUID}") - container.mount(Static(f"No glossary term data returned for GUID: {self.term_GUID}")) - else: - self.log(f"Glossary term data returned for term: {self.term_GUID}, content: {self.glossary_term_data} \n") - markdown = Markdown(self.glossary_term_data) - markdown.code_indent_guides = False - self.query_one("#data_details_placeholder_container").mount(markdown) - - def display_selected_digital_product(self, digital_product_GUID) -> Any: - """ The user has selected a glossary term, build a display of the term details, - and show along side the glossary tree """ - self.digital_product_GUID = digital_product_GUID - self.log(f"Selected digital product: {self.digital_product_GUID}") - try: - self.digital_product_data = exec_report_spec(format_set_name="Digital-Products-MyE", - output_format="MD", - params={"search_string": self.digital_product_GUID, - "filter_string": self.digital_product_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving digital product details: {e!s}") - self.digital_product_data = [] - - container = self.query_one("#data_details_placeholder_container") - container.remove_children() - - if not self.digital_product_data or self.digital_product_data == [] or self.digital_product_data == None: - self.log(f"No digital product data returned for: {self.digital_product_GUID}") - container.mount(Static(f"No digital product data returned for: {self.digital_product_GUID}")) - else: - self.log(f"Digital product data returned for: {self.digital_product_GUID}") - if isinstance(self.digital_product_data, dict) and self.digital_product_data.get("kind") == "empty": - self.digital_product_data = "# No content found for this digital product" - elif isinstance(self.digital_product_data, dict) and self.digital_product_data.get("kind") == "text": - if self.digital_product_data.get("mime") == "text/markdown": - self.digital_product_data = self.digital_product_data.get("content") - else: - self.digital_product_data = "# No Markdown content found for this digital product" - else: - self.digital_product_data = "# No dict content found for this digital product" - - markdown = Markdown(str(self.digital_product_data)) - container.mount(markdown) - sample_container = self.query_one("#data_sample_placeholder_container") - sample_container.remove_children() - sample_markdown= Markdown(str(self.sample_data)) - sample_container.mount(sample_markdown) - - def display_selected_data_dictionary(self, data_dictionary_GUID) -> Any: - """ The user has selected a data dictionary, build a display of the dictionary details, - and show along side the dictionary tree """ - self.data_dictionary_GUID = data_dictionary_GUID - self.log(f"Selected data dictionary GUID: {self.data_dictionary_GUID}") - try: - self.data_dictionary_data = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="MD", - params={"search_string": self.data_dictionary_GUID, - "filter_string": self.data_dictionary_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving data dictionary details: {e!s}") - self.data_dictionary_data = [] - - container = self.query_one("#data_details_placeholder_container") - container.remove_children() - - if not self.data_dictionary_data or self.data_dictionary_data == []: - self.log(f"No data dictionary data returned for GUID: {self.data_dictionary_GUID}") - container.mount(Static(f"No data dictionary data returned for GUID: {self.data_dictionary_GUID}")) - else: - self.log(f"Data dictionary data returned for GUID: {self.data_dictionary_GUID}") - if isinstance(self.data_dictionary_data, dict) and self.data_dictionary_data.get("kind") == "text": - if self.data_dictionary_data.get("mime") == "text/markdown": - self.data_dictionary_data = self.data_dictionary_data.get("content") - else: - self.data_dictionary_data = "No Markdown content found for this data dictionary" - markdown = Markdown(str(self.data_dictionary_data)) - markdown.code_indent_guides = False - container.mount(markdown) - container.refresh() - - def display_selected_business_domain(self, business_domain_GUID) -> int: - """ The user has selected a business domain, build a display of the domain details, - and show along side the glossary tree """ - self.business_domain_GUID = business_domain_GUID - self.log(f"Selected business domain GUID: {self.business_domain_GUID}") - try: - self.business_domain_data = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params={"search_string": self.business_domain_GUID, - "filter_string": self.business_domain_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving business domain details: {e!s}") - self.business_domain_data = [] - - container = self.query_one("#data_details_placeholder_container") - container.remove_children() - - if not self.business_domain_data or self.business_domain_data == []: - self.log(f"No business domain data returned for GUID: {self.business_domain_GUID}") - container.mount(Static(f"No business domain data returned for GUID: {self.business_domain_GUID}")) - else: - self.log(f"Business domain data returned for GUID: {self.business_domain_GUID}") - text_area = TextArea(f"Business domain data returned for GUID: {self.business_domain_GUID}", - id="business_domain_details_text_area", - read_only=True) - container.mount(text_area) - text_area.text = str(self.business_domain_data) - - def display_selected_data_specification(self, data_specification_GUID) -> int: - """ The user has selected a data specification, build a display of the specification details - and show alongside data specifications tree """ - - self.data_specification_qualified_name = data_specification_GUID - self.log(f"Data specification selected: {self.data_specification_qualified_name}") - try: - self.data_specification_data = exec_report_spec(format_set_name="Data-Specification", - output_format="DICT", - params={"search_string": self.data_specification_qualified_name, - "filter_string": self.data_specification_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving data specification details: {e!s}") - self.data_specification_data = [] - - container = self.query_one("#data_details_placeholder_container") - container.remove_children() - - if not self.data_specification_data or self.data_specification_data == []: - self.log(f"No data specification data returned for qualified name: {self.data_specification_qualified_name}") - container.mount(Static(f"No data specification data returned for qualified name: {self.data_specification_qualified_name}")) - else: - self.log(f"Data specification data returned for qualified name: {self.data_specification_qualified_name}") - text_area = TextArea(f"Data specification data returned for qualified name: {self.data_specification_qualified_name}", - id="data_specification_details_text_area", - read_only=True) - container.mount(text_area) - text_area.text = str(self.data_specification_data) - - def display_selected_root_collection(self, root_collection_GUID) -> int: - """ The user has selected a root collection, build a display of the collection details, - and show alongside the collection tree """ - self.root_collection_GUID = root_collection_GUID - self.log(f"Selected root collection GUID: {self.root_collection_GUID}") - try: - self.root_collection_data = exec_report_spec(format_set_name="Collections", - output_format="MD", - params={"search_string": self.root_collection_GUID, - "filter_string": self.root_collection_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - - except PyegeriaException as e: - self.log(f"Error retrieving root collection data: {e}") - self.collection_data = [] - - container = self.query_one("#data_details_placeholder_container") - container.remove_children() - - if not self.root_collection_data or self.root_collection_data == [] or self.root_collection_data == None: - self.log(f"No Root collection data returned for GUID: {self.root_collection_GUID}") - container.mount(Static(f"No Root collection data returned for GUID: {self.root_collection_GUID}")) - else: - self.log(f"Root collection data returned for GUID: {self.root_collection_GUID}") - if isinstance(self.root_collection_data, dict) and self.root_collection_data.get("kind") == "text": - if self.root_collection_data.get("mime") == "text/markdown": - self.root_collection_data = self.root_collection_data.get("content") - else: - self.root_collection_data = "No Markdown content found for this root collection" - markdown = Markdown(str(self.root_collection_data)) - markdown.code_indent_guides = False - container.mount(markdown) - container.refresh() - - def action_subscribe(self): - """ The subscribe option in the footer has been selected. """ - self.log(f"Subscribe to data selected: {self.node_GUID}") - if self.node_GUID == None: - self.log(f"No node GUID selected for subscription") - self.notify("The node selected for subscription has no Digital Products available, please select another node", timeout=10, severity="error") - return - else: - # return a completion code, the guid/qualified name of the selected item and the id of the tree it was selected from - self.dismiss([211, self.node_GUID, self.tree_selected]) - - diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/ShopForDataScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/ShopForDataScreen.py deleted file mode 100644 index 02704286..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/ShopForDataScreen.py +++ /dev/null @@ -1,164 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.events import Action -from textual.screen import Screen -from textual.widgets import DataTable, Header, TextArea, Static, Footer - - -class ShopForDataScreen(Screen): - """ Screen to Present a choice of different data sources to the user.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("s", "sample_data_source", "Sample data source"), - ("b", "back", "Go back")] - - CSS_PATH = "my_profile.tcss" - - def __init__ (self, glossary_table, digital_product_catalog_table, data_dictionary_table, business_domain_table, root_collection_table, - user_name, user_password, view_server, platform_url): - """Initialize the ShopForDataScreen screen.""" - self.glossary_table: DataTable = glossary_table - self.digital_product_catalog_table: DataTable = digital_product_catalog_table - self.data_dictionary_table: DataTable = data_dictionary_table - self.business_domain_table: DataTable = business_domain_table - self.root_collection_table: DataTable = root_collection_table - self.user_name = user_name - self.user_password = user_password - self.view_server = view_server - self.platform_url = platform_url - super().__init__() - - def compose(self) -> ComposeResult: - """ Compose the UI components for the ShopForDataScreen screen.""" - yield Header(show_clock=True) - yield TextArea(f"\n\n\nPlease click on a line to select a data source:") - yield ScrollableContainer( - Static("Glossary"), - self.glossary_table) - yield ScrollableContainer( - Static("Digital Product Catalog"), - self.digital_product_catalog_table) - yield ScrollableContainer( - Static("Data Dictionary"), - self.data_dictionary_table) - yield ScrollableContainer( - Static("Business Domain"), - self.business_domain_table) - yield ScrollableContainer( - Static("Root Collection"), - self.root_collection_table) - yield Footer() - - - def on_mount(self) -> None: - self.header = f"Egeria Data Sources for user {self.user_name}" - self.sub_header = "Shop for Data" - - # @on(DataTable.RowSelected) - # def handle_data_table_highlight(self, event: DataTable.RowHighlighted): - # self.row_highlighted = event.row_key - # self.cursor_row_highlighted = event.cursor_row - # self.data_table_highlighted = event.data_table - # self.log(f"Row highlighted: {self.row_highlighted}") - - - @on(DataTable.RowSelected, "#glossary_table") - def handle_glossary_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.glossary_table.get_row(row_selected) - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["glossary", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#digital_product_catalog_table") - def handle_digital_product_catalog_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - self.log(f"Digital product row selected: {row_selected}, event: {event}") - row_values = self.query_one("#digital_product_catalog_table", DataTable).get_row(event.row_key) - self.log(f"Digital product row values: {row_values}") - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["catalog", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#data_dictionary_table") - def handle_data_dictionary_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#data_dictionary_table", DataTable).get_row(event.row_key) - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["dictionary", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#business_domain_table") - def handle_business_domain_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#business_domain_table", DataTable).get_row(event.row_key) - row_qualified_name = row_values[0] - row_type_name = row_values[1] - row_guid = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, type name: {row_type_name}, guid: {row_guid}") - self.dismiss (["domain", row_qualified_name, row_type_name]) - - @on(DataTable.RowSelected, "#data_specification_table") - def handle_data_specification_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#data_specification_table", DataTable).get_row(event.row_key) - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["specification", row_qualified_name, row_display_name]) - - @on (DataTable.RowSelected, "#root_collection_table") - def handle_root_collection_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#root_collection_table", DataTable).get_row(event.row_key) - row_qualified_name = row_values[0] - row_description = row_values[1] - row_GUID = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, description: {row_description}, GUID: {row_GUID}") - self.dismiss (["collection", row_qualified_name, row_description]) - - # @on(DataTable.RowHighlighted) - # def handle_row_highlighted(self, event: DataTable.RowHighlighted) -> None: - # """ Handle the highlighting of a row in the DataTable.""" - # table = event.data_table - # row_key = event.row_key - # row_values = self.query_one("#"+str(table), DataTable).get_row(row_key) - # row_qualified_name = row_values[0] - # row_description = row_values[1] - # row_GUID = row_values[2] - # self.log(f"Table {table} Row highlighted: {row_key}, values: {row_values}, qualified name: {row_qualified_name}, description: {row_description}, GUID: {row_GUID}") - - def action_back(self) -> None: - """ The back option in the footer has been selected. Dismiss the screen.""" - self.dismiss([200]) - - def action_search_for_term(self) -> None: - """ The search for term option in the footer has been selected. Dismiss the screen.""" - self.dismiss([201]) - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss([210]) - - # def action_subscribe_to_data(self) -> None: - # """ The subscribe to data option in the footer has been selected.""" - # self.dismiss([211]) - - def action_sample_data_source(self): - """ The sample data source option in the footer has been selected.""" - self.dismiss([212, self.row_highlighted, self.cursor_row_highlighted, self.data_table_highlighted]) \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/ShowCommentsScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/ShowCommentsScreen.py deleted file mode 100644 index 5542a489..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/ShowCommentsScreen.py +++ /dev/null @@ -1,226 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from typing import Any - -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import DataTable, Header, Static, Footer, Input, Button - -from pyegeria import PyegeriaException, EgeriaTech, exec_report_spec - - -class ShowCommentsScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "quit", "Quit"), - ("ctrl+a", "add_comment", "Add Comment") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, table_name, table_row, view_server, platfgorm_url, user_name, user_password, *args, **kwargs): - super().__init__(*args, **kwargs) - self.table = table_name - self.row = table_row - self.view_server = view_server - self.platform_url = platfgorm_url - self.user_name = user_name - self.user_password = user_password - self.selected_row = "" - self.comment_text = "" - self.comment_type = "" - self.backend_id = "" - self.show_comments_datatable: DataTable = DataTable(id="show_comments_dt") - - def on_mount(self): - """On mount, find the GUID for the Row """ - self.title = "Egeria - My Profile" - self.sub_title = "Show Comments" - # Extract the correct string identifier - backend_id = self.extract_backend_identifier(self.table, self.row) - self.log(f"Backend identifier: {backend_id} extracted") - if backend_id: - self.notify(f"Accessing backend system with identifier: {backend_id}") - self.backend_id = backend_id - try: - comments_list = exec_report_spec(format_set_name="Comment-by-Element", - output_format="DICT", - params={"element_guid" : backend_id,}) - self.log(f"comments_list: {comments_list}") - - if isinstance(comments_list, dict): - comment_count = 0 - structured_comments: list[list] = [] - comment_text: list[Any] = [] - for key, value in comments_list.items(): - structured_comments[comment_count] = (key, - value["Display Name"], - value["Qualified Name"], - value["Comment Guid"], - value["Description"]) - comment_count += 1 - for row in structured_comments: - comment_text.append[row] - self.query_one("#show_comments_container", ScrollableContainer).mount(Static(row)) - elif isinstance(comments_list, str): - self.log(f"processing str comment: {comments_list}") - comment_text = str(comments_list) - self.query_one("#show_comments_container", ScrollableContainer).mount(Static(comment_text)) - else: - self.log(f"processing list of: {len(comments_list)} comments") - for comment in comments_list: - comment_text = str(comment) - self.query_one("#show_comments_container", ScrollableContainer).mount(Static(comment_text)) - except PyegeriaException as e: - self.notify(f"No comments found: {str(e)}", severity="warning") - else: - self.notify("Selected table does not contain valid GUID or Qualified Name columns.", severity="error") - - def extract_backend_identifier(self, table_name, row_key): - try: - # Query the table from the current active screen (MainScreen) - # We look for the screen with id 'main_screen' if it's not the current one - try: - target_screen = self.app.get_screen("main_screen") - except KeyError: - target_screen = self.app.get_screen("main") - - table_addr = target_screen.query_one(f"#{table_name}", DataTable) - except (NoMatches, KeyError): - try: - # Fallback to current screen - table_addr = self.app.screen.query_one(f"#{table_name}", DataTable) - except NoMatches: - self.log(f"Table {table_name} not found on any relevant screen") - return None - - # Map the clean string representation of column labels to their index positions - column_mapping = {col.label.plain.strip(): idx for idx, col in enumerate(table_addr.columns.values())} - self.log(f"Column mapping for table {table_name}: {column_mapping}") - - # Case-insensitive check for target columns - upper_mapping = {k.upper(): v for k, v in column_mapping.items()} - idx = None - if "GUID" in upper_mapping: - idx = upper_mapping["GUID"] - elif "QUALIFIED NAME" in upper_mapping: - # use pyegeria to get a GUID for that Qualified Name entity and use it as the index (upper) - comment_attributes = exec_report_spec(format_set_name="Search-Keywords", - output_format="DICT", - params=({"search_string":upper_mapping["QUALIFIED NAME"]})) - backend_id = comment_attributes["GUID"] - idx = backend_id - self.log(f"Target column idx value: {idx}") - if idx is None: - self.log(f"Target columns GUID or Qualified Name not found in {list(column_mapping.keys())}") - return None - - self.selected_row = idx - self.log(f"Selected row index: {self.selected_row}") - - try: - # handle if row_key is RowKey object or string - actual_row_key = row_key - row_data = table_addr.get_row(actual_row_key) - self.log(f"Row data: {row_data}") - return str(row_data[idx]) - except Exception as e: - self.log(f"Error retrieving row {row_key} from table {table_name}: {e}") - return None - - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield ScrollableContainer( - Static(f"Show Comments for: {self.table}", id="show_comments_static"), - id="show_comments_container" - ) - yield Footer() - - def action_add_comment(self): - """ Add an attached Comment to thew selected row """ - if self.backend_id: - self.log(f"Selected row: {self.backend_id}") - container = (self.query_one("#show_comments_container", ScrollableContainer)) - container.remove_children() - container.mount(Static(f"[b]Add Comment[/b]")) - container.mount(Input(placeholder="Enter comment", id="add_comment_input")) - container.mount(Static(f"Valid comment types are: Question, Answer, Suggestion and Requirement")) - container.mount(Input(placeholder="Comment Type: Question, Suggestion, etc.", id="add_comment_type_input")) - container.mount(Button("Add", id="add_comment_button")) - else: - self.notify("No row selected, a selected row is required to add a comment!") - - def on_input_changed(self, event: Input.Changed): - self.log(f"Input detected: {event.input.id}, {event.input.value}") - if event.input.id == "add_comment_input": - self.log(f"Input changed: {event.input.value}") - self.comment_text = str(event.input.value) - elif event.input.id == "add_comment_type_input": - self.log(f"Input changed: {event.input.value}") - self.comment_type = str(event.input.value) - else: - self.notify("Input not recognized! Please try again") - return - self.log(f"Comment text: {self.comment_text}, Comment type: {self.comment_type}") - - def on_button_pressed(self, event: Button.Pressed): - if event.button.id == "add_comment_button": - self.log(f"Button pressed: {event.button.id}") - self.log(f"Comment text: {self.comment_text}, Comment type: {self.comment_type}") - if self.comment_text and self.comment_type: - self.add_comment() - else: - self.notify("Both comment text and comment type are required!") - return - else: - self.notify("Button not recognized! Please try again") - - def add_comment(self): - if not self.comment_text or not self.comment_type or not self.backend_id: - self.notify("A row must be selected and Both comment text and comment type are required!") - return - - try: - egeria_tech = EgeriaTech(self.view_server, - self.platform_url, - self.user_name, - self.user_password) - egeria_tech.create_egeria_bearer_token(self.user_name, self.user_password) - except PyegeriaException as e: - self.notify(f"Error creating Egeria Tech object: {e}", timeout=5, severity="error") - self.notify("Unable to connect to the Egeria Server, please check the server is running and your credentials and try again", - timeout=10, - severity="error") - return - - try: - # Add comment to selected element - self.comment_type = self.comment_type.upper() - self.log(f"Adding comment to element: {self.backend_id}, values: {self.comment_text}, {self.comment_type}") - response = egeria_tech.add_comment_to_element( - element_guid=self.backend_id, - comment=self.comment_text, - comment_type=self.comment_type - ) - self.log(f"Comment successfully added! New Comment GUID: {response}") - self.notify(f"Comment successfully added! New Comment GUID: {response}") - container = (self.query_one("#show_comments_container", ScrollableContainer)) - container.remove_children() - rc = 200 - except Exception as e: - self.log(f"Failed to add comment: {e}") - self.notify(f"Failed to add comment: {e} \n Please try again.") - rc = 400 - self.dismiss() - - def action_quit(self): - self.app.pop_screen() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/StatusScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/StatusScreen.py deleted file mode 100644 index 90ca4695..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/StatusScreen.py +++ /dev/null @@ -1,68 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -import re -from typing import Any - -from pyegeria import copy_to_clipboard -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import Header, TextArea, Footer, Static - - -class StatusScreen(ModalScreen): - """Modal screen to display the status of the application.""" - BINDINGS = [("q", "quit", "Quit"), - ("return", "successful", "Continue"), - ("b", "unsuccessful", "Bad result"), - ("c", "copy_guid_to_clipboard", "Copy GUID to clipboard"), - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, status_message: str) -> None: - """Initialize the StatusScreen screen.""" - super().__init__() - self.status_message = status_message - - def compose(self) -> ComposeResult: - """ Compose the UI components for the StatusScreen screen.""" - yield Header(show_clock=True) - yield Static("Status", classes="span-3", id="status_title") - yield ScrollableContainer( - Static("Status"), - TextArea(self.status_message, id="status_message_text_area", read_only=True), - Static("End of Status"), - id="status_container") - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss(200) - - def action_unsuccessful(self) -> None: - """ The bad result option in the footer has been selected. Dismiss the screen.""" - self.dismiss(400) - - def action_copy_guid_to_clipboard(self) -> None: - """ The copy GUID to clipboard option in the footer has been selected. Copy the GUID to the clipboard.""" - # The GUID is between single ' marks in the status_message when a good result is recorded. - match = re.search(r"'(.*?)'", self.status_message) - self.log(f"Match: {match}") - if match: - guid = match.group(1) - copy_to_clipboard(guid) - self.log(f"Copied GUID to clipboard: {guid}") - else: - self.dismiss(400) - self.dismiss(200) - - def action_successful(self): - """ The successful option in the footer has been selected. Dismiss the screen.""" - self.dismiss(200) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/TechnologyTypeScreens.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/TechnologyTypeScreens.py deleted file mode 100644 index 64602eea..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/TechnologyTypeScreens.py +++ /dev/null @@ -1,661 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from typing import Any - -from textual.widgets._tree import TreeNode - -from pyegeria import load_app_config, settings -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Container -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Static, OptionList, Button, Footer, Input, Tree, TextArea -from textual.widgets._option_list import Option - - -class TechnologyTypeOptionsScreen(ModalScreen): - """ Modal screen to display a technology type's templates and processes.""" - BINDINGS = [("q", "dismiss", "Quit"), - ("b", "back", "Go_Back"), - # ("ctl+s", "Select", "tech_type_option_select") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, tech_type_guid: str, - tech_type_name: str, - tech_type_description: str, - user_name: str, - user_pwd:str, - user_kpts: int, - tech_type_templates: list[dict], - tech_type_processes: list[dict]) -> None: - """Initialize the TechnologyTypeOptions screen with a technology type's templates and processes.""" - super().__init__() - self.selected_process_index = None - self.tech_type_guid = tech_type_guid - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.user_name = user_name - self.user_password = user_pwd - self.karma_points = user_kpts - self.tech_type_templates = tech_type_templates - self.tech_type_processes = tech_type_processes - self.selected_template_guid = None - self.selected_process_guid = None - self.option_type_selected = None - self.selected_template = None - self.selected_template_index = None - self.selected_template_data = None - self.selected_process = None - self.selected_process_data = None - - load_app_config("config/config.json") - app_config = settings.Environment - app_user = settings.User_Profile - - if not self.user_name: - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - - def compose(self) -> ComposeResult: - - """ Compose the UI components for the TechnologyTypeOptions screen.""" - yield Header(show_clock=True) - yield Static(f"Description: {self.tech_type_description}") - yield ScrollableContainer( - Static(f"Technology Type: {self.tech_type_name}"), - Container( - Static("Select a template or process to continue."), - Static(f"Templates", id="number_of_templates_label"), - ScrollableContainer( - Static("Available Templates", id="template_options_label"), - OptionList(id="template_options"), - Button("Select Template", id="select_template_btn"), - id="template_options_container"), - Static(f"Processes:", id="number_of_processes_label"), - ScrollableContainer( - Static("Available Processes",id="process_options_label"), - OptionList(id="process_options"), - id="process_options_container"), - Button("Select Process", - id="select_process_btn"))) - yield Footer() - - async def on_mount(self) -> int: - """Mount the TechnologyTypeOptions screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - self.log(f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}") - - for widget in self.query(): - self.log(f"Widgets: {widget}") - - self.log(f"Templates: {self.tech_type_templates}, Processes: {self.tech_type_processes}") - self.log(f"Templates Type: {type(self.tech_type_templates)}, Processes Type: {type(self.tech_type_processes)}") - - if self.tech_type_templates and self.tech_type_templates != "None": - for t in self.tech_type_templates: - try: - self.log(f"Template: {t}") - templates = self.query_one("#template_options", OptionList).add_option( - Option(t.get("displayName"))) - self.log(f"Added option: {t.get('Catalog Template Name')}") - await templates.mount(after=self.query_one("#template_options_label")) - self.log(f"Mounted option") - self.query_one("#select_template_btn", Button).disabled = False - self.log(f"Enabled the template button") - except NoMatches as e: - try: - templates: OptionList = OptionList(id="template_options") - await templates.mount(after=self.query_one("#template_options_label")) - templates.add_option(Option(t.get("displayName"))) - templates.refresh() - self.query_one("#select_template_btn", Button).disabled = False - continue - except Exception as e: - self.log(f"Error creating template option list: {e}, (410") - return (410) - except Exception as e: - self.log(f"Error creating template option list: {e} (411)") - return (411) - else: - continue - else: - try: - self.log(f" No Templates") - templates = self.query_one("#template_options", OptionList).add_option( - Option("No Templates Found for this Tech Type")) - await templates.mount(after=self.query_one("#template_options_label")) - self.query_one("#select_template_btn", Button).disabled = True - except NoMatches as e: - try: - templates: OptionList = OptionList(id="template_options") - await templates.mount(after=self.query_one("#template_options_label")) - templates.add_option(Option("No Templates found for this Tech Type")) - self.query_one("#select_template_btn", Button).disabled = True - except Exception as e: - self.log(f"Error creating template option list: {e}") - return (409) - except Exception as e: - self.log(f"Error creating template option list: {e}") - return (408) - - if self.tech_type_processes and self.tech_type_processes != "None": - for p in self.tech_type_processes: - try: - self.log(f"Process: {p}") - processes = self.query_one("#process_options", OptionList).add_option( - Option(p.get("displayName"))) - self.log(f"Process option added: {p.get('displayName')}") - await processes.mount(after=self.query_one("#process_options_label")) - self.query_one("#select_process_btn", Button).disabled = False - self.log(f"Enabled the process button") - except NoMatches as e: - try: - processes: OptionList = OptionList(id="process_options") - await processes.mount(after=self.query_one("#process_options_label")) - processes.add_option(Option(p.get("displayName"))) - processes.refresh() - self.query_one("#select_process_btn", Button).disabled = False - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (407) - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (406) - else: - continue - else: - try: - self.log(f" No Processes") - processes = self.query_one("#process_options", OptionList).add_option( - Option("No processes found for this Tech Type")) - await processes.mount(after=self.query_one("#process_options_label")) - self.query_one("#select_process_btn", Button).disabled = True - except NoMatches as e: - try: - processes: OptionList = OptionList(id="process_options") - await processes.mount(after=self.query_one("#process_options_label")) - processes.add_option(Option(" No Processes found for this Tech Type")) - self.query_one("#select_process_btn", Button).disabled = True - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (405) - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (404) - - return(200) - - @on(OptionList.OptionHighlighted, "#template_options") - def handle_template_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: - """Handle the highlighting of an option in the template option list.""" - self.log(f"Template option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.selected_template_index = event.option_index - selected_option = event.option - self.selected_template = selected_option.prompt - self.log(f"Highlighted Template: {self.selected_template}, index: {self.selected_template_index}") - - @on(OptionList.OptionSelected, "#template_options") - def handle_template_option_selected(self, event: OptionList.OptionSelected) -> None: - """Handle the selection of an option from the template or process option lists.""" - self.log(f"Template selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.option_type_selected = event.option_list.name - self.selected_template_index = event.option_index - selected_option = event.option - self.selected_template = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_template}, index: {self.selected_template_index}") - - @on(OptionList.OptionHighlighted, "#process_options") - def handle_process_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: - """Handle the highlighting of an option in the process option list.""" - self.log(f"Process option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.selected_process_index = event.option_index - selected_option = event.option - self.selected_process = selected_option.prompt - self.log(f"Highlighted Process: {self.selected_process}, index: {self.selected_process_index}") - - @on(OptionList.OptionSelected, "#process_options") - def handle_process_option_selected(self, event: OptionList.OptionSelected) -> None: - """Handle the selection of an option from the template or process option lists.""" - self.log(f"Process selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.option_type_selected = event.option_list.name - self.selected_process_index = event.option_index - selected_option = event.option - self.selected_process = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_process}, index: {self.selected_process_index}") - - def action_go_back(self) -> None: - """ Handle the back key press.""" - self.dismiss("back") - - def action_Quit(self) -> None: - """ Handle the quit key press""" - self.dismiss(200) - - @on(Button.Pressed, "#select_template_btn") - def handle_template_selected(self, event: Button.Pressed) -> None: - """Handle the selection of a template option.""" - if self.selected_template is None: - return - self.log(f"Template option selected: {self.selected_template}") - self.dismiss(["template", self.selected_template]) - - @on(Button.Pressed, "#select_process_btn") - def handle_process_selected(self, event: Button.Pressed) -> None: - """Handle the selection of a process option.""" - if self.selected_process is None: - return - self.log(f"Process option selected: {self.selected_process}") - self.dismiss(["process", self.selected_process]) - -class TechnologyTypeProcessesScreen(ModalScreen[Any]): - """Modal screen to display technology type processes in Egeria.""" - BINDINGS = [("q", "quit", "Quit"), - ("b", "back", "Go back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "my_profile.tcss" - - def __init__ (self, - user_name: str, - user_kpts: int, - tech_type_name: str, - tech_type_description: str, - selected_t_option, - tech_type_option_selected, - tech_type_processes - ) -> None: - """Initialize the TechnologyTypeProcesses screen.""" - super().__init__() - self.user_name = user_name - self.karma_points = user_kpts - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.selected_t_option = selected_t_option - self.selected_t_option_selected = tech_type_option_selected - self.tech_type_processes = tech_type_processes - self.full_process = None - self.selected_process = None - load_app_config() - - async def on_mount(self) -> None: - """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - - if self.selected_t_option == "process": - self.log(f"Processing processes, with data: {self.selected_t_option_selected}") - # get selected process from the tech_type data - self.log(f"Technology Type Process: {self.tech_type_processes}") - if isinstance(self.tech_type_processes, list): - for process in self.tech_type_processes: - self.log(f"Process: {process.get("displayName")}, Selected: {self.selected_t_option_selected}") - if self.selected_t_option_selected == process.get("displayName"): - self.full_process = process - self.selected_t_process = process - self.log(f"Selected Process: {self.selected_t_process}") - break - else: - continue - self.log(f"Selected Process: {self.selected_t_process}, type: {type(self.selected_t_process)}") - - if self.selected_t_process is None: - self.log("No process selected, skipping placeholder display") - return - else: - specification = self.selected_t_process.get("specification") - if specification is None: - self.log("Selected process has no specification, skipping placeholder display") - return - else: - placeholderProperties = specification.get("supportedRequestParameter") - if placeholderProperties is None: - self.log("Selected process has no req uest parameters, skipping display") - return - else: - for parameter in placeholderProperties: - if parameter.get("class") != "SupportedRequestParameter": - continue - else: - name = parameter.get("name") or None - Description = parameter.get("description") or None - Type = parameter.get("dataType") - Example = parameter.get("example") - Required = parameter.get("required") - # governance_action_steps = self.full_process.get("Governance Action Steps") or None - - # Sanitize the name for use as a CSS ID - safe_name = name.replace(" ", "_") if name else f"placeholder_{id(self.selected_t_process)}" - safe_name = safe_name.replace("::", "_") - safe_name = safe_name.replace(":", "_") - self.log(f"Safe name = {safe_name}") - placeholder_text: TextArea = TextArea( - f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", - id=f"{safe_name}_placeholder_text_area", - read_only=True - ) - # Ensure TextArea is visible - placeholder_text.styles.height = 8 - - placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") - - # Mount the TextArea and the associated Input field into the ScrollableContainer - try: - load_point = self.query_one("#technology_type_processes_input") - await load_point.mount(placeholder_text, before="#process_submit_button") - await load_point.mount(placeholder_input, before="#process_submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") - continue - except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") - self.app.dismiss(416) - - - def compose(self) -> ComposeResult: - """ Compose the UI components for the Technology_Type_Processes screen.""" - yield Header(show_clock=True) - yield Static("Please complete the required fields and any optional fields you prefer:") - yield ScrollableContainer( - Static("Technology Type Process Input"), - Button("Submit", variant="primary", id="process_submit_button"), - id="technology_type_processes_input" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200", ) - - @on(Button.Pressed, "#process_submit_button") - def handle_submit_button_pressed(self, event: Button.Pressed) -> None: - """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") - save_input_data:dict = {} - for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") - save_input_data.update({input_widget.id: input_widget.value}) - self.log(f"Save input data: {save_input_data}") - self.dismiss(["input", save_input_data, self.full_process]) - - @on(Input.Changed, "#technology_type_processes_input") - def handle_input_changed(self, event: Input.Changed) -> None: - """The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") - - -class TechnologyTypesScreen(ModalScreen): - """Modal screen to display technology types in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("g", "go_back", "Back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, ttlist, user_name, user_pwd, user_kpts): - """Initialize the TechnologyTypes screen with a list of technology types.""" - super().__init__() - self.tech_type_list = ttlist - self.user_name = user_name - self.user_password = user_pwd - self.karma_points = user_kpts - self.tech_type_tree: Tree[str] = Tree(label="Technology Types", id="technology_types_tree") - self.tech_type_tree.root.expand() - self.tech_type_tree.auto_expand = True - self.selected_t_node = None - self.selected_t_node_label = None - self.node_id = None - self.node_status = "expanded" - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}" - self.sub_title = "Select a technology type" - - def compose(self) -> ComposeResult: - """ Compose and display the technology type screen""" - self.tech_type_tree.refresh() - if self.tech_type_list: - self.log(f"Technology types: {self.tech_type_list}, type: {type(self.tech_type_list)}") - self.render_tech_type_hierarchy_to_tree(self.tech_type_list, self.tech_type_tree) - else: - self.tech_type_tree.root.add("No technology types found", expand=True) - self.tech_type_tree.refresh() - self.log(f"Technology types tree: {self.tech_type_tree}") - - yield Header(show_clock=True) - yield ScrollableContainer( - Static("Display technology types in Egeria"), - self.tech_type_tree, - # Button("Select", id="select_tech_type_btn"), - id="technology_types_table" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200") - - def action_go_back(self) -> None: - """ The back option in the footer has been selected. Dismiss the screen.""" - self.dismiss("201") - - @on(Tree.NodeSelected) - def handle_tree_node_selected(self, event: Tree.NodeSelected) -> None: - """ The user has selected a node in the tree. """ - self.log(f"Tree node selected, node: {event.node}") - self.selected_t_node = event.node - self.selected_t_node_label = event.node.label - self.log(f"Selected node: {self.selected_t_node}, label: {self.selected_t_node.label}") - self.dismiss(str(self.selected_t_node.label)) - - @on(Tree.NodeCollapsed) - def handle_tree_node_collapsed(self, event: Tree.NodeCollapsed) -> None: - # handle the twisty to close a node in the tree - # logger.debug(f"TreeNodeCollapsed: {event.node.id}") - self.node_id = str(event.node.id) - self.node_status = "collapsed" - - @on(Tree.NodeExpanded) - def handle_tree_node_expanded(self, event: Tree.NodeExpanded) -> None: - # handle the twisty to open a node in the tree - # logger.debug(f"TreeNodeExpanded: {event.node.id}") - self.node_id = str(event.node.id) - self.node_status = "expanded" - - def action_expand(self): - # handle the hot key to expand/collapse all nodes in the tree (see bindings) - self.tech_type_tree = self.query_one("#technology_types_tree", Tree) - if self.node_status == "collapsed": - self.tech_type_tree.root.expand_all() - else: - self.tech_type_tree.root.collapse_all() - return - - def render_tech_type_hierarchy_to_tree(self, - data: dict | list[dict], - tree_or_node: Tree[str] | TreeNode[str], - label_attr: str = "displayName", - guid_attr: str = "guid", - children_attr: str = "subTypes" - ) -> None: - """Recursively render a DICT) hierarchy into a Textual Tree. - - Args: - data: The data structure from pyegeria.get_tech_type_hierarchy. - tree_or_node: The Tree object or a TreeNode to add children to. - label_attr: The attribute in the data for the node label. - guid_attr: The attribute in the data for the node data (GUID). - children_attr: The attribute in the data containing the list of children. - """ - if isinstance(data, list): - for item in data: - self.render_tech_type_hierarchy_to_tree(item, tree_or_node, label_attr, guid_attr, children_attr) - return - - if not isinstance(data, dict): - return - - label = str(data.get(label_attr) or data.get("Display Name") or "Unknown") - guid = str(data.get(guid_attr) or data.get("GUID") or "") - - # If it's a Tree, we add to root. If it's a TreeNode, we add to it. - if isinstance(tree_or_node, Tree): - node = tree_or_node.root.add(label, data=guid, expand=True) - else: - node = tree_or_node.add(label, data=guid, expand=True) - - children = data.get(children_attr) - if children and isinstance(children, list): - for child in children: - self.render_tech_type_hierarchy_to_tree(child, node, label_attr, guid_attr, children_attr) - - -class TechnologyTypeTemplatesScreen(ModalScreen[Any]): - """Modal screen to display technology type templates in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "back", "Go back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "my_profile.tcss" - - def __init__ (self, - user_name: str, - user_kpts: int, - tech_type_name: str, - tech_type_description: str, - selected_t_option, - tech_type_option_selected, - tech_type_templates - ) -> None: - """Initialize the Technology_Type_Templatess screen.""" - super().__init__() - self.user_name = user_name - self.karma_points = user_kpts - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.selected_t_option = selected_t_option - self.selected_t_option_selected = tech_type_option_selected - self.tech_type_templates = tech_type_templates - self.full_template = None - self.selected_t_template = None - load_app_config() - - async def on_mount(self) -> None: - """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - - if self.selected_t_option == "template": - self.log(f"Processing templates, with data: {self.selected_t_option_selected}") - # get selected template from the tech_type data - self.log(f"Technology Type Templates: {self.tech_type_templates}") - if isinstance(self.tech_type_templates, list): - for template in self.tech_type_templates: - self.log(f"Template: {template.get('displayName')}, Selected: {self.selected_t_option_selected}") - if self.selected_t_option_selected == template.get("displayName"): - self.full_template = template - self.selected_t_template = template - self.log(f"Selected Template: {self.selected_t_template}") - break - else: - continue - self.log(f"Selected Template: {self.selected_t_template}") - - if self.selected_t_template is None: - self.log("No template selected, skipping placeholder display") - return - else: - specification = self.selected_t_template.get("specification") - if specification is None: - self.log("Selected template has no specification, skipping placeholder display") - return - else: - placeholderProperties = specification.get("placeholderProperty") - if placeholderProperties is None: - self.log("Selected template has no placeholder properties, skipping display") - return - else: - for placeholder in placeholderProperties: - if placeholder.get("class") != "PlaceholderProperty": - continue - else: - name = placeholder.get("name") or None - Description = placeholder.get("description") or None - Type = placeholder.get("dataType") or None - Example = placeholder.get("example") or None - Required = placeholder.get("required") or False - - # Sanitize the name for use as a CSS ID - safe_name = name.replace(" ", "_") if name else f"placeholder_{id(placeholder)}" - placeholder_text: TextArea = TextArea( - f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", - id=f"{safe_name}_placeholder_text_area", - read_only=True - ) - # Ensure TextArea is visible - placeholder_text.styles.height = 8 - - placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") - - # Mount the TextArea and the associated Input field into the ScrollableContainer - try: - load_point = self.query_one("#technology_type_templates_input") - await load_point.mount(placeholder_text, before="#template_submit_button") - await load_point.mount(placeholder_input, before="#template_submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") - continue - except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") - self.app.dismiss(416) - - def compose(self) -> ComposeResult: - """ Compose the UI components for the Technology_Type_Templatess screen.""" - yield Header(show_clock=True) - yield Static("Please complete the required fields and any optional fields you prefer:") - yield ScrollableContainer( - Static("Technology Type Template Input"), - Button("Submit", id="template_submit_button"), - id="technology_type_templates_input" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200", ) - - @on(Button.Pressed, "#template_submit_button") - def handle_submit_button_pressed(self, event: Button.Pressed) -> None: - """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") - save_input_data: dict = {} - for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") - save_input_data.update({input_widget.id: input_widget.value}) - self.log(f"Save input data: {save_input_data}") - self.dismiss(["input", save_input_data, self.full_template]) - - @on(Input.Changed, "#technology_type_templates_input") - def handle_input_changed(self, event: Input.Changed) -> None: - """ The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/UserIdentitiesScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/UserIdentitiesScreen.py deleted file mode 100644 index 249bdfc7..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/UserIdentitiesScreen.py +++ /dev/null @@ -1,102 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, Placeholder, DataTable, Static - - -class UserIdentitiesScreen(ModalScreen): - """ Modal Screen to display the user identities as stored in Egeria """ - - BINDINGS = [ - ("escape", "app.pop_screen", "Exit Screen"), - ("q", "app.pop_screen", "Exit Screen"), - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, user_name, user_password, karma_points, user_identities): - super().__init__() - self.user_name = user_name - self.user_password = user_password - self.karma_points = karma_points - self.user_identities = user_identities - self.log(f"UserIdentitiesScreen initialized for user: {self.user_name}, Karma Points: {self.karma_points}") - self.log(f"User identity input: {self.user_identities}") - - def on_mount(self) -> None: - self.title = "My_Egeria User Identities Screen" - self.sub_title = f"User Identities for {self.user_name}, Karma Points: {self.karma_points}" - try: - self.user_identity_datatable = self.query_one("#user_identity_datatable", DataTable) - self.user_identity_datatable.clear(columns=False) - except NoMatches: - self.user_identity_datatable : DataTable = DataTable(id="user_identity_datatable") - self.user_identity_datatable.cursor_type="row" - self.user_identity_datatable.zebra_stripes=True - self.user_identity_datatable.show_header=True - self.user_identity_datatable.add_columns("Display Name", - "Category", - "Description", - "Type Name", - "URL", - "GUID", - "Qualified Name", - "Metadata Collection ID", - "Metadata Collection Name", - "User ID", - "Distinguished Name" - ) - if self.user_identities: - for identity in self.user_identities: - self.log(f"User identity: {identity}") - display_name = identity.get('Display Name', 'Unknown') - qualified_name = identity.get('Qualified Name', 'Unknown') - category = identity.get('Category', 'Unknown') - description = identity.get('Description', 'No description available') - type_name = identity.get('Type Name', 'Unknown') - URL = identity.get('URL', 'Unknown') - GUID = identity.get('GUID', 'Unknown') - metadata_collection_id = identity.get('Metadata Collection ID', 'Unknown') - metadata_collection_name = identity.get('Metadata Collection Name', 'Unknown') - useridentity_user_id = identity.get('User ID', 'Unknown') - distinguished_name = identity.get('Distinguished Name', 'Unknown') - self.user_identity_datatable.add_row( - display_name, - category, - description, - type_name, - URL, - GUID, - qualified_name, - metadata_collection_id, - metadata_collection_name, - useridentity_user_id, - distinguished_name - ) - continue - self.query_one("#user_identities_placeholder", Placeholder).remove() - container = self.query_one("#user_identities_container", ScrollableContainer) - container.mount(self.user_identity_datatable) - else: - self.query_one("#user_identities_placeholder", Placeholder).remove() - container = self.query_one("#user_identities_container", ScrollableContainer) - container.mount(Static("No User Identities Data found for this user")) - - def compose(self) -> ComposeResult: - yield Header() - yield ScrollableContainer( - Placeholder("Placeholder", id="user_identities_placeholder"), - id = "user_identities_container" - ) - yield Footer() - - def action_exit_screen(self) -> None: - self.app.pop_screen() diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/__init__.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/__init__.py deleted file mode 100644 index 5b0687bd..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/catalog_api_calls.md b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/catalog_api_calls.md deleted file mode 100644 index dbcca4d1..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/catalog_api_calls.md +++ /dev/null @@ -1,103 +0,0 @@ -# Catalog (Shop for Data) — pyegeria API Call Reference - -This document maps each UI function in the **Catalogs / Shop for Data** flow of `my_profile_app.py` -to the underlying pyegeria API calls it makes. - ---- - -## Left Navigation Pane — Catalog Chooser (`ShopForDataScreen`) - -Populated when the user selects **"Catalogs/Shop for Data"** from the main menu option list. -All five tables are loaded before the screen is pushed. - -| Table | pyegeria Call | Report Spec / Method | Key Parameters | Suggested Call | Comments | -|---|---|---|---|---|---| -| Glossaries | `exec_report_spec()` | `format_set_name="Glossaries"` | `search_string="*"` | | | -| Digital Product Catalog | `exec_report_spec()` | `format_set_name="Digital-Product-Catalog"` | `search_string="*"`, `metadata_element_subtypes=["DigitalProduct","DigitalProductFamily"]` | | | -| Data Dictionaries | `exec_report_spec()` | `format_set_name="Data-Dictionaries"` | `search_string="*"` | | | -| Business Domains | `exec_report_spec()` | `format_set_name="BusinessCapabilities"` | `search_string="*"` | | | -| Root Collections | `exec_report_spec()` | `format_set_name="BasicCollections"` | `search_string="RootCollection"` | | | - -All five calls use `output_format="DICT"` and share the same `view_server`, `view_url`, `user`, and `user_pass` from app config. - ---- - -## Right-Hand Pane — Detail View (`SelectionOverviewScreen`) - -Populated via the `shop_for_data_callback()` after the user selects a row in the left nav. -The `selection_type` string returned by `ShopForDataScreen` determines which builder runs. - -### Glossary selected → `build_glossary_details()` - -| Action | pyegeria Call | Notes | Suggested Call | Comments | -|---|---|---|---|---| -| Build folder/term tree | _(none — reuses pre-fetched data)_ | Parses `self.glossary_data_extract` loaded during left-nav population; no additional API call | | | - -### Digital Product Catalog selected → `build_catalog_details()` - -| Action | pyegeria Call | Report Spec / Method | Key Parameters | Suggested Call | Comments | -|---|---|---|---|---|---| -| Load catalog items | `exec_report_spec()` | `format_set_name="Digital-Product-Catalog"` | `search_string=`, `filter_string=` | | | -| Retrieve sample data for DigitalProduct members | `DataEngineer.find_tabular_data_sets()` | _(direct method)_ | `search_string=`, `start_from=0`, `page_size=10`, `output_format="MD"` | | | - -### Data Dictionary selected → `build_dictionary_details()` - -| Action | pyegeria Call | Report Spec / Method | Key Parameters | Suggested Call | Comments | -|---|---|---|---|---|---| -| Load dictionary terms | `exec_report_spec()` | `format_set_name="Data-Dictionaries"` | `search_string=`, `filter_string=` | | | - -### Business Domain selected → `build_domain_details()` - -| Action | pyegeria Call | Report Spec / Method | Key Parameters | Suggested Call | Comments | -|---|---|---|---|---|---| -| Load domain structure | `exec_report_spec()` | `format_set_name="BusinessCapabilities"` | `search_string=`, `filter_string=` | | | - -### Root Collection selected → `build_root_collection_details()` - -| Action | pyegeria Call | Notes | Suggested Call | Comments | -|---|---|---|---|---| -| Build member tree | _(none — reuses pre-fetched data)_ | Parses `self.collections` loaded during left-nav population; no additional API call | | | - ---- - -## Subscribe Action (`overview_callback` → r_code 211) - -Triggered when the user clicks **Subscribe** in `SelectionOverviewScreen`. - -| Action | pyegeria Call | Key Parameters | Suggested Call | Comments | -|---|---|---|---|---| -| Create digital subscription | `ProductManager.create_digital_subscription()` | `selected_item` (qualified name of selected element) | | | - ---- - -## Supporting Flows (Main Screen / App Startup) - -These run on app mount, before the catalog screen is reached. - -| Function | pyegeria Call | Report Spec / Method | Key Parameters | Suggested Call | Comments | -|---|---|---|---|---|---| -| Load user profile | `MyProfile._async_get_my_profile()` | `report_spec="My-User-MD"` | `output_format="DICT"` | | | -| Load user identities | `MyProfile.get_my_profile()` | `report_spec="User-Identities"` | `output_format="DICT"` | | | -| Load team members (on role row select) | `exec_report_spec()` | `format_set_name="Team-Members"` | `search_string=` | | | - ---- - -## Technology Types Flow (separate from catalog) - -| Function | pyegeria Call | Key Parameters | Suggested Call | Comments | -|---|---|---|---|---| -| Populate tech-type tree (left nav) | `AutomatedCuration._async_get_tech_type_hierarchy()` | `filter_string="*"` | | | -| Load tech-type detail (right pane) | `AutomatedCuration.get_tech_type_detail()` | `filter_string=`, `output_format="JSON"` | | | -| Create element from template | `AutomatedCuration.initiate_gov_action_process()` | `body=` (includes `templateGUID`, `placeholderPropertyValues`) | | | - ---- - -## Client Classes Used - -| Client Class | Used For | -|---|---| -| `MyProfile` | User profile and identity retrieval | -| `AutomatedCuration` | Technology type hierarchy, detail, and template/process instantiation | -| `DataEngineer` | Tabular data set retrieval for catalog sample data | -| `ProductManager` | Creating digital subscriptions | -| `exec_report_spec()` | All report-spec-driven lookups (glossaries, catalogs, dictionaries, domains, collections, team members) | diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile.tcss b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile.tcss deleted file mode 100644 index e0a54bbc..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile.tcss +++ /dev/null @@ -1,169 +0,0 @@ -/* *********************************************************************************************** */ -/* PDX-License-Identifier: Apache-2.0 */ -/* Copyright Contributors to the ODPi Egeria project. */ -/* */ -/* This file provides a set of report specification related functions for my_egeria. */ -/* */ -/* Unless you are running Microsoft's Studio Product with the Textual Extension installed */ -/* the file will show multiple errors, for example I use the PyCharm IDE for which there are */ -/* no extensions for Textual, regardless of the errors the file will run correctly with Textual.*/ -/* *********************************************************************************************** */ - -Screen { - layout: grid; - grid-size: 3; - grid-gutter: 1; -} - -#main_screen { - layout: grid; - grid-size: 3; - grid-columns: 1fr 1fr 1fr; - /*grid-rows: 3 1fr 1fr 1fr 1;*/ - grid-rows: 1fr 1fr 1fr; - grid-gutter: 1; -} - -#edit_associations_screen { - layout: grid; - grid-size: 2; - grid-columns: 1fr 1fr; - grid-gutter: 1; - border: round white; -} - -#main_header { - column-span: 3; - height: 3; - width: 100%; -} - -#main_footer { - column-span: 3; - height: 1; - width: 100%; -} - -ModalScreen { - layout: grid; - grid-size: 3; - grid-rows: auto; - outline: chartreuse; - grid-gutter: 2; -} - -.EditProfileScreen { - grid-rows: auto auto 1fr auto; -} - -.EditTeamsScreen { - grid-rows: auto auto auto 1fr auto; -} - -.EditRolesScreen, .EditCommunitiesScreen, .EditIdentitiesScreen,.StatusScreen { - grid-rows: auto auto 1fr auto; -} - -ModalScreen > Header, ModalScreen > Footer { - column-span: 3; -} - -#edit_profile_title, #create_profile_title, #edit_teams_title, #edit_roles_title, #edit_communities_title, #edit_identities_title, #status_title, .span-3 { - column-span: 3; - text-align: center; - background: darkblue; - color: white; -} - -#main_projects_container, -#main_communities_container, -#main_roles_container, -#main_teams_container, -#main_activities_container, -#main_identities_container, -#other_function_container, -#other_function_list { - width: 100%; - height: 100%; -} - -#edit_roles_container, -#edit_communities_container, -#edit_identities_container, -#edit_teams_container, -#status_container, -#user_identities_container, -#main_identities_container { - column-span: 3; - width: 100%; - height: 100%; -} - -#technology_types_table, -#business_domain_table, -#communities_table, -#data_dictionary_table, -#data_specification_table, -#digital_product_catalog_table, -#glossary_table, -#projects_table, -#roles_table, -#teams_table, -#user_identity_table, -#user_identity_datatable, -#status_table, -#root_collection_table { - width: 100%; - height: 100%; -} - -Button { - background: greenyellow; - color: black; -} - -DataTable, TextArea, SelectionList, Tree, OptionList { - color: white; - background: black; -} - -DataTable { - text-style: bold; -} - -DataTable > .datatable--header { - background: darkblue; - color: white; - text-style: bold; -} - -DataTable > .datatable--cursor, OptionList > .option-list--cursor { - background: greenyellow; - color: black; -} - -#edit_profile_form Horizontal { - height: auto; - width: 100%; -} - -#blogs_table, -#journal_table, -#todos_table { - width: 100%; - height: 1fr; -} - -#edit_profile_form Horizontal Static { - width: 22; -} - -#edit_profile_form Horizontal Input { - width: 1fr; -} - -Input { - width: 1fr; - height: auto; - border: solid white; -} diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile_app-orig.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile_app-orig.py deleted file mode 100644 index c151639e..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile_app-orig.py +++ /dev/null @@ -1,1924 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -import datetime -import re -import json -import sys -from pathlib import Path - -# Add the project root to sys.path to allow running this script from any directory -root_path = Path(__file__).resolve().parents[4] -if str(root_path) not in sys.path: - sys.path.append(str(root_path)) - -from pyegeria import load_app_config, settings, MyProfile, PyegeriaException, print_basic_exception, exec_report_spec, \ - AutomatedCuration, ProductManager, \ - PyegeriaInvalidParameterException, PyegeriaAPIException, DataEngineer -from textual import on -from textual.app import App, ComposeResult -from textual.containers import ScrollableContainer -from textual.widgets import DataTable, OptionList, Header, Static, Footer, Tree - -from CreateProfileScreen import CreateProfileScreen -from my_egeria.DemoCode.Deprecated.EditProfileScreen import EditProfileScreen -from my_egeria.DemoCode.Deprecated.EditCommunitiesScreen import EditCommunitiesScreen -from my_egeria.DemoCode.Deprecated.EditIdentitiesScreen import EditIdentitiesScreen -from my_egeria.DemoCode.Deprecated.EditRolesScreen import EditRolesScreen -from my_egeria.DemoCode.Deprecated.EditTeamsScreen import EditTeamsScreen -from my_egeria.DemoCode.Deprecated.TechnologyTypesScreen import TechnologyTypesScreen -from my_egeria.DemoCode.Deprecated.TechnologyTypeOptionsScreen import TechnologyTypeOptionsScreen -from my_egeria.DemoCode.Deprecated.TechnologyTypeTemplatesScreen import TechnologyTypeTemplatesScreen -from my_egeria.DemoCode.Deprecated.TechnologyTypeProcessesScreen import TechnologyTypeProcessesScreen -from StatusScreen import StatusScreen -from ShopForDataScreen import ShopForDataScreen -from SelectionOverviewScreen import SelectionOverviewScreen -from MyTeam import MyTeam -from MainScreen import MainScreen -from SearchForTermScreen import SearchForTermScreen -from CreateSubscriptionRequestScreen import CreateSubscriptionRequestScreen -from UserIdentitiesScreen import UserIdentitiesScreen - - -class MyProfileApp(App): - """My Profile App. - - Retrieves a user's profile from Egeria and displays current work items. - If no profile is found, offers a UI to create one. - """ - - BINDINGS = [("q", "quit", "Quit"), - ("r", "refresh", "Refresh Data"), - ] - - CSS_PATH = "my_profile.tcss" - - SCREENS = { - "main": MainScreen, - "_default": MainScreen, - "create_profile": CreateProfileScreen, - "edit_profile": EditProfileScreen, - "edit_communities": EditCommunitiesScreen, - "edit_identities": EditIdentitiesScreen, - "edit_roles": EditRolesScreen, - "edit_teams": EditTeamsScreen, - "tech_types": TechnologyTypesScreen, - "tech_type_options": TechnologyTypeOptionsScreen, - "tech_type_templates": TechnologyTypeTemplatesScreen, - "tech_type_processes": TechnologyTypeProcessesScreen, - "status": StatusScreen, - "shop_4_data": ShopForDataScreen, - "search_for_term": SearchForTermScreen, - "overview": SelectionOverviewScreen, - "create_subscription": CreateSubscriptionRequestScreen, - "my_team": MyTeam - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.contribution_record = None - self.heading = "My_Profile" - self.subheading = "Egeria Profile for current user" - self.description = "Display the user related items for the current user." - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - print("Platform:", app_config.egeria_platform_url) - print("View Server:", app_config.egeria_view_server) - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - - # Ensure compose() is safe before data loads - self.actor_profile: dict = {} - self.projects = [] - self.communities = [] - self.roles = [] - self.actions = [] - self.teams = [] - self.other_function_list = [] - self.tech_type_json: str - self.tech_type_response = None - self.tech_type_list = [] - self.tech_type_guid = "" - self.tech_type_name = "" - self.tech_type_description = "" - self.selected_t_node = None - self.selected_t_node_label = None - self.karma_points = 0 - self.tech_type_templates = [{}] - self.tech_type_processes = [{}] - self.full_template = None - self.glossary_data_extract = None - self.business_glossary_data_extract = None - self.display_glossary_data_extract = None - self.digital_glossary_data_extract = None - self.team_members: list[list] = [] - self.max_mermaid_node_count = 0 # This is to tell egeria we dont want mermaid graphs in the response packet. - self.graph_query_depth = 0 # This tell egeria not to include relationships in the response packet - self.user_GUID = "" - self.user_data = {} - - def compose(self) -> ComposeResult: - yield Header() - yield Footer() - - async def on_mount(self) -> None: - """Load profile; if missing, prompt to create it; then populate tables.""" - # DOMInfo.attach_to(self) - - await self._load_or_create_profile() - await self._populate_tables() - - - async def _load_or_create_profile(self) -> None: - try: - #instantiate the class - self.my_profile_inst = MyProfile(self.view_server, self.platform_url, self.user_name, self.user_password) - self.my_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) - #retrieve the profile - self.my_profile_data = await self.my_profile_inst._async_get_my_profile( - report_spec="My-User-MD", - output_format="DICT", - ) - self.log(f"retrieve profile result: {self.my_profile_data}") - except (PyegeriaException) as e: - self.log(f"Error retrieving profile: {e!s}") - print_basic_exception(e) - self.exit(402) - return - - if self.my_profile_data == []: - self.log(f"Error retrieving profile. Prompting to create one...") - self.log(f"To create a profile you must have a valid userid in the system, please contact your system administrator to create one if needed") - await self.push_screen(CreateProfileScreen( - self.user_name, - self.user_password, - self.view_server, - self.platform_url, - ), callback = self.new_profile_return) - else: - self.new_profile_return(200) - - def new_profile_return(self, result: int) -> None: - """ This function handles either the return from the create new profile screen or - when the user already has a profile continue processing """ - self.log(f"Profile creation result: {result}") - if not result or result != 200: - self.log(f"Profile creation cancelled/failed; return: {result}, exiting.") - self.exit(403) - return - - self.result = result - - # Retry after creation if necessary - try: - self.user_profile_struct = self.my_profile_inst.get_my_profile( - output_format="DICT", - report_spec="My-User-MD", - ) - self.log(f"Profile retrieved successfully: {self.user_profile_struct}") - self.push_screen("main") - except PyegeriaException as e2: - self.log(f"Error retrieving user profile: {e2!s}") - self.exit(412) - return - if self.user_profile_struct is []: - self.log(f"Error retrieving user profile. Exiting.") - self.exit(413) - return - - # strip out the individual profile elements - self.user_profile = self.user_profile_struct[0] - self.contribution_record = self.user_profile.get("Contribution Record") or {} - for c in self.contribution_record: - self.karma_points = c.get("Karma Points") or 0 - break - self.my_projects_data = self.user_profile.get("Projects") or [] - self.my_teams_data = self.user_profile.get("Teams") or [] - self.my_communities_data = self.user_profile.get("Communities") or [] - self.my_roles_data = self.user_profile.get("Roles") or [] - self.my_actions_data = self.user_profile.get("Actions") or [] - self.log(f"Contribution Record: {self.contribution_record}") - self.log(f"Karma Points: {self.karma_points}") - self.log(f"my_projects_data: {self.my_projects_data}") - self.log(f"my_teams_data: {self.my_teams_data}") - self.log(f"my_communities_data: {self.my_communities_data}") - self.log(f"my_roles_data: {self.my_roles_data}") - self.log(f"my_actions_data: {self.my_actions_data}") - # User Identities - try: - self.user_identities = self.my_profile_inst.get_my_profile( - report_spec="User-Identities", - output_format="DICT", - ) - self.log(f"User-Identities: {self.user_identities}, type: {type(self.user_identities)}") - except PyegeriaException as e: - self.log(f"Error retrieving User-Identities: {e!s}") - self.user_identities = {} - - # User GUID - - try: - self.user_data = exec_report_spec(format_set_name="Actor-Profiles", - output_format="DICT", - params = {"search_string" : self.user_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving actor profile: {e!s}") - self.exit(420) - self.log(f"Actor Profile retrieved: {self.user_data}") - self.user_data = self.user_data.get("data") - self.log(f"Extracted data : {self.user_data}") - self.user_GUID = self.user_data[0].get("GUID") - self.log(f"User GUID retrieved: {self.user_GUID}") - - # Normalize expected keys - self.full_name = self.user_profile.get("Full Name") or "" - self.sub_title = f"{self.full_name} ({self.user_profile.get('User ID')}, Karma Points: {self.karma_points})" - self.projects = self.my_projects_data or [] - self.communities = self.my_communities_data or [] - self.roles = self.my_roles_data or [] - self.actions = self.user_profile.get("actions") or [] - self.teams = self.my_teams_data or [] - if isinstance(self.user_identities, list): - self.user_identity = self.user_identities - else: - self.user_identity = self.user_identities.get("User-Identities") or [] - - async def _populate_tables(self) -> None: - """Populates tables from normalized profile data""" - main_screen = self.get_screen("main") - self.projects_table = main_screen.query_one("#projects_table", DataTable) - self.communities_table = main_screen.query_one("#communities_table", DataTable) - self.roles_table = main_screen.query_one("#roles_table", DataTable) - self.actions_table = main_screen.query_one("#actions_table", DataTable) - self.user_identity_table = main_screen.query_one("#user_identity_table", DataTable) - self.teams_table = main_screen.query_one("#teams_table", DataTable) - - assert self.projects_table is not None - assert self.communities_table is not None - assert self.roles_table is not None - assert self.actions_table is not None - assert self.user_identity_table is not None - assert self.teams_table is not None - - self.projects_table.clear(columns=True) - self.projects_table.add_columns("Project Name", "Description", "Qualified Name") - - self.communities_table.clear(columns=True) - self.communities_table.add_columns("Assignment Type", "Community Name", "Description", "GUID") - - self.roles_table.clear(columns=True) - self.roles_table.add_columns("Role Name", "Role Type","Description", "GUID") - self.roles_table.zebra = True - self.roles_table.cursor_type = "row" - - self.teams_table.clear(columns=True) - self.teams_table.add_columns("Assignment Type", "Team Name", "Description","GUID") - - self.actions_table.clear(columns=True) - self.actions_table.add_columns("Action Name", "Status", "Description") - - self.user_identity_table.clear(columns=True) - self.user_identity_table.add_columns("Display Name", "User ID", "Distinguished Name") - - # Populate rows - for p in self.projects if isinstance(self.projects, list) else []: - self.projects_table.add_row( - str(p.get("Name", "")), - str(p.get("Description", "")), - str(p.get("Qualified Name", "")), - ) - - for c in self.communities if isinstance(self.communities, list) else []: - self.communities_table.add_row( - str(c.get("Assignment Type", "")), - str(c.get("Name", "")), - str(c.get("Description", "")), - str(c.get("GUID", "")) - ) - - for r in self.roles if isinstance(self.roles, list) else []: - self.roles_table.add_row( - str(r.get("Name", "")), - str(r.get("Type", "")), - str(r.get("Description", "")), - str(r.get("GUID", "")), - ) - - for a in self.actions if isinstance(self.actions, list) else []: - self.actions_table.add_row( - str(a.get("Display Name", "")), - str(a.get("Description", "")), - ) - - for t in self.teams if isinstance(self.teams, list) else []: - self.teams_table.add_row( - str(t.get("Assignment Type", "")), - str(t.get("Name", "")), - str(t.get("Description", "")), - str(t.get("GUID", "")), - ) - - def action_quit(self) -> None: - # quit selected by user, so exit app - self.exit(200) - - async def action_refresh(self) -> None: - # refresh requested by User - self.log("Refreshing data...") - await self._load_or_create_profile() - await self._populate_tables() - self.log("Data refresh completed.") - - @on(OptionList.OptionSelected, "#other_function_list") - async def handle_option_selected(self, event: OptionList.OptionSelected): - # option selected by user - selected_option = event.option.prompt - selected_option_id = event.option.id - self.log(f"Selected option: {selected_option} ({selected_option_id})") - if selected_option == "Technology Types": - self.log("Fetching technology types...") - await self.fetch_technology_types() - self.log(f"Tech Type Response: {self.tech_type_response} | {self.tech_type_list}") - if self.tech_type_response == []: - self.log("No technology types found.") - self.exit(200) - elif len(self.tech_type_response) == 3 and int(self.tech_type_response) >= 400: - self.log("Error fetching technology types.") - self.exit(int(self.tech_type_response)) - self.log("Technology types fetched successfully.") - self.log("Displaying technology types...") - await self.push_screen(TechnologyTypesScreen(self.tech_type_list, - self.user_name, - self.user_password, - self.karma_points), - callback=self.tech_type_callback) - self.log("Technology types displayed successfully.") - elif selected_option == "User Identities": - await self.push_screen(UserIdentitiesScreen(self.user_name, - self.user_password, - self.karma_points, - self.user_identities), - callback=self.user_identities_callback) - elif selected_option == "Catalogs/Shop for Data": - """ Push new Screen, Show Glossaries, Digital Product Catalogs, Data Dictionaries and - Business Domains, allow the user to select from one of the 4 categories and use that selection to - display a list of available collections of the chosen type and allow the user to subscribe to them - """ - - # start by gathering the data using Pyegeria to access the Egeria backend servers - - # Glossaries - glossary_table: DataTable = DataTable(id="glossary_table") - glossary_table.add_columns("Glossary Name", "Description", "Qualified Name") - glossary_table.cursor_type = "row" - glossary_table.zebra_stripes = True - try: - self.glossary_data = exec_report_spec(format_set_name="Glossaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving glossary details: {e!s}") - self.exit(420) - return(420) - self.log(f"Glossary data returned: {self.glossary_data}") - self.glossary_data_extract = self.glossary_data.get("data") or [] - self.log(f"Glossary data extracted: {self.glossary_data_extract}") - if self.glossary_data_extract == []: - self.log(f"No glossary data found for search string: {self.selected_t_node}") - glossary_table.add_row("No glossaries found", "No data returned from Egeria", "") - else: - for g in self.glossary_data_extract: - glossary_table.add_row(g.get("Display Name"), g.get("Description"), g.get("Qualified Name")) - continue - - # Digital Product Catalogs - digital_product_catalog_table: DataTable = DataTable(id="digital_product_catalog_table") - digital_product_catalog_table.add_columns("Digital Product Catalog Name", "Description", "Qualified Name") - digital_product_catalog_table.cursor_type = "row" - digital_product_catalog_table.zebra_stripes = True - try: - self.digital_product_catalog_data = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params = {"search_string" : "*", - "metadata_element_subtypes": ["DigitalProduct", "DigitalProductFamily"]}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving digital product catalog details: {e!s}") - self.exit(421) - return(421) - self.log(f"Digital Product Catalog data returned: {self.digital_product_catalog_data}") - self.digital_product_catalog_data_extract = self.digital_product_catalog_data.get("data") or [] - self.log(f"Digital Product Catalog data extracted: {self.digital_product_catalog_data_extract}") - if self.digital_product_catalog_data_extract == []: - self.log(f"No digital product catalog data found for user: {self.user_name}") - digital_product_catalog_table.add_row("No digital product catalogs found", "No data returned from Egeria", "") - else: - for catalog_item in self.digital_product_catalog_data_extract: - digital_product_catalog_table.add_row(catalog_item["Display Name"], - catalog_item["Description"], - catalog_item["Qualified Name"]) - continue - - # Data Dictionaries - data_dictionary_table: DataTable = DataTable(id="data_dictionary_table") - data_dictionary_table.add_columns("Data Dictionary Name", "Description", "Qualified Name") - data_dictionary_table.cursor_type = "row" - data_dictionary_table.zebra_stripes = True - try: - self.data_dictionary_data = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving data dictionary details: {e!s}") - self.exit(422) - return(422) - self.data_dictionary_data_extract = self.data_dictionary_data.get("data") or [] - if self.data_dictionary_data_extract == []: - self.log(f"No data dictionary details found for user: {self.user_name}") - data_dictionary_table.add_row("No data dictionaries found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.data_dictionary_data_extract} data dictionaries for user {self.user_name}") - data_dictionary_table.add_row("Display Name", "Description", "Qualified Name") - for dictionary in self.data_dictionary_data_extract: - data_dictionary_table.add_row(dictionary.get("Display Name", ""), - dictionary.get("Description", ""), - dictionary.get("Qualified Name", "")) - continue - - # Business Domains - business_domain_table: DataTable = DataTable(id="business_domain_table") - business_domain_table.add_columns("Business Area Name", "Type Name", "GUID") - business_domain_table.cursor_type = "row" - business_domain_table.zebra_stripes = True - try: - self.business_domain_data = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(423) - return(423) - self.business_domain_data_extract = self.business_domain_data.get("data") or [] - if self.business_domain_data_extract == []: - self.log(f"No business domains found for user {self.user_name}") - business_domain_table.add_row("No business domains found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.business_domain_data_extract} business domains for user {self.user_name}") - # business_domain_table.add_row("Business Domain Name", "Description", "GUID") - for domain in self.business_domain_data_extract: - business_domain_table.add_row(domain.get("Qualified Name", ""), - domain.get("Type Name", ""), - domain.get("GUID", "")) - continue - - # Data Specifications removed from the display, code is working but being replaced by Root Collections table - # data_specification_table: DataTable = DataTable(id="data_specification_table") - # data_specification_table.add_columns("Display Name", "Description", "Qualified Name") - # data_specification_table.cursor_type = "row" - # data_specification_table.zebra_stripes = True - # try: - # self.data_specification_data = exec_report_spec(format_set_name="Data-Specifications", - # output_format="DICT", - # params = {"search_string": "*"}, - # view_server=self.view_server, - # view_url=self.platform_url, - # user=self.user_name, - # user_pass=self.user_password) - # except PyegeriaException as e: - # self.log(f"Error retrieving data specification details: {e!s}") - # self.exit(423) - # return (423) - # - # if isinstance(self.data_specification_data, dict): - # self.data_specification_data_extract = self.data_specification_data.get("data") - # elif isinstance(self.data_specification_data, list): - # self.data_specification_data_extract = self.data_specification_data - # else: - # self.data_specification_data_extract = self.data_specification_data.get("Data-Specifications") or [] - # - # if self.data_specification_data_extract == [] or self.data_specification_data_extract == {} or self.data_specification_data_extract == None: - # self.log(f"No data specifications found for user {self.user_name}") - # data_specification_table.add_row("No data specifications found", "No data returned from Egeria", "") - # else: - # self.log(f"Found {self.data_specification_data_extract} data specifications for user {self.user_name}") - # - # for spec in self.data_specification_data_extract: - # data_specification_table.add_row(spec.get("Display Name", ""), - # spec.get("Description", ""), - # spec.get("Qualified Name", "")) - # continue - - # Root Collections - - self.root_collection_table = DataTable(id="root_collection_table") - self.root_collection_table.add_columns("Root Collection Name", "Description", "GUID") - self.root_collection_table.cursor_type = "row" - self.root_collection_table.zebra_stripes = True - - try: - self.collections = exec_report_spec(format_set_name="BasicCollections", - output_format="DICT", - params = {"search_string": "RootCollection"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - - except PyegeriaException as e: - print_basic_exception(e) - self.collections = "Error retrieving collections: " + str(e) - self.log(f"Found {len(self.collections)} root collections for user {self.user_name}") - self.log(f"Root collections: {self.collections}") - if isinstance(self.collections, str): - self.root_collection_table.add_row("No root collections found", self.collections, "") - elif isinstance(self.collections, dict) and self.collections.get("kind") == "json": - self.collections:list[dict] = self.collections.get("data") - for collection in self.collections: - self.root_collection_table.add_row(collection.get("Qualified Name"), collection.get("Type Name"), collection.get("GUID")) - continue - elif isinstance(self.collections, list): - for collection in self.collections: - self.root_collection_table.add_row(collection.get("Qualified Name"), collection.get("Type Name"), collection.get("GUID")) - continue - else: - self.log(f"Unexpected data type returned from Egeria: {type(self.collections)}") - self.root_collection_table.add_row("Invalid data type", "Unexpected data type returned from Egeria", "") - - # hand the data to the Screen for displaying - await self.push_screen(ShopForDataScreen(glossary_table, - digital_product_catalog_table, - data_dictionary_table, - business_domain_table, - # data_specification_table, - self.root_collection_table, - self.user_name, - self.user_password, - self.view_server, - self.platform_url), - callback = self.shop_for_data_callback) - elif selected_option == "Edit Profile": - await self.push_screen(EditProfileScreen( - self.user_name, - self.user_password, - self.view_server, - self.platform_url, - self.karma_points, - self.user_profile, - self.user_GUID - ), - callback = self.edit_profile_callback) - elif selected_option == "User Bookmarks": - pass - elif selected_option == "Subscriptions": - pass - else: - pass - - async def tech_type_callback(self, result) -> int: - """ Callback for Technology Types screen - If the result is int (4xx) it indicates an error in the screen - if the result is str it contains the GUID of the selected technology type - """ - #clear local data fields - tech_type_description = "" - self.tech_type_data = {} - self.tech_type_data_extracted = {} - self.tech_type_templates = [] - self.tech_type_processes = [] - # check that we got a valid result from the screen and process accordingly - if not result or isinstance(result, int) : - self.log(f"Technology Types screen cancelled or failed; return: {result}.") - await self.push_screen(MainScreen()) - return (result) - self.selected_t_node = str(result) - self.log(f"Technology Types screen returned: {self.selected_t_node}") - # Request details for selected tech type - try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) - self.tech_type_data = p_client.get_tech_type_detail(filter_string=self.selected_t_node, - output_format="JSON", - # body={"maxMermaidNodeCount": self.max_mermaid_node_count, - # "graphQueryDepth": self.graph_query_depth} - ) - except PyegeriaException as e: - print_basic_exception(e) - error_category = "Technology Type Process Detail Retrieval" - error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") - await self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - self.log(f"Technology Type Data: {self.tech_type_data}, type: {type(self.tech_type_data)}") - - if "specificationMermaidGraph" in self.tech_type_data: - del self.tech_type_data["specificationMermaidGraph"] - self.log(f"Technology Type Mermaid Graph Removed") - - self.tech_type_guid = self.tech_type_data.get("technologyTypeGUID") - self.tech_type_name = self.tech_type_data.get("displayName") - self.tech_type_templates = self.tech_type_data.get("catalogTemplates") - self.tech_type_templates = self.clean_structure(self.tech_type_templates) - self.tech_type_templates = self.bools_to_strings(self.tech_type_templates) - self.log(f"Templates: {self.tech_type_templates}, type: {type(self.tech_type_templates)}") - self.tech_type_processes = self.tech_type_data.get("governanceActionProcesses") - self.tech_type_processes = self.clean_structure(self.tech_type_processes) - self.tech_type_processes = self.bools_to_strings(self.tech_type_processes) - self.log(f"Processes: {self.tech_type_processes}, type: {type(self.tech_type_processes)}") - self.tech_type_description = self.tech_type_data.get("description") - - self.log(f"Technology Type GUID: {self.tech_type_guid}") - self.log(f"Technology Type Name: {self.tech_type_name}") - self.log(f"Technology Type Description: {self.tech_type_description}") - self.log(f"Technology Type Templates: {self.tech_type_templates}" or [{"templates": "None"}]) - self.log(f"Technology Type Processes: {self.tech_type_processes}" or [{"processes": "None"}]) - - await self.push_screen(TechnologyTypeOptionsScreen(self.tech_type_guid, - self.tech_type_name, - self.tech_type_description, - self.user_name, - self.user_password, - self.karma_points, - self.tech_type_templates, - self.tech_type_processes), callback = self.tech_type_options_callback) - return(200) - - async def tech_type_options_callback(self, result:list): - self.log(f"Technology Type Options screen returned: {result}") - self.selected_t_option = None - self.selected_t_option_selected = None - - if not result or result == None: - self.log(f"Technology Type Options screen returned no input; return: {result}.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) or isinstance(result, str): - if not result or result == "back": - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) and result == 200: - self.log("Technology Type Options screen returned successfully.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) and result != 200: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - self.exit(415) - else: - self.log(f"Technology Type Options screen returned unexpected result: {result}, type: {type(result)}") - elif isinstance(result, list): - self.log(f"Input is a list: {result}, type: {type(result)}") - self.log(f"Result 0 = {result[0]}, 1 = {result[1]}") - if not result[0] or isinstance(result[0], int): - if isinstance(result[0], int) and result[0] == 200: - self.log("Technology Type Options screen returned successfully.") - await self.push_screen(MainScreen()) - elif isinstance(result, list) and result[0] == "back": - await self.push_screen(MainScreen()) - else: - self.selected_t_option = str(result[0]) - self.selected_t_option_selected = str(result[1]) - self.log(f"Technology Type Options screen returned: {self.selected_t_option} | {self.selected_t_option_selected}") - else: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, type: {type(result)}.") - await self.push_screen(MainScreen()) - - # display the screen so the objects we need to mount to are created in the DOM - # then we can build the display elements for the placeholder properties - # and mount them in the appropriate containers on the screen - - self.log(f"Selected: {self.selected_t_option} | {self.selected_t_option_selected}") - if self.selected_t_option == "template": - await self.push_screen(TechnologyTypeTemplatesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_templates), - callback=self.tech_type_templates_callback) - elif self.selected_t_option == "process": - await self.push_screen(TechnologyTypeProcessesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_processes), - callback=self.tech_type_processes_callback) - else: - self.log(f"Technology Type Options screen returned invalid option: {self.selected_t_option_selected}") - self.exit(416) - return(200) - - async def fetch_technology_types(self) -> int: - self.tech_type_list: list = [{}] - try: - self.autoc = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - # retrieve the tech type data - self.log(f"Fetching technology type hierarchy for tech_type='*'") - self.tech_type_response = await self.autoc._async_get_tech_type_hierarchy(filter_string = "*" ) - except Exception as e: - self.log(f"Exception in get_tech_type_hierarchy: {e}") - self.log(print_basic_exception(e)) - self.tech_type_list = [{}] - self.exit(416) - return(416) - - self.log (f"tech_type_response: {self.tech_type_response}") - # Copy the data into a working variable for the extraction routine - self.tech_type_list = self.tech_type_response - return(200) - - def unpack_egeria_data(self) -> int: - """ Unpack the data returned from Egeria """ - output_data: list[dict] = [{}] - output_data.clear() - if isinstance(self.tech_type_data, dict): - if "data" in self.tech_type_data: - output_data = self.tech_type_data.get("data") - else: - output_data = [self.tech_type_data] - elif isinstance(self.tech_type_data, list): - for entry in self.tech_type_data: - if isinstance(entry, dict): - output_data = entry.get("data") - elif isinstance(entry, list): - for subentry in entry: - if isinstance(subentry, dict): - output_data = [subentry] - else: - self.log(f"error unknown data structure for Tech Type data") - return(417) - else: - self.log(f"error unknown outer data structure for Tech Type data") - return (417) - else: - self.log(f"Tech Type data not dict or list") - return(417) - - self.log(f"output_data: {output_data}, {type(output_data)}") - # return the extracted data (dict) - self.tech_type_list = output_data - return (200) - - def tech_type_templates_callback(self, result): - """ Callback for Technology Type Templates screen - result contains up to 3 elements: - [0] = return code or 'input', [1] input data, [2] full template""" - # take the input data and use it in the form and - # create from template - self.log(f"Technology Type Templates screen returned: {result}") - # Check for return code - if isinstance(result, int): - self.log(f"Technology Type Templates screen returned: {result}, exiting.") - return(result) - # Check for unexpected return - if not result or result[0] != "input": - self.log(f"Technology Type Templates screen cancelled/failed; return: {result}, exiting.") - return(418) - - # make the keys to the input data match the keys in the template structure - # take each key and use the data value to replace the placeholder value in the template - # then use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - # make a working version of the full template if it is returned - if isinstance(result[2], dict): - self.full_template = result[2] - # create an empty dict for the returned data - my_placeholderPropertyValues: dict = {} - # load in the returned placeholder user data - if isinstance(result[1], dict): - self.placeholder_input = result[1] - # clean up the keys of the user data to match the template keys - if isinstance(self.placeholder_input, dict): - # for each data input item in the dict - for input_item, input_value in self.placeholder_input.items(): - self.log(f"input_item: {input_item}, input_value: {input_value}") - input_fix1 = input_item.replace("_", " ") - self.log(f"input_item after underscore removal: {input_fix1}") - input_fix2 = input_fix1.replace(" placeholder input", "") - self.log(f"input_item after placeholder removal: {input_fix2}") - - self.log(f"fixed input_item: {input_fix2}, {input_value}") - # build the placeholderPropertyValues dict - my_placeholderPropertyValues.update({input_fix2: input_value}) - continue - else: - self.log(f"placeholder_input is not a dict: {self.placeholder_input}") - return(419) - - # create a dict structure that matches the template structure - # we have the input data in my_placeholderPropertyValues and - # the selected template in self.full_template - # so we can use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - self.log(f"my_placeholderPropertyValues: {my_placeholderPropertyValues}") - self.log(f"self.placeholder_input: {self.placeholder_input}") - self.log(f"self.full_template: {self.full_template}") - - # copy the retrieved template into the request body - request_body:dict = { - "class" : "TemplateRequestBody", - "externalSourceGUID" : self.full_template.get("externalSourceGUID") or "", - "externalSourceName" : self.full_template.get("externalSourceName") or "", - "typeName" : self.full_template.get("typeName") or "", - "templateGUID" : self.full_template.get("Catalog Template GUID"), - "anchorGUID" : self.full_template.get("anchorGUID"), - "isOwnAnchor" : "false", - "effectiveFrom" : "2026-01-01", - "effectiveTo": "2030-12-31", - "replacementProperties" : self.full_template.get("replacementProperties") or {}, - "placeholderPropertyValues" : {}, - "parentGUID" : None, - "parentRelationshipTypeName" : None, - "parentRelationshipProperties" : None, - "parentAtEnd1" : self.full_template.get("parentAtEnd1") or True, - "effectiveTime" : self.full_template.get("effectiveTime") or datetime.datetime.now().isoformat(), - } - self.log(f"request_body: {request_body}") - # update the request body with the user provided input - for key, value in my_placeholderPropertyValues.items(): - request_body["placeholderPropertyValues"].update({key: value}) - continue - self.log(f"request_body after update: {request_body}") - # now instantiate and call the function to create the element from the template - try: - tokendata = self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - my_md_instance = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password, tokendata) - # new_guid = my_md_instance.create_metadata_element_from_template(self.template_guid, body = request_body) - new_guid = my_md_instance.initiate_gov_action_process(body=request_body) - except Exception as e: - self.log(f"Exception in create_element_from_template: {e}") - if isinstance(e, PyegeriaException): - self.log(print_basic_exception(e)) - else: - self.log(f"Exception in create_element_from_template: {e}") - self.push_screen(StatusScreen(f"Error creating element from Template/n{print_basic_exception(e)}"), callback=self.status_callback) - return(420) - - self.log(f"new_guid: {new_guid}") - self.push_screen(StatusScreen(f"Element created from Template/Metadata element created with GUID: '{new_guid}'"), callback=self.status_callback) - - def status_callback(self, status_callback_rc) -> None: - """ Callback routine from the status screen, - this screen is typically displayed at the end of a process - to indicate success or failure of the process.""" - - self.log(f"Status screen returned: {status_callback_rc}") - self.exit(status_callback_rc) - - def tech_type_processes_callback(self, result): - """ Callback for Technology Type Processes screen""" - # take the input data and use it to get the template for running the process, display it and allow - #the user to complete the data in the placeholders in order to run the process - self.type = None - self.full_process = None - self.result = result - if isinstance(result, int): - self.log(f"Technology Type Processes Screen returned: {result}, exiting.") - return(result) - if isinstance(self.result, list): - self.type = result[0] - self.input_data = result[1] - self.full_process = result[2] - self.log(f"Received process type: {self.type}, data: {self.input_data}, full process: {self.full_process}") - else: - self.log(f"Unexpected result type from TechnologyTypeProcessesScreen: {type(result)}, exiting.") - return(418) - if self.type != "input": - self.log(f"Process type is not 'input', {self.type}, display error screen") - error_category = "Governance Action Process" - error_message = "Unknown response from TechnologyTypeProcessesScreen: "+self.type - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - my_process_property_values: list[dict] = [{}] - if not isinstance(self.result, dict): - self.log("No process Request Parameter input data returned, this is possibly valid, but unlikely") - self.log(f" Input data type: {type(self.result)}, data: {self.result}") - self.log("Processing will proceed with no Supported Request Parameters for the process") - self.result = {None:None} - else: - for input_key, input_value in self.result.items(): - self.log(f"Processing input value: {input_key} with value: {input_value}") - input_fix1 = input_key.replace("_", " ") - input_fix2 = input_fix1.replace(" process input", "") - self.log(f"Process input key after cleaning: {input_fix2}") - my_process_property_values.append ( - {"class":"SupportedRequestParameter", - "specificationPropertyType": "SUPPORTED_REQUEST_PARAMETER", - "name": input_fix2, - "value": input_value, - "Description": "", - "dataType": "string", - "example": "", - "required": "False"}) - continue - - self.log(f"Process property values: {my_process_property_values}") - self.log(f"Original input: {self.result}") - self.log(f"full process: {self.full_process}") - - request_body: dict ={ - "displayName": self.result.get("displayName"), - "description": self.result.get("description"), - "additionalProperties": - {"templateGUID": self.result.get("additionalProperties")("templateGUID")}, - "specification": - {"supportedRequestParameter": - my_process_property_values - } - } - - def tech_type_processes_details(self, tech_type, selected_t_option_selected): - """ Retrieve process data for Technology Type Processes screen""" - # ************************************************************************************************** - # take the input data and use it to get the template for running the process, display it and allow - # the user to complete the data in the placeholders in order to run the process - # with the change to retrieve the JSON this specific processing is no longer needed - # ************************************************************************************************** - self.type = None - self.full_process = None - self.selected_q_name = None - required_process = None - self.type = str(tech_type) - self.selected_q_name = str(selected_t_option_selected) - self.log(f"Received process type: {self.type}, selected: {self.selected_q_name}") - - if self.selected_q_name != None: - self.log(f"Selected qualified name: {self.selected_q_name}") - try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) - governance_actions = p_client.get_tech_type_detail(filter_string=self.selected_q_name, output_format="JSON") - except PyegeriaException as e: - print_basic_exception(e) - error_category = "Technology Type Process Detail Retrieval" - error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - self.log(f"Egeria returned: {governance_actions}, type: {type(governance_actions)}") - # load Json data - if isinstance(governance_actions, str): - if governance_actions != "No elements found": - technology_response = json.loads(governance_actions) - else: - technology_response = None - else: - technology_response = governance_actions - - self.log(f"Tech type process data retrieved: {technology_response}") - self.governance_action_processes = technology_response.get("governanceActionProcesses", []) - self.log(f"Retrieved {len(self.governance_action_processes)} governance action processes for {self.selected_q_name}") - self.log(f"Data: {self.governance_action_processes}") - for process in self.governance_action_processes: - self.log(f"Process: {process['name']}, Description: {process['description']}") - if process['name'] == self.selected_q_name: - self.log(f"Found requested process: {process['description']}") - # temporary end of function, more code needed here. - required_process = process - break - else: - continue - - return(required_process) - else: - self.log(f"selected_q_name in process_details function is None, returnng None to caller: {self.selected_q_name}") - return(None) - - async def shop_for_data_callback(self, result): - """ Callback for Shop For Data screen""" - if isinstance(result[0], int): - selection_type = int(result) - selection_parm_1 = None - selection_parm_2 = None - selection_parm_3 = None - self.log(f"Shop For Data screen returned: {selection_type}, type: {type(selection_type)}.") - else: - self.log(f"Shop For Data screen returned: {result}, type: {type(result)}.") - selection_type = result[0] - selection_parm_1 = result[1] - selection_parm_2 = result[2] - # selection_parm_3 = result[3] - self.log(f"Shop For Data screen returned: {selection_type}, with parameters: {selection_parm_1}, {selection_parm_2}") - - if isinstance(selection_type, int): - if selection_type == 200: - self.log(f"Shop For Data screen returned successfully.") - return(200) - elif selection_type == 201: - self.log(f"Shop For Data screen returned: {selection_type}, request to search for a term ") - await self.push_screen(SearchForTermScreen(self.user_name, - self.user_password, - self.view_server, - self.platform_url), callback=self.search_for_term_callback) - return(200) - elif selection_type == 211: - self.log(f"Shop For Data screen returned: {selection_type}, request to subscribe to data source ") - return(211) - elif selection_type == 212: - self.log(f"Shop For Data screen returned: {selection_type}, request to sample data source ") - self.request_to_sample_data_source(selection_parm_1, selection_parm_2, selection_parm_3) - else: - self.log(f"Shop For Data screen returned: {selection_type}.") - return(selection_type) - - if selection_type == "dictionary": - # data dictionary - self.log(f"Selected dictionary with qualified name: {selection_parm_1}") - self.build_dictionary_details(selection_parm_1, selection_parm_2) - elif selection_type == "domain": - # business domain - self.log(f"Selected business domain with qualified name: {selection_parm_1}") - self.build_domain_details(selection_parm_1, selection_parm_2) - elif selection_type == "catalog": - # data product catalog - self.log(f"Selected catalog with qualified name: {selection_parm_1}") - self.build_catalog_details(selection_parm_1, selection_parm_2) - elif selection_type == "glossary": - # glossary - self.log(f"Selected glossary with qualified name: {selection_parm_2}") - self.build_glossary_details(selection_parm_1, selection_parm_2) - elif selection_type == "collection": - # root collection - self.log(f"Selected Root Collection with qualified name: {selection_parm_1}") - self.build_root_collection_details(selection_parm_1, selection_parm_2) - else: - self.log(f"Unknown selection type: {selection_type}") - self.exit(429) - - def build_dictionary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a dictionary details screen""" - self.log(f"Building dictionary details for qualified name: {target_qualified_name}") - self.dictionary_qualified_name = target_qualified_name - self.dictionary_display_name = target_display_name - build_structure = {} - - try: - self.dictionary_details = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params={"search_string": self.dictionary_qualified_name, "filter_string": self.dictionary_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving dictionary details: {e!s}") - self.exit(420) - return (420) - self.log(f"Dictionary Details: {self.dictionary_details}") - if not self.dictionary_details or self.dictionary_details == None: - error_category = "Dictionary Details" - error_message = "No dictionary details found" - self.log(f"Error retrieving dictionary details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.dictionary_details.get("kind") == "empty": - dictionary_tree: Tree = Tree(label="Empty Dictionary", id="data_dictionary_tree") - dictionary_tree.root.expand() - dictionary_tree.root.content = "No dictionary terms found for this dictionary" - else: - dictionary_tree: Tree = Tree(label=self.dictionary_display_name, id="data_dictionary_tree") - dictionary_tree.root.expand() - dictionary_tree.auto_expand = True - self.dictionary_details_data = self.dictionary_details.get("data") - for term in self.dictionary_details_data: - self.log(f"Dictionary term: {term} being processed") - term_qualified_name = term.get("Qualified Name") or "" - term_subject = term.get("Subject Area") or "" - term_summary = term.get("Summary") or "" - # create dict structure for loading the tree - if term_subject not in build_structure: - build_structure[term_subject] = [] - build_structure[term_subject].append({term_qualified_name: term_summary}) - continue - # Once the structure is complete we can build the tree from it - for term_subject, terms in build_structure.items(): - self.log(f"Building tree for subject: {term_subject}, term: {terms}") - dictionary_branch = dictionary_tree.root.add(term_subject) - dictionary_branch.expand() - for term_dict in terms: - self.log(f"Adding term to tree: {term_dict}") - for term_qualified_name, term_summary in term_dict.items(): - dictionary_branch.add_leaf(term_summary, data=term_qualified_name) - dictionary_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("dictionary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=dictionary_tree), callback=self.overview_callback) - - def build_domain_details(self, target_qualified_name, target_type__name): - """ Build the details object for a business domain details screen""" - self.log(f"Building domain details for qualified name: {target_qualified_name}") - self.domain_qualified_name = target_qualified_name - self.domain_type__name = target_type__name - build_structure = {} - - try: - self.domain_details = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params={"search_string": self.domain_qualified_name, "filter_string": self.domain_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(420) - return (420) - self.log(f"domain_details: {self.domain_details}") - if not self.domain_details or self.domain_details == None: - error_category = "Business Domain Details" - error_message = "No domain details found" - self.log(f"Error retrieving business domain details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.domain_details.get("kind") == "empty": - domain_tree: Tree = Tree(label="Empty Business Domain", id="business_domain_tree") - domain_tree.root.expand() - domain_tree.root.content = "No domain details found for this business domain" - else: - self.domain_details_data = self.domain_details.get("data") - self.log(f"domain_details_data: {self.domain_details_data}") - self.domain_display_name = self.domain_details_data.get("Qualified Name") - domain_tree: Tree = Tree(label=self.domain_display_name, id="business_domain_tree") - domain_tree.root.expand() - domain_tree.auto_expand = True - for term in self.domain_details_data: - if term == None: - continue - term_qualified_name = term.get("Qualified Name") or "" - term_type = term.get("Type Name") or "" - term_GUID = term.get("GUID") or "" - term_members = term.get("Containing Members") - term_memberof = term.get("Member Of") - # create dict structure for loading the tree - build_structure[term_qualified_name] = { - "term_type": term_type, - "term_GUID": term_GUID, - "term_members": term_members, - "term_memberof": term_memberof - } - continue - # Once the structure is complete we can build the tree from it - for qualified_name, details in build_structure.items(): - domain_branch = domain_tree.root.add(qualified_name, id=qualified_name, data=[details["term_type"], details["term_GUID"]]) - if details["term_members"] != None: - domain_branch_members = domain_branch.add("Containing Members") - for member in details["term_members"]: - domain_branch_members.add_leaf(member) - if details["term_memberof"] != None: - for member in details["term_memberof"]: - domain_branch.add_leaf(member) - domain_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("domain", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=domain_tree), callback=self.overview_callback) - - def build_catalog_details(self, target_qualified_name, target_display_name): - """ Build the details object for a product catalog details screen""" - self.log(f"Building product catalog details for qualified name: {target_qualified_name}") - self.catalog_qualified_name = target_qualified_name - self.catalog_display_name = target_display_name - build_structure = {} - - try: - self.catalog_details = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params={"search_string": self.catalog_qualified_name, "filter_string": self.catalog_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving catalog details: {e!s}") - self.exit(420) - return (420) - self.log(f"catalog_details: {self.catalog_details}") - if not self.catalog_details or self.catalog_details == None: - error_category = "Catalog Details" - error_message = "No catalog details found" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.catalog_details.get("kind") == "empty": - catalog_tree: Tree = Tree(label="Empty Catalog", id="digital_product_catalog_tree") - catalog_tree.root.expand() - catalog_tree.root.content = "No catalog terms found for this catalog" - else: - catalog_tree: Tree = Tree(label=self.catalog_display_name, id="digital_product_catalog_tree") - catalog_tree.root.expand() - catalog_tree.auto_expand = True - self.catalog_details_data: list[dict] = self.catalog_details.get("data") - self.log(f"catalog_details_data: {self.catalog_details_data}") - if not self.catalog_details_data or self.catalog_details_data == None: - error_category = "Catalog Details" - error_message = "No catalog details found or the data dict entry is missing" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - for product in self.catalog_details_data: - self.log(f"product: {product}") - term_qualified_name = product.get("Qualified Name") or "" - term_subject = product.get("Display Name") or "" - term_summary = product.get("Description") or "" - # create dict structure for loading the tree - if term_subject not in build_structure: - build_structure[term_subject] = [] - build_structure[term_subject].append({term_qualified_name: term_summary}) - self.log(f"build_structure: {build_structure}") - continue - # Once the structure is complete we can build the tree from it - sample_data: list = [] - for instance, data_prods in build_structure.items(): - catalog_branch = catalog_tree.root.add(instance) - for data_prod in data_prods: - for term_qualified_name, term_summary in data_prod.items(): - catalog_branch.add_leaf(term_summary, data=term_qualified_name) - self.log(f"term_qualified_name: {term_qualified_name}, term summary: {term_summary}") - catalog_tree.root.expand() - - # get some sample data from the data source for each product - for product in self.catalog_details_data: - collection_memberships = product.get("Member Of") - if collection_memberships: - collection_membership_list = collection_memberships.split(",") - self.log(f"Membership List for product {product.get('Display Name')}: {collection_membership_list}") - for membership_qname in collection_membership_list: - self.log(f"Processing collection membership: {membership_qname}") - if "DigitalProduct" in membership_qname: - self.log(f"Processing dataset: {membership_qname}") - try: - # create client instance and retrieve data - dclient = DataEngineer(self.view_server, self.platform_url, self.user_name, - self.user_password) - token = dclient.create_egeria_bearer_token(self.user_name, self.user_password) - data_set_metadata = dclient.find_tabular_data_sets( - search_string=membership_qname, - start_from=0, - page_size=1, - output_format="DICT" - ) - self.log(f"Dataset metadata retrieved: {data_set_metadata}") - if isinstance(data_set_metadata, list) and len(data_set_metadata) > 0: - data_set_guid = data_set_metadata[0].get("GUID") - if data_set_guid: - data_set_data = dclient.get_tabular_data_set( - tabular_data_set_guid=data_set_guid, - start_from_row=0, - max_row_count=10, - output_format="MD" - ) - self.log(f"Dataset data retrieved: {data_set_data}") - sample_data.append(data_set_data) - else: - sample_data.append(f"No GUID found for dataset {membership_qname}") - else: - sample_data.append(f"No metadata found for dataset {membership_qname}") - continue - except PyegeriaException as e: - self.log(f"Error retrieving dataset data: {e}") - print_basic_exception(e) - else: - continue - self.log(f"Sample data after product {product.get('Display Name')}: {sample_data}") - self.log(f"Final sample data length: {len(sample_data)}") - - self.push_screen(SelectionOverviewScreen("catalog", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=catalog_tree, - data_samples=sample_data), callback=self.overview_callback) - - def build_glossary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a glossary details screen""" - self.log(f"Building glossary details for qualified name: {target_qualified_name}") - self.glossary_qualified_name = target_qualified_name - self.glossary_display_name = target_display_name - - for glossary_instance in self.glossary_data_extract: - if glossary_instance.get("Qualified Name") == target_qualified_name: - self.glossary_folders = glossary_instance.get("Folders") or None - self.log(f"glossary_folders: {self.glossary_folders}") - category = "Uncategorised" - if self.glossary_folders != None: - glossary_tree: Tree = Tree(label=self.glossary_display_name, id="glossary_details_tree") - glossary_tree.root.expand() - glossary_tree.auto_expand = True - - folder_entries = [f.strip() for f in self.glossary_folders.split(',')] - nodes = {(): glossary_tree.root} - prefixes = ["GlossaryCategory", "GlossaryTerm", "CollectionFolder"] - - for entry in folder_entries: - parts = entry.split('::') - if len(parts) == 1: - parts = entry.split('/') - - is_leaf = False - path_parts = [] - full_id = "" - if any(parts[0].startswith(p) for p in prefixes): - type_prefix = parts[0] - path_parts = parts[1:] - is_leaf = "Term" in type_prefix - if is_leaf: - full_id = path_parts[-1] - else: - path_parts = parts - - current_path = () - for i, part in enumerate(path_parts): - parent_path = current_path - current_path = current_path + (part,) - - if current_path not in nodes: - parent_node = nodes[parent_path] - if is_leaf and i == len(path_parts) - 1: - nodes[current_path] = parent_node.add_leaf(part, data=full_id) - else: - new_node = parent_node.add(part, data=part) - new_node.expand() - nodes[current_path] = new_node - - glossary_tree.refresh() - else: - self.log(f"No glossary folders found in the glossary data extract") - folder_category = "Empty Glossary" - category = glossary_tree.root.add(folder_category) - folder_term = "No glossary terms found for this glossary" - category.add_leaf(folder_term) - - self.push_screen(SelectionOverviewScreen("glossary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=glossary_tree), callback=self.overview_callback) - - def build_root_collection_details(self, target_qualified_name, target_display_name): - """ Build the details object for a root collection details screen""" - member_tree: Tree = Tree(label="Root Collection", id="root_collection_members_tree") - member_tree.root.expand() - member_tree.auto_expand = True - - self.log(f"Building root collection details for qualified name: {target_qualified_name}") - self.root_collection_qualified_name = target_qualified_name - collection_branch = member_tree.root.add(self.root_collection_qualified_name, expand=True) - self.log(f"self_collections: {self.collections}") - collection = self.collections[0] - self.log(f"collection: {collection}") - if target_qualified_name == collection.get("Qualified Name"): - root_collection_contains: str = collection.get("Containing Members") or None - self.log(f"root_collection_contains: {root_collection_contains}") - folders = str.split(root_collection_contains, ', ') - for folder in folders: - collection_branch.add_leaf(folder) - continue - - self.push_screen(SelectionOverviewScreen("collection", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=member_tree), callback=self.overview_callback) - - - - def overview_callback(self, r_code): - """Callback function for handling overview screen actions.""" - if isinstance(r_code, list): - self.selected_item = r_code[1] - self.selected_tree = r_code[2] - r_code: int = r_code[0] - if r_code == 410: - error_category = "Collection Category" - error_message = "Unknown collection category returned" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 411: - error_category = "Glossary" - error_message = "query_one no matches found for glossary tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 412: - error_category = "Digital Product Catalog" - error_message = "query_one no matches found for digital product catalog tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 413: - error_category = "Data Dictionary" - error_message = "query_one no matches found for data dictionary tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 414: - error_category = "Business Domain" - error_message = "query_one no matches fround for business domain tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - elif r_code == 415: - error_category = "Root Collections" - error_message = "query_one no matches found for root collections tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - else: - # good return from overview screen - self.log (f"Overview screen callback, return code : {r_code}") - if r_code == 211: - # Subscribe to selected item - self.log(f"Subscribing to selected item: {self.selected_item} from {self.selected_tree}") - try: - s_client = ProductManager(self.view_server, self.platform_url, self.user_name, self.user_password) - s_client.create_egeria_bearer_token(self.user_name, self.user_password) - s_client.create_digital_subscription(self.selected_item) - except (PyegeriaInvalidParameterException, PyegeriaAPIException, PyegeriaException): - self.log(f"Error creating digital subscription: {self.selected_item} from {self.selected_tree}") - # more error handling required here - """ Request Body layout, not all fields are required to be completed - { - "class" : "NewAgreementRequestBody", - "isOwnAnchor" : true, - "anchorScopeGUID" : "", - "parentGUID" : "", - "parentRelationshipTypeName" : "CollectionMembership", - "parentAtEnd1": true, - "properties": { - "class" : "DigitalSubscriptionProperties", - "qualifiedName": "DigitalSubscription::", - "displayName" : "display name", - "description" : "Add description of the subscription here", - "userDefinedStatus" : "DRAFT", - "identifier" : "Add subscription identifier here", - "supportLevel" : "Add the level of support agreed/requested", - "serviceLevels" : { - "property1Name" : "property1Value", - "property2Name" : "property2Value" - }, - "additionalProperties": { - "property1Name" : "property1Value", - "property2Name" : "property2Value" - } - }, - "initialStatus" : "OTHER", - "externalSourceGUID": "add guid here", - "externalSourceName": "add qualified name here", - "effectiveTime" : "{{$isoTimestamp}}", - "forLineage" : false, - "forDuplicateProcessing" : false - }""" - self.push_screen(CreateSubscriptionRequestScreen(), callback=self.create_subscription_callback) - else: - self.push_screen(ShopForDataScreen(self.view_server, self.platform_url, self.user_name, self.user_password, self.selected_item, self.selected_tree), callback=self.shop_for_data_callback) - - def create_subscription_callback(self, result): - """ Callback routine for create subscription request screen - This routine checks for valid input and then processes the subscription request """ - if result is None: - self.log("User cancelled subscription creation") - return - self.log(f"Subscription created: {result}") - - def extract_glossary_terms(self, text): - """ - Extracts GlossaryTerm items from a string structure. - Pattern: Starts with 'GlossaryTerm::', ends at the next ','. - """ - # Pattern explanation: - # GlossaryTerm:: - Matches the literal prefix - # ([^,\']+) - Captures one or more characters that are NOT a comma or a single quote - pattern = r"GlossaryTerm::([^,\']+)" - - # Find all matches in the text - matches = re.findall(pattern, text) - - self.log(f"Extracted {len(matches)} GlossaryTerm items from text") - - # Clean up whitespace and return as a list - return [match.strip() for match in matches] - - def display_glossary_term_details(self, term): - """ Displays the details of a GlossaryTerm item""" - self.target_term = term - try: - self.term_details = exec_report_spec(format_set_name="Glossary-Terms", - output_format="JSON", - params={"search_string": self.target_term, "filter_string": self.target_term}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving term details: {e!s}") - self.exit(440) - return (440) - - self.log(f"term_details: {self.term_details}") - if not self.term_details or self.term_details == None: - error_category = "Glossary Term Details" - error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.term_details.get("kind") == "empty": - self.log(f"No glossary term details found for qualified name: {self.target_term}") - error_category = "Glossary Term Details" - error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - self.term_details_data = self.term_details.get("data") - - if not self.term_details_data or self.term_details_data == None: - error_category = "Glossary Term Details" - error_message = "No glossary term details found or the data dict entry is missing" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - else: - self.term_details_container: ScrollableContainer = self.screen.query_one(DataTable) - self.term_details_container.mount() - for data_item_key, data_item_value in self.term_details_data.items(): - Static(f"Field: {data_item_key}, Value: {data_item_value}").mount(self.term_details_container) - continue - # Once all the fields are complete we can leave it to the screen processing to handle the rest - return (200) - - @on(DataTable.RowSelected, "#roles_table") - def handle_roles_table_row_selection(self, event): - role_table = event.data_table - selected_row_key = event.row_key - selected_row_data = role_table.get_row(selected_row_key) - selected_role_description = selected_row_data[2] - selected_role_name = selected_row_data[0] - selected_role_type = selected_row_data[1] - selected_role_guid = selected_row_data[3] - self.log(f"Selected role: {selected_row_data}") - team_members_list: list = [] - self.team_members = [] - - # provide list of team members for team leader - if "TeamLeader" in selected_role_name or "TeamLeader" in selected_role_type: - self.log(f"Selected role is a TeamLeader: {selected_role_name}") - team_members_list, team_display_name, team_qualified_name, team_category, team_description = self.find_team_members(selected_role_name) - elif "TeamMembers" in selected_role_name or "TeamMembers" in selected_role_type: - self.log(f"Selected role is a TeamMember: {selected_role_name}") - team_members_list, team_display_name, team_qualified_name, team_category, team_description = self.find_team_members(selected_role_name) - else: - self.log(f"Selected role is not a TeamLeader or TeamMember: {selected_role_name}") - return (201) - - self.log(f"team_members_list: {team_members_list}") - # Process team data for display on screen - team_member_properties: list = [] - team_properties: list = [] - team_properties.append(team_display_name) - team_properties.append(team_qualified_name) - team_properties.append(team_category) - team_properties.append(team_description) - self.log(f"team_properties: {team_properties}") - # Process team member data for display on screen - for team_member in team_members_list: - if isinstance(team_member, list) and "Selection Error" in team_member[0]: - self.log(f"Team member details contain 'Selection Error': {team_member}") - self.team_members.append(team_member) - break - else: - self.log(f"Processing team member properties: {team_member}") - team_member_properties = [] - team_member_properties.append(team_member.get("Individual")) - team_member_properties.append(team_member.get("Assignment Type")) - team_member_properties.append(team_member.get("Individual GUID")) - self.log(f"team_member_properties: {team_member_properties}") - self.team_members.append(team_member_properties) - self.log(f"team_members: {self.team_members}") - continue - self.log(f"team_members: {self.team_members}") - self.log(f"team_properties: {team_properties}") - self.log(f"User name: {self.user_name}") - self.push_screen(MyTeam(self.team_members, team_properties, self.user_name), callback=self.my_team_callback) - - def find_team_members(self, role_name): - # common routine for finding team members given a role name - # extract the Department::nnn from the name to use as the search key - selected_role_name = role_name - team_members_list: list = [] - self.team_members = [] - - search_key_parts = selected_role_name.split("::") - role_search_key = '::'.join(search_key_parts[1:]) - self.log(f"Role search key: {role_search_key}") - # get the team member details for that department - team_member_data: dict = exec_report_spec(format_set_name="Team-Members", - output_format="DICT", - params={"search_string": role_search_key}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - self.log(f"team_member_data: {team_member_data}") - # team members? - if team_member_data.get("kind") != "empty": - team_members_data_struct: list[dict] = team_member_data.get("data") - self.log(f"team members data extracted: {team_members_data_struct}") - else: - self.log(f"No team members found for role: {role_search_key}") - error_category = "Team Members" - error_message = "No team members found" - self.log(f"Error retrieving team members: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - team_members_list.clear() - for team_member in team_members_data_struct: - # get team details - team_display_name: str = team_member.get("Display Name", None) - team_qualified_name: str = team_member.get("Qualified Name", None) - team_category: str = team_member.get("Category", None) - team_description: str = team_member.get("Description", None) - # extract the team member entries - team_member_structure: list[dict] = team_member.get("Members", None) - - if team_member_structure != None: - for entry in team_member_structure: - team_members_list.append(entry) - - # if this is for a team leader then: - # get team members profile for contact information and contribution record (karma points) - - # if "TeamLeader" in selected_role_name: - # member_guid = entry.get("Individual GUID", None) - # - - continue - return(team_members_list, team_display_name, team_qualified_name, team_category, team_description) - else: - error_category = "Team Member Details" - error_message = "No team member details found" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return (430) - - def my_team_callback(self, status) -> None: - self.log(f"Callback received with status: {status}") - if status == 200: - self.log("MyTeam screen completed successfully") - else: - self.log(f"Error in MyTeam screen: {status}") - self.push_screen("main") - - def search_for_term_callback(self, status) -> None: - self.log(f"Callback received with status: {status}") - if status == 200: - self.log("Search for term screen completed successfully") - self.push_screen("main") - elif status == 201: - glossary_table = self.query_one("g#glossary_table", DataTable) - digital_product_catalog_table = self.query_one("d#digital_product_catalog_table", DataTable) - data_dictionary_table = self.query_one("d#data_dictionary_table", DataTable) - business_domain_table = self.query_one("b#business_domain_table", DataTable) - data_specification_table = self.query_one("d#data_specification_table", DataTable) - self.log("No matches found for search term") - self.push_screen(ShopForDataScreen(glossary_table, - digital_product_catalog_table, - data_dictionary_table, - business_domain_table, - data_specification_table, - self.user_name, - self.user_password, - self.view_server, - self.platform_url), - callback=self.shop_for_data_callback) - - else: - self.log(f"Error in Search for term screen: {status}") - error_category = "Search for Term" - error_message = "Error retrieving Term details" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - def request_to_sample_data_source(self, selection_parm_1, selection_parm_2, selection_parm_3): - """ The user has requested to see a sample of the data from the selected data source """ - self.row_highlighted = selection_parm_1 - self.cursor_row_highlighted = selection_parm_2 - self.data_table_highlighted = selection_parm_3 - self.log(f"Data selected: row={selection_parm_1}, cursor_row={selection_parm_2}, data_table={selection_parm_3}") - selected_item = self.query_one("#"+self.data_table_highlighted, DataTable) - item_content = selected_item.get_row(self.row_highlighted) - - - def truncate_at_sequence(self, data, target="specificationMermaidGraph"): - """ - Recursively traverses data. Truncates strings containing the target - and drops all following sibling items within lists or dictionaries. - Returns (truncated_data, should_terminate_parent_container) - """ - # Handle Strings - if isinstance(data, str): - if target in data: - # Drop the sequence and everything after it - truncated_str = data.split(target)[0] - return truncated_str, True - return data, False - - # Handle Dictionaries - elif isinstance(data, dict): - new_dict = {} - for key, value in data.items(): - # First, check if the key itself contains the target - if isinstance(key, str) and target in key: - truncated_key = key.split(target)[0] - if truncated_key: # Keep key fragment if not empty - new_dict[truncated_key] = value - return new_dict, True - - # Recursively check the value - updated_value, terminate = self.truncate_at_sequence(value, target) - new_dict[key] = updated_value - if terminate: - return new_dict, True - return new_dict, False - - # Handle Lists and Tuples - elif isinstance(data, (list, tuple)): - new_list = [] - for item in data: - updated_item, terminate = self.truncate_at_sequence(item, target) - new_list.append(updated_item) - if terminate: - break - # Maintain original type integrity - return (tuple(new_list) if isinstance(data, tuple) else new_list), terminate - - # Handle Booleans, Numbers, None - return data, False - - # Helper wrapper for clean execution - def clean_structure(self, data, target="specificationMermaidGraph"): - result, _ = self.truncate_at_sequence(data, target) - return result - - def bools_to_strings(self, data): - """ - Recursively traverses data structures and converts - all Boolean values into their corresponding string format. - """ - # Check explicitly for bool first (since bool is a subclass of int in Python) - if isinstance(data, bool): - return str(data) - - # Handle Dictionaries - elif isinstance(data, dict): - return {key: self.bools_to_strings(value) for key, value in data.items()} - - # Handle Lists and Tuples - elif isinstance(data, (list, tuple)): - converted = [self.bools_to_strings(item) for item in data] - return tuple(converted) if isinstance(data, tuple) else converted - - # Return all other types unchanged (ints, floats, strings, None) - return data - - def user_identities_callback(self): - """ Callback roiutine for the user identities screen - the user has requested to exit the screen and so will will - push the main screen again""" - self.push_screen("main") - - def edit_identities_callback(self, rows_with_keys): - """ Callback for EditIdentitiesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#user_identity_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_communities_callback(self, rows_with_keys): - """ Callback for EditCommunitiesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#communities_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_roles_callback(self, rows_with_keys): - """ Callback for EditRolesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#roles_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_teams_callback(self, rows_with_keys): - """ Callback for EditTeamsScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#teams_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_profile_callback(self, return_c): - """ Callback routine for the edit profile screen """ - if isinstance(return_c, int): - if return_c == 200: - self.push_screen("main") - else: - self.log(f"Error returned from EditProfileScreen: {return_c}") - self.log("Returning to main screen") - self.push_screen("main") - elif isinstance(return_c, str): - if return_c == "identity": - main_screen = self.get_screen("main") - self.identities_table = main_screen.query_one("#user_identity_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.identities_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.identities_table.rows: - rows_with_keys.append((row_key.value, self.identities_table.get_row(row_key))) - self.push_screen(EditIdentitiesScreen(columns, rows_with_keys), callback=self.edit_identities_callback) - elif return_c == "community": - main_screen = self.get_screen("main") - self.communities_table = main_screen.query_one("#communities_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.communities_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.communities_table.rows: - rows_with_keys.append((row_key.value, self.communities_table.get_row(row_key))) - self.push_screen(EditCommunitiesScreen(columns, rows_with_keys), callback=self.edit_communities_callback) - elif return_c == "role": - main_screen = self.get_screen("main") - self.roles_table = main_screen.query_one("#roles_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.roles_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.roles_table.rows: - rows_with_keys.append((row_key.value, self.roles_table.get_row(row_key))) - self.push_screen(EditRolesScreen(columns, rows_with_keys), callback=self.edit_roles_callback) - elif return_c == "team": - main_screen = self.get_screen("main") - self.teams_table = main_screen.query_one("#teams_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.teams_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.teams_table.rows: - # row_key.value gets the string representation of the RowKey - # get_row() returns the list of cell values for that row - rows_with_keys.append((row_key.value, self.teams_table.get_row(row_key))) - self.push_screen(EditTeamsScreen(columns, rows_with_keys), callback=self.edit_teams_callback) - else: - self.log(f"Unexpected return type from EditProfileScreen: {type(return_c)}") - self.push_screen("main") - - -if __name__ == "__main__": - app = MyProfileApp() - app.run() -# -# if __name__ == "__main__": -# main() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile_app.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile_app.py deleted file mode 100644 index 57a848c9..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/my_profile_app.py +++ /dev/null @@ -1,2217 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -import datetime -import re -import json -import sys -from pathlib import Path -from typing import Any - -# Add the project root to sys.path to allow running this script from any directory -root_path = Path(__file__).resolve().parents[4] -if str(root_path) not in sys.path: - sys.path.append(str(root_path)) - -from pyegeria import (load_app_config, settings, MyProfile, PyegeriaException, - print_basic_exception, exec_report_spec, - AutomatedCuration, ProductManager, - PyegeriaInvalidParameterException, PyegeriaAPIException, DataEngineer, Egeria) -from textual import on -from textual.app import App, ComposeResult -from textual.containers import ScrollableContainer -from textual.widgets import DataTable, OptionList, Header, Static, Footer, Tree - -from CreateProfileScreen import CreateProfileScreen -from EditElementsScreens import (EditProfileScreen, - EditCommunitiesScreen, - EditIdentitiesScreen, - EditProjectsScreen, - EditTodosScreen, - EditRolesScreen, - EditTeamsScreen, - EditBlogsScreen, - EditJournalScreen, - EditAssociationsScreen) -from TechnologyTypeScreens import (TechnologyTypesScreen, - TechnologyTypeOptionsScreen, - TechnologyTypeTemplatesScreen, - TechnologyTypeProcessesScreen) -from StatusScreen import StatusScreen -from ShopForDataScreen import ShopForDataScreen -from SelectionOverviewScreen import SelectionOverviewScreen -from MyTeamScreen import MyTeam -from MainScreen import MainScreen -from SearchForTermScreen import SearchForTermScreen -from CreateSubscriptionRequestScreen import CreateSubscriptionRequestScreen -from UserIdentitiesScreen import UserIdentitiesScreen -from ShowCommentsScreen import ShowCommentsScreen -from AddToElementsScreens import (AddRoleScreen, - AddProjectScreen, - AddCommunityScreen, - AddTeamScreen, - AddBlogEntryScreen, - AddJournalEntryScreen, - AddTodoScreen, - AddAssociationScreen) - -class MyProfileApp(App): - """My Profile App. - - Retrieves a user's profile from Egeria and displays current work items. - If no profile is found, offers a UI to create one. - """ - - BINDINGS = [("q", "quit", "Quit"), - ("r", "refresh", "Refresh Data"), - ] - - CSS_PATH = "my_profile.tcss" - - SCREENS = { - "main": MainScreen, - "create_profile": CreateProfileScreen, - "edit_profile": EditProfileScreen, - "edit_communities": EditCommunitiesScreen, - "edit_identities": EditIdentitiesScreen, - "edit_roles": EditRolesScreen, - "edit_teams": EditTeamsScreen, - "edit_todos": EditTodosScreen, - "edit_projects": EditProjectsScreen, - "edit_blogs": EditBlogsScreen, - "edit_journal": EditJournalScreen, - "edit_associations": EditAssociationsScreen, - "tech_types": TechnologyTypesScreen, - "tech_type_options": TechnologyTypeOptionsScreen, - "tech_type_templates": TechnologyTypeTemplatesScreen, - "tech_type_processes": TechnologyTypeProcessesScreen, - "status": StatusScreen, - "shop_4_data": ShopForDataScreen, - "search_for_term": SearchForTermScreen, - "overview": SelectionOverviewScreen, - "create_subscription": CreateSubscriptionRequestScreen, - "my_team": MyTeam, - "show_comments": ShowCommentsScreen, - "add_role": AddRoleScreen, - "add_project": AddProjectScreen, - "add_community": AddCommunityScreen, - "add_team": AddTeamScreen, - "add_blog_entry": AddBlogEntryScreen, - "add_journal_entry": AddJournalEntryScreen, - "add_todo": AddTodoScreen, - "add_association": AddAssociationScreen, - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.contribution_record = None - self.heading = "My_Profile" - self.subheading = "Egeria Profile for current user" - self.description = "Display the user related items for the current user." - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - print("Platform:", app_config.egeria_platform_url) - print("View Server:", app_config.egeria_view_server) - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - - # Ensure compose() is safe before data loads - self.actor_profile: dict = {} - self.projects = [] - self.communities = [] - self.roles = [] - self.blogs = [] - self.journal = [] - self.todos = [] - self.teams = [] - self.other_function_list = [] - self.tech_type_json: str = "" - self.tech_type_response = None - self.tech_type_list = [] - self.tech_type_guid = "" - self.tech_type_name = "" - self.tech_type_description = "" - self.selected_t_node = None - self.selected_t_node_label = None - self.karma_points = 0 - self.tech_type_templates = [{}] - self.tech_type_processes = [{}] - self.full_template = None - self.glossary_data_extract = None - self.business_glossary_data_extract = None - self.display_glossary_data_extract = None - self.digital_glossary_data_extract = None - self.team_members: list[list] = [] - self.max_mermaid_node_count = 0 # This is to tell egeria we dont want mermaid graphs in the response packet. - self.graph_query_depth = 0 # This tell egeria not to include relationships in the response packet - self.user_GUID = "" - self.user_data = {} - - def compose(self) -> ComposeResult: - yield Header() - yield Footer() - - async def on_mount(self) -> None: - """Load profile; if missing, prompt to create it; then populate tables.""" - # DOMInfo.attach_to(self) - await self.push_screen("main") - await self._load_or_create_profile() - await self._populate_tables() - - - async def _load_or_create_profile(self) -> None: - try: - #instantiate the class - self.my_profile_inst = MyProfile(self.view_server, self.platform_url, self.user_name, self.user_password) - self.my_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) - #retrieve the profile - self.my_profile_data = await self.my_profile_inst._async_get_my_profile( - report_spec="My-User-MD", - output_format="DICT", - ) - self.log(f"retrieve profile result: {self.my_profile_data}") - except (PyegeriaException) as e: - self.log(f"Error retrieving profile: {e!s}") - print_basic_exception(e) - self.exit(402) - return - - if self.my_profile_data == []: - self.log(f"Error retrieving profile. Prompting to create one...") - self.log(f"To create a profile you must have a valid userid in the system, please contact your system administrator to create one if needed") - await self.push_screen(CreateProfileScreen( - self.user_name, - self.user_password, - self.view_server, - self.platform_url, - ), callback = self.new_profile_return) - else: - self.new_profile_return(200) - - def new_profile_return(self, result: int) -> None: - """ This function handles either the return from the create new profile screen or - when the user already has a profile continue processing """ - self.log(f"Profile creation result: {result}") - if not result or result != 200: - self.log(f"Profile creation cancelled/failed; return: {result}, exiting.") - self.exit(403) - return - - self.result = result - - # Retry after creation if necessary - try: - self.user_profile_struct = self.my_profile_inst.get_my_profile( - output_format="DICT", - report_spec="My-User-MD", - ) - self.log(f"Profile retrieved successfully: {self.user_profile_struct}") - - self._show_main_screen() - except PyegeriaException as e2: - self.log(f"Error retrieving user profile: {e2!s}") - self.exit(412) - return - if not self.user_profile_struct or self.user_profile_struct == []: - self.log(f"Error retrieving user profile. Exiting.") - self.exit(413) - return - - # clear the target data structures. - self.my_blogs_data = [{}] - self.my_journal_data = [{}] - - # strip out the individual profile elements - self.user_profile = self.user_profile_struct[0] - self.contribution_record = self.user_profile.get("Contribution Record") or {} - if isinstance(self.contribution_record, list) and len(self.contribution_record) > 0: - self.karma_points = self.contribution_record[0].get("Karma Points") or 0 - elif isinstance(self.contribution_record, dict): - self.karma_points = self.contribution_record.get("Karma Points") or 0 - else: - self.karma_points = 0 - self.my_projects_data = self.user_profile.get("Projects") or [] - self.my_teams_data = self.user_profile.get("Teams") or [] - self.my_communities_data = self.user_profile.get("Communities") or [] - self.my_roles_data = self.user_profile.get("Roles") or [] - self.my_note_logs = self.user_profile.get("Note Logs") or [] - self.log(f"my_note_logs: {self.my_note_logs}, type: {type(self.my_note_logs)}") - for entry in self.my_note_logs: - if entry.get("class") == "BlogEntryProperties": - self.my_blogs_data.append(entry) - continue - elif entry.get("class") == "JournalEntryProperties": - self.my_journal_data.append(entry) - continue - self.log(f"Contribution Record: {self.contribution_record}") - self.log(f"Karma Points: {self.karma_points}") - self.log(f"my_projects_data: {self.my_projects_data}") - self.log(f"my_teams_data: {self.my_teams_data}") - self.log(f"my_communities_data: {self.my_communities_data}") - self.log(f"my_roles_data: {self.my_roles_data}") - self.log(f"my_blogs_data: {self.my_blogs_data}") - self.log(f"my_journal_data: {self.my_journal_data}") - - # User Identities - try: - self.user_identities = self.my_profile_inst.get_my_profile( - report_spec="User-Identities", - output_format="DICT", - ) - self.log(f"User-Identities: {self.user_identities}, type: {type(self.user_identities)}") - except PyegeriaException as e: - self.log(f"Error retrieving User-Identities: {e!s}") - self.user_identities = {} - - # User To-Dos - - try: - self.my_todos_data = self.my_profile_inst.get_my_to_dos(report_spec="My-User-ToDos", - output_format="DICT", - ) - self.log(f"My To-Dos: {self.my_todos_data}, type: {type(self.my_todos_data)}") - except PyegeriaException as e: - self.log(f"Error retrieving My To-Dos: {e!s}") - self.my_todos_data = {} - - self.log(f"my_todos_data: {self.my_todos_data}") - - # User GUID — resolve self-scoped; never let a missing/empty lookup crash - # the app (this runs in on_mount, before first paint, so an unguarded - # TypeError here kills the whole session). - self.user_GUID = "" - # The current user's own profile already carries its GUID when - # the My-User-MD format set includes it — no directory search required. - if isinstance(self.user_profile, dict) and self.user_profile.get("GUID"): - self.user_GUID = self.user_profile.get("GUID") - else: - # Fallback: actor-profile directory lookup. Egeria's Actor-Profiles - # search matches the display *name*, so a userId search legitimately - # returns {'kind': 'empty'} → guard against None / empty data. - try: - actor = exec_report_spec(format_set_name="Actor-Profiles", - output_format="DICT", - params={"search_string": self.user_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving actor profile: {e!s}") - actor = None - data = actor.get("data") if isinstance(actor, dict) else None - if data: - self.user_GUID = data[0].get("GUID") or "" - else: - self.log("Actor-Profiles lookup returned no data; user_GUID left unset.") - self.log(f"User GUID retrieved: {self.user_GUID!r}") - - # Normalize expected keys - self.full_name = self.user_profile.get("Full Name") or "" - self.sub_title = f"{self.full_name} ({self.user_profile.get('User ID')}, Karma Points: {self.karma_points})" - self.projects = self.my_projects_data or [] - self.communities = self.my_communities_data or [] - self.roles = self.my_roles_data or [] - self.blogs = self.my_blogs_data or [] - self.journal = self.my_journal_data or [] - self.todos = self.my_todos_data or [] - self.teams = self.my_teams_data or [] - self.log(f"Blogs data: {self.blogs}") - self.log(f"Journal data: {self.journal}") - self.log(f"Todos data: {self.todos}") - if isinstance(self.user_identities, list): - self.user_identity = self.user_identities - else: - self.user_identity = self.user_identities.get("User-Identities") or [] - - async def _populate_tables(self) -> None: - """Populates tables from normalized profile data""" - - main_screen = self.get_screen("main") - - self.projects_table = main_screen.query_one("#projects_table", DataTable) - self.communities_table = main_screen.query_one("#communities_table", DataTable) - self.roles_table = main_screen.query_one("#roles_table", DataTable) - self.blogs_table = main_screen.query_one("#blogs_table", DataTable) - self.journal_table = main_screen.query_one("#journal_table", DataTable) - self.todos_table = main_screen.query_one("#todos_table", DataTable) - self.user_identity_table = main_screen.query_one("#user_identity_table", DataTable) - self.teams_table = main_screen.query_one("#teams_table", DataTable) - self.associations_table = main_screen.query_one("#associations_table", DataTable) - self.my_collections_table = main_screen.query_one("#my_collections_table", DataTable) - - # assert self.projects_table is not None - # assert self.communities_table is not None - # assert self.roles_table is not None - # assert self.blogs_table is not None - # assert self.journal_table is not None - # assert self.todos_table is not None - # assert self.user_identity_table is not None - # assert self.teams_table is not None - # assert self.associations_table is not None - # assert self.my_collections_table is not None - - self.projects_table.clear(columns=True) - self.projects_table.add_columns("Status or Type", "Name", "Description", "GUID") - self.projects_table.zebra_stripes = True - self.projects_table.cursor_type = "row" - - self.communities_table.clear(columns=True) - self.communities_table.add_columns("Assignment Type", "Community Name", "Description", "GUID") - self.communities_table.zebra_stripes = True - self.communities_table.cursor_type = "row" - - self.roles_table.clear(columns=True) - self.roles_table.add_columns("Role Name", "Role Type","Description", "GUID") - self.roles_table.zebra_stripes = True - self.roles_table.cursor_type = "row" - - self.teams_table.clear(columns=True) - self.teams_table.add_columns("Assignment Type", "Team Name", "Description","GUID") - self.teams_table.zebra_stripes = True - self.teams_table.cursor_type = "row" - - self.blogs_table.clear(columns=True) - self.blogs_table.add_columns("Blog Title", "Date", "Text", "GUID") - self.blogs_table.zebra_stripes = True - self.blogs_table.cursor_type = "row" - - self.journal_table.clear(columns=True) - self.journal_table.add_columns("Journal Entry", "Date", "Text", "GUID") - self.journal_table.zebra_stripes = True - self.journal_table.cursor_type = "row" - - self.todos_table.clear(columns=True) - self.todos_table.add_columns("To-Do Name", "Activity Status", "Description", "GUID") - self.todos_table.zebra_stripes = True - self.todos_table.cursor_type = "row" - - self.user_identity_table.clear(columns=True) - self.user_identity_table.add_columns("Display Name", "User ID", "Distinguished Name", "GUID") - self.user_identity_table.zebra_stripes = True - self.user_identity_table.cursor_type = "row" - - self.associations_table.clear(columns=True) - self.associations_table.add_columns("Status or Type", "Name", "Description", "GUID") - self.associations_table.zebra_stripes = True - self.associations_table.cursor_type = "row" - - self.my_collections_table.clear(columns=True) - self.my_collections_table.add_columns("Collection Name", "Collection Description", "Collection GUID") - self.my_collections_table.zebra_stripes = True - self.my_collections_table.cursor_type = "row" - - # Populate rows - for p in self.projects if isinstance(self.projects, list) else []: - self.projects_table.add_row( - str(p.get("Project Status", "")), - str(p.get("Name", "")), - str(p.get("Description", "")), - str(p.get("GUID", "")), - ) - - for c in self.communities if isinstance(self.communities, list) else []: - self.communities_table.add_row( - str(c.get("Assignment Type", "")), - str(c.get("Name", "")), - str(c.get("Description", "")), - str(c.get("GUID", c.get("guid", ""))) - ) - - for r in self.roles if isinstance(self.roles, list) else []: - self.roles_table.add_row( - str(r.get("Name", "")), - str(r.get("Type", "")), - str(r.get("Description", "")), - str(r.get("GUID", r.get("guid", ""))), - ) - - for b in self.blogs if isinstance(self.blogs, list) else []: - self.blogs_table.add_row( - str(b.get("title", "")), - str(b.get("time", "")), - str(b.get("text", "")), - str(b.get("GUID", b.get("guid", ""))) - ) - - for j in self.journal if isinstance(self.journal, list) else []: - self.journal_table.add_row( - str(j.get("title", "")), - str(j.get("time", "")), - str(j.get("text", "")), - str(j.get("GUID", j.get("guid", ""))) - ) - - for td in self.todos if isinstance(self.todos, list) else []: - self.todos_table.add_row( - str(td.get("Name", "")), - str(td.get("Activity Status", "")), - str(td.get("Description", "")), - str(td.get("GUID", td.get("guid", ""))) - ) - - for t in self.teams if isinstance(self.teams, list) else []: - self.teams_table.add_row( - str(t.get("Assignment Type", "")), - str(t.get("Name", "")), - str(t.get("Description", "")), - str(t.get("GUID", t.get("guid", ""))), - ) - self.associations_table.add_row(f"[b u]Projects[/b u]", "", "", "") - for p in self.projects if isinstance(self.projects, list) else []: - self.associations_table.add_row( - str(p.get("Project Status", "")), - str(p.get("Name", "")), - str(p.get("Description", "")), - str(p.get("GUID", "")), - ) - self.associations_table.add_row(f"[b u]Communities[/b u]", "", "", "") - for c in self.communities if isinstance(self.communities, list) else []: - self.associations_table.add_row( - str(c.get("Assignment Type", "")), - str(c.get("Name", "")), - str(c.get("Description", "")), - str(c.get("GUID", c.get("guid", ""))) - ) - - def action_quit(self) -> Any: - # quit selected by user, so exit app - self.exit(200) - - async def action_refresh(self) -> None: - # refresh requested by User - self.log("Refreshing data...") - await self._load_or_create_profile() - await self._populate_tables() - self.log("Data refresh completed.") - - @on(OptionList.OptionSelected, "#other_function_list") - async def handle_option_selected(self, event: OptionList.OptionSelected): - # option selected by user - selected_option = event.option.prompt - selected_option_id = event.option.id - self.log(f"Selected option: {selected_option} ({selected_option_id})") - if selected_option == "Technology Types": - self.log("Fetching technology types...") - await self.fetch_technology_types() - self.log(f"Tech Type Response: {self.tech_type_response} | {self.tech_type_list}") - if self.tech_type_response == []: - self.log("No technology types found.") - self.exit(200) - elif len(self.tech_type_response) == 3 and int(self.tech_type_response) >= 400: - self.log("Error fetching technology types.") - self.exit(int(self.tech_type_response)) - self.log("Technology types fetched successfully.") - self.log("Displaying technology types...") - await self.push_screen(TechnologyTypesScreen(self.tech_type_list, - self.user_name, - self.user_password, - self.karma_points), - callback=self.tech_type_callback) - self.log("Technology types displayed successfully.") - elif selected_option == "User Identities": - await self.push_screen(UserIdentitiesScreen(self.user_name, - self.user_password, - self.karma_points, - self.user_identities), - callback=self.user_identities_callback) - elif selected_option == "Catalogs/Shop for Data": - """ Push new Screen, Show Glossaries, Digital Product Catalogs, Data Dictionaries and - Business Domains, allow the user to select from one of the 4 categories and use that selection to - display a list of available collections of the chosen type and allow the user to subscribe to them - """ - - # start by gathering the data using Pyegeria to access the Egeria backend servers - - # Glossaries - glossary_table: DataTable = DataTable(id="glossary_table") - glossary_table.add_columns("Glossary Name", "Description", "Qualified Name") - glossary_table.cursor_type = "row" - glossary_table.zebra_stripes = True - try: - self.glossary_data = exec_report_spec(format_set_name="Glossaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving glossary details: {e!s}") - self.exit(420) - return(420) - self.log(f"Glossary data returned: {self.glossary_data}") - self.glossary_data_extract = self.glossary_data.get("data") or [] - self.log(f"Glossary data extracted: {self.glossary_data_extract}") - if self.glossary_data_extract == []: - self.log(f"No glossary data found for search string: {self.selected_t_node}") - glossary_table.add_row("No glossaries found", "No data returned from Egeria", "") - else: - for g in self.glossary_data_extract: - glossary_table.add_row(g.get("Display Name"), g.get("Description"), g.get("Qualified Name")) - continue - - # Digital Product Catalogs - digital_product_catalog_table: DataTable = DataTable(id="digital_product_catalog_table") - digital_product_catalog_table.add_columns("Digital Product Catalog Name", "Description", "Qualified Name") - digital_product_catalog_table.cursor_type = "row" - digital_product_catalog_table.zebra_stripes = True - try: - self.digital_product_catalog_data = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params = {"search_string" : "*", - "metadata_element_subtypes": ["DigitalProduct", "DigitalProductFamily"]}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving digital product catalog details: {e!s}") - self.exit(421) - return(421) - self.log(f"Digital Product Catalog data returned: {self.digital_product_catalog_data}") - self.digital_product_catalog_data_extract = self.digital_product_catalog_data.get("data") or [] - self.log(f"Digital Product Catalog data extracted: {self.digital_product_catalog_data_extract}") - if self.digital_product_catalog_data_extract == []: - self.log(f"No digital product catalog data found for user: {self.user_name}") - digital_product_catalog_table.add_row("No digital product catalogs found", "No data returned from Egeria", "") - else: - for catalog_item in self.digital_product_catalog_data_extract: - digital_product_catalog_table.add_row(catalog_item["Display Name"], - catalog_item["Description"], - catalog_item["Qualified Name"]) - continue - - # Data Dictionaries - data_dictionary_table: DataTable = DataTable(id="data_dictionary_table") - data_dictionary_table.add_columns("Data Dictionary Name", "Description", "Qualified Name") - data_dictionary_table.cursor_type = "row" - data_dictionary_table.zebra_stripes = True - try: - self.data_dictionary_data = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving data dictionary details: {e!s}") - self.exit(422) - return(422) - self.data_dictionary_data_extract = self.data_dictionary_data.get("data") or [] - if self.data_dictionary_data_extract == []: - self.log(f"No data dictionary details found for user: {self.user_name}") - data_dictionary_table.add_row("No data dictionaries found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.data_dictionary_data_extract} data dictionaries for user {self.user_name}") - data_dictionary_table.add_row("Display Name", "Description", "Qualified Name") - for dictionary in self.data_dictionary_data_extract: - data_dictionary_table.add_row(dictionary.get("Display Name", ""), - dictionary.get("Description", ""), - dictionary.get("Qualified Name", "")) - continue - - # Business Domains - business_domain_table: DataTable = DataTable(id="business_domain_table") - business_domain_table.add_columns("Business Area Name", "Type Name", "GUID") - business_domain_table.cursor_type = "row" - business_domain_table.zebra_stripes = True - try: - self.business_domain_data = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(423) - return(423) - self.business_domain_data_extract = self.business_domain_data.get("data") or [] - if self.business_domain_data_extract == []: - self.log(f"No business domains found for user {self.user_name}") - business_domain_table.add_row("No business domains found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.business_domain_data_extract} business domains for user {self.user_name}") - # business_domain_table.add_row("Business Domain Name", "Description", "GUID") - for domain in self.business_domain_data_extract: - business_domain_table.add_row(domain.get("Qualified Name", ""), - domain.get("Type Name", ""), - domain.get("GUID", "")) - continue - - # Root Collections - - self.root_collection_table = DataTable(id="root_collection_table") - self.root_collection_table.add_columns("Root Collection Name", "Description", "GUID") - self.root_collection_table.cursor_type = "row" - self.root_collection_table.zebra_stripes = True - - try: - self.collections = exec_report_spec(format_set_name="BasicCollections", - output_format="DICT", - params = {"search_string": "RootCollection"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - - except PyegeriaException as e: - print_basic_exception(e) - self.collections = "Error retrieving collections: " + str(e) - self.log(f"Found {len(self.collections)} root collections for user {self.user_name}") - self.log(f"Root collections: {self.collections}") - if isinstance(self.collections, str): - self.root_collection_table.add_row("No root collections found", self.collections, "") - elif isinstance(self.collections, dict) and self.collections.get("kind") == "json": - self.collections:list[dict] = self.collections.get("data") - for collection in self.collections: - self.root_collection_table.add_row(collection.get("Qualified Name"), collection.get("Type Name"), collection.get("GUID")) - continue - elif isinstance(self.collections, list): - for collection in self.collections: - self.root_collection_table.add_row(collection.get("Qualified Name"), collection.get("Type Name"), collection.get("GUID")) - continue - else: - self.log(f"Unexpected data type returned from Egeria: {type(self.collections)}") - self.root_collection_table.add_row("Invalid data type", "Unexpected data type returned from Egeria", "") - - # hand the data to the Screen for displaying - await self.push_screen(ShopForDataScreen(glossary_table, - digital_product_catalog_table, - data_dictionary_table, - business_domain_table, - # data_specification_table, - self.root_collection_table, - self.user_name, - self.user_password, - self.view_server, - self.platform_url), - callback = self.shop_for_data_callback) - elif selected_option == "Edit Profile": - await self.push_screen(EditProfileScreen( - self.user_name, - self.user_password, - self.view_server, - self.platform_url, - self.karma_points, - self.user_profile, - self.user_GUID - ), - callback = self.edit_profile_callback) - elif selected_option == "User Bookmarks": - pass - elif selected_option == "Subscriptions": - pass - else: - pass - - async def tech_type_callback(self, result) -> int: - """ Callback for Technology Types screen - If the result is int (4xx) it indicates an error in the screen - if the result is str it contains the GUID of the selected technology type - """ - #clear local data fields - tech_type_description = "" - self.tech_type_data = {} - self.tech_type_data_extracted = {} - self.tech_type_templates = [] - self.tech_type_processes = [] - # check that we got a valid result from the screen and process accordingly - if not result or isinstance(result, int) : - self.log(f"Technology Types screen cancelled or failed; return: {result}.") - self._show_main_screen() - return (result) - self.selected_t_node = str(result) - self.log(f"Technology Types screen returned: {self.selected_t_node}") - # Request details for selected tech type - try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) - self.tech_type_data = p_client.get_tech_type_detail(filter_string=self.selected_t_node, - output_format="JSON", - # body={"maxMermaidNodeCount": self.max_mermaid_node_count, - # "graphQueryDepth": self.graph_query_depth} - ) - except PyegeriaException as e: - print_basic_exception(e) - error_category = "Technology Type Process Detail Retrieval" - error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") - await self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - self.log(f"Technology Type Data: {self.tech_type_data}, type: {type(self.tech_type_data)}") - - if "specificationMermaidGraph" in self.tech_type_data: - del self.tech_type_data["specificationMermaidGraph"] - self.log(f"Technology Type Mermaid Graph Removed") - - self.tech_type_guid = self.tech_type_data.get("technologyTypeGUID") - self.tech_type_name = self.tech_type_data.get("displayName") - self.tech_type_templates = self.tech_type_data.get("catalogTemplates") - self.tech_type_templates = self.clean_structure(self.tech_type_templates) - self.tech_type_templates = self.bools_to_strings(self.tech_type_templates) - self.log(f"Templates: {self.tech_type_templates}, type: {type(self.tech_type_templates)}") - self.tech_type_processes = self.tech_type_data.get("governanceActionProcesses") - self.tech_type_processes = self.clean_structure(self.tech_type_processes) - self.tech_type_processes = self.bools_to_strings(self.tech_type_processes) - self.log(f"Processes: {self.tech_type_processes}, type: {type(self.tech_type_processes)}") - self.tech_type_description = self.tech_type_data.get("description") - - self.log(f"Technology Type GUID: {self.tech_type_guid}") - self.log(f"Technology Type Name: {self.tech_type_name}") - self.log(f"Technology Type Description: {self.tech_type_description}") - self.log(f"Technology Type Templates: {self.tech_type_templates}" or [{"templates": "None"}]) - self.log(f"Technology Type Processes: {self.tech_type_processes}" or [{"processes": "None"}]) - - await self.push_screen(TechnologyTypeOptionsScreen(self.tech_type_guid, - self.tech_type_name, - self.tech_type_description, - self.user_name, - self.user_password, - self.karma_points, - self.tech_type_templates, - self.tech_type_processes), callback = self.tech_type_options_callback) - return(200) - - async def tech_type_options_callback(self, result: list): - self.log(f"Technology Type Options screen returned: {result}") - self.selected_t_option = None - self.selected_t_option_selected = None - - if not result: - self.log(f"Technology Type Options screen returned no input; return: {result}.") - self._show_main_screen() - return 200 - elif isinstance(result, (int, str)): - if result == "back" or result == "": - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - self._show_main_screen() - return 200 - elif isinstance(result, int) and result == 200: - self.log("Technology Type Options screen returned successfully.") - self._show_main_screen() - return 200 - elif isinstance(result, int) and result != 200: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - self.exit(415) - return 415 - else: - self.log(f"Technology Type Options screen returned unexpected result: {result}, type: {type(result)}") - self._show_main_screen() - return 200 - elif isinstance(result, (list, tuple)): - self.log(f"Input is a list: {result}, type: {type(result)}") - if len(result) == 0: - self._show_main_screen() - return 200 - self.log(f"Result 0 = {result[0]}") - if isinstance(result[0], int): - if result[0] == 200: - self.log("Technology Type Options screen returned successfully.") - self._show_main_screen() - return result[0] - elif result[0] == "back": - self._show_main_screen() - return 200 - elif len(result) >= 2: - self.selected_t_option = str(result[0]) - self.selected_t_option_selected = str(result[1]) - self.log(f"Technology Type Options screen returned: {self.selected_t_option} | {self.selected_t_option_selected}") - else: - self.selected_t_option = str(result[0]) - self.selected_t_option_selected = "" - else: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, type: {type(result)}.") - self._show_main_screen() - return 200 - - # display the screen so the objects we need to mount to are created in the DOM - # then we can build the display elements for the placeholder properties - # and mount them in the appropriate containers on the screen - - self.log(f"Selected: {self.selected_t_option} | {self.selected_t_option_selected}") - if self.selected_t_option == "template": - await self.push_screen(TechnologyTypeTemplatesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_templates), - callback=self.tech_type_templates_callback) - elif self.selected_t_option == "process": - await self.push_screen(TechnologyTypeProcessesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_processes), - callback=self.tech_type_processes_callback) - else: - self.log(f"Technology Type Options screen returned invalid option: {self.selected_t_option_selected}") - self.exit(416) - return(200) - - async def fetch_technology_types(self) -> int: - self.tech_type_list: list = [{}] - try: - self.autoc = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - # retrieve the tech type data - self.log(f"Fetching technology type hierarchy for tech_type='*'") - self.tech_type_response = await self.autoc._async_get_tech_type_hierarchy(filter_string = "*" ) - except Exception as e: - self.log(f"Exception in get_tech_type_hierarchy: {e}") - self.log(print_basic_exception(e)) - self.tech_type_list = [{}] - self.exit(416) - return(416) - - self.log (f"tech_type_response: {self.tech_type_response}") - # Copy the data into a working variable for the extraction routine - self.tech_type_list = self.tech_type_response - return(200) - - def unpack_egeria_data(self) -> int: - """ Unpack the data returned from Egeria """ - output_data: list[dict] = [{}] - output_data.clear() - if isinstance(self.tech_type_data, dict): - if "data" in self.tech_type_data: - output_data = self.tech_type_data.get("data") - else: - output_data = [self.tech_type_data] - elif isinstance(self.tech_type_data, list): - for entry in self.tech_type_data: - if isinstance(entry, dict): - output_data = entry.get("data") - elif isinstance(entry, list): - for subentry in entry: - if isinstance(subentry, dict): - output_data = [subentry] - else: - self.log(f"error unknown data structure for Tech Type data") - return(417) - else: - self.log(f"error unknown outer data structure for Tech Type data") - return (417) - else: - self.log(f"Tech Type data not dict or list") - return(417) - - self.log(f"output_data: {output_data}, {type(output_data)}") - # return the extracted data (dict) - self.tech_type_list = output_data - return (200) - - def tech_type_templates_callback(self, result): - """ Callback for Technology Type Templates screen - result contains up to 3 elements: - [0] = return code or 'input', [1] input data, [2] full template""" - # take the input data and use it in the form and - # create from template - self.log(f"Technology Type Templates screen returned: {result}") - # Check for return code - if isinstance(result, int): - self.log(f"Technology Type Templates screen returned: {result}, exiting.") - return result - # Check for unexpected return - if not result or not isinstance(result, (list, tuple)) or len(result) < 3 or result[0] != "input": - self.log(f"Technology Type Templates screen cancelled/failed; return: {result}, exiting.") - return 418 - - # make a working version of the full template if it is returned - if isinstance(result[2], dict): - self.full_template = result[2] - else: - self.full_template = {} - # create an empty dict for the returned data - my_placeholderPropertyValues: dict = {} - # load in the returned placeholder user data - if isinstance(result[1], dict): - self.placeholder_input = result[1] - else: - self.placeholder_input = {} - # clean up the keys of the user data to match the template keys - if isinstance(self.placeholder_input, dict): - # for each data input item in the dict - for input_item, input_value in self.placeholder_input.items(): - self.log(f"input_item: {input_item}, input_value: {input_value}") - input_fix1 = input_item.replace("_", " ") - self.log(f"input_item after underscore removal: {input_fix1}") - input_fix2 = input_fix1.replace(" placeholder input", "") - self.log(f"input_item after placeholder removal: {input_fix2}") - - self.log(f"fixed input_item: {input_fix2}, {input_value}") - # build the placeholderPropertyValues dict - my_placeholderPropertyValues[input_fix2] = input_value - else: - self.log(f"placeholder_input is not a dict: {self.placeholder_input}") - return 419 - - # create a dict structure that matches the template structure - # we have the input data in my_placeholderPropertyValues and - # the selected template in self.full_template - # so we can use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - self.log(f"my_placeholderPropertyValues: {my_placeholderPropertyValues}") - self.log(f"self.placeholder_input: {self.placeholder_input}") - self.log(f"self.full_template: {self.full_template}") - - # copy the retrieved template into the request body - request_body: dict = { - "class": "TemplateRequestBody", - "externalSourceGUID": self.full_template.get("externalSourceGUID") or "", - "externalSourceName": self.full_template.get("externalSourceName") or "", - "typeName": self.full_template.get("typeName") or "", - "templateGUID": self.full_template.get("Catalog Template GUID"), - "anchorGUID": self.full_template.get("anchorGUID"), - "isOwnAnchor": "false", - "effectiveFrom": "2026-01-01", - "effectiveTo": "2030-12-31", - "replacementProperties": self.full_template.get("replacementProperties") or {}, - "placeholderPropertyValues": {}, - "parentGUID": None, - "parentRelationshipTypeName": None, - "parentRelationshipProperties": None, - "parentAtEnd1": self.full_template.get("parentAtEnd1") or True, - "effectiveTime": self.full_template.get("effectiveTime") or datetime.datetime.now().isoformat(), - } - self.log(f"request_body: {request_body}") - # update the request body with the user provided input - for key, value in my_placeholderPropertyValues.items(): - request_body["placeholderPropertyValues"][key] = value - self.log(f"request_body after update: {request_body}") - # now instantiate and call the function to create the element from the template - try: - tokendata = self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - my_md_instance = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password, tokendata) - new_guid = my_md_instance.initiate_gov_action_process(body=request_body) - except Exception as e: - self.log(f"Exception in create_element_from_template: {e}") - if isinstance(e, PyegeriaException): - self.log(print_basic_exception(e)) - else: - self.log(f"Exception in create_element_from_template: {e}") - self.push_screen(StatusScreen(f"Error creating element from Template\n{print_basic_exception(e)}"), callback=self.status_callback) - return 420 - - self.log(f"new_guid: {new_guid}") - self.push_screen(StatusScreen(f"Element created from Template/Metadata element created with GUID: '{new_guid}'"), callback=self.status_callback) - - def status_callback(self, status_callback_rc) -> None: - """ Callback routine from the status screen, - this screen is typically displayed at the end of a process - to indicate success or failure of the process.""" - - self.log(f"Status screen returned: {status_callback_rc}") - self.exit(status_callback_rc) - - def tech_type_processes_callback(self, result): - """ Callback for Technology Type Processes screen""" - # take the input data and use it to get the template for running the process, display it and allow - # the user to complete the data in the placeholders in order to run the process - self.log(f"Technology Type Processes screen returned: {result}") - if isinstance(result, int): - self.log(f"Technology Type Processes Screen returned: {result}, exiting.") - return result - - if not result or not isinstance(result, (list, tuple)) or len(result) < 3 or result[0] != "input": - self.log(f"Technology Type Processes screen cancelled or failed; return: {result}, exiting.") - error_category = "Governance Action Process" - error_message = f"Unknown response from TechnologyTypeProcessesScreen: {result}" - self.log(f"Error retrieving process details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return 418 - - self.type = result[0] - self.input_data = result[1] - self.full_process = result[2] - self.result = result - self.log(f"Received process type: {self.type}, data: {self.input_data}, full process: {self.full_process}") - - my_process_property_values: list[dict] = [] - if isinstance(self.input_data, dict): - for input_key, input_value in self.input_data.items(): - self.log(f"Processing input value: {input_key} with value: {input_value}") - input_fix1 = input_key.replace("_", " ") - input_fix2 = input_fix1.replace(" placeholder input", "").replace(" process input", "") - self.log(f"Process input key after cleaning: {input_fix2}") - my_process_property_values.append({ - "class": "SupportedRequestParameter", - "specificationPropertyType": "SUPPORTED_REQUEST_PARAMETER", - "name": input_fix2, - "value": input_value, - "Description": "", - "dataType": "string", - "example": "", - "required": "False" - }) - else: - self.log("No process Request Parameter input data returned, this is possibly valid, but unlikely") - self.log(f" Input data type: {type(self.input_data)}, data: {self.input_data}") - self.log("Processing will proceed with no Supported Request Parameters for the process") - - self.log(f"Process property values: {my_process_property_values}") - self.log(f"Original input: {self.result}") - self.log(f"full process: {self.full_process}") - - additional_properties = self.full_process.get("additionalProperties", {}) if isinstance(self.full_process, dict) else {} - template_guid = additional_properties.get("templateGUID") if isinstance(additional_properties, dict) else None - - request_body: dict = { - "displayName": self.full_process.get("displayName") if isinstance(self.full_process, dict) else None, - "description": self.full_process.get("description") if isinstance(self.full_process, dict) else None, - "additionalProperties": { - "templateGUID": template_guid - }, - "specification": { - "supportedRequestParameter": my_process_property_values - } - } - - def tech_type_processes_details(self, tech_type, selected_t_option_selected): - """ Retrieve process data for Technology Type Processes screen""" - # ************************************************************************************************** - # take the input data and use it to get the template for running the process, display it and allow - # the user to complete the data in the placeholders in order to run the process - # with the change to retrieve the JSON this specific processing is no longer needed - # ************************************************************************************************** - self.type = None - self.full_process = None - self.selected_q_name = None - required_process = None - self.type = str(tech_type) - self.selected_q_name = str(selected_t_option_selected) - self.log(f"Received process type: {self.type}, selected: {self.selected_q_name}") - - if self.selected_q_name != None: - self.log(f"Selected qualified name: {self.selected_q_name}") - try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) - governance_actions = p_client.get_tech_type_detail(filter_string=self.selected_q_name, output_format="JSON") - except PyegeriaException as e: - print_basic_exception(e) - error_category = "Technology Type Process Detail Retrieval" - error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - self.log(f"Egeria returned: {governance_actions}, type: {type(governance_actions)}") - # load Json data - if isinstance(governance_actions, str): - if governance_actions != "No elements found": - technology_response = json.loads(governance_actions) - else: - technology_response = None - else: - technology_response = governance_actions - - self.log(f"Tech type process data retrieved: {technology_response}") - self.governance_action_processes = technology_response.get("governanceActionProcesses", []) - self.log(f"Retrieved {len(self.governance_action_processes)} governance action processes for {self.selected_q_name}") - self.log(f"Data: {self.governance_action_processes}") - for process in self.governance_action_processes: - self.log(f"Process: {process['name']}, Description: {process['description']}") - if process['name'] == self.selected_q_name: - self.log(f"Found requested process: {process['description']}") - # temporary end of function, more code needed here. - required_process = process - break - else: - continue - - return(required_process) - else: - self.log(f"selected_q_name in process_details function is None, returnng None to caller: {self.selected_q_name}") - return(None) - - async def shop_for_data_callback(self, result): - """ Callback for Shop For Data screen""" - self.log(f"Shop For Data screen returned: {result}, type: {type(result)}.") - if not result: - self.log("Shop For Data screen returned empty result.") - self._show_main_screen() - return 200 - - if isinstance(result, int): - selection_type = result - selection_parm_1 = None - selection_parm_2 = None - selection_parm_3 = None - elif isinstance(result, (list, tuple)): - selection_type = result[0] - selection_parm_1 = result[1] if len(result) > 1 else None - selection_parm_2 = result[2] if len(result) > 2 else None - selection_parm_3 = result[3] if len(result) > 3 else None - else: - self.log(f"Unexpected result type from Shop For Data screen: {type(result)}") - self._show_main_screen() - return 418 - - if isinstance(selection_type, int): - if selection_type == 200: - self.log("Shop For Data screen returned successfully.") - self._show_main_screen() - return 200 - elif selection_type == 201: - self.log(f"Shop For Data screen returned: {selection_type}, request to search for a term ") - await self.push_screen(SearchForTermScreen(self.user_name, - self.user_password, - self.view_server, - self.platform_url), callback=self.search_for_term_callback) - return 200 - elif selection_type == 211: - self.log(f"Shop For Data screen returned: {selection_type}, request to subscribe to data source ") - return 211 - elif selection_type == 212: - self.log(f"Shop For Data screen returned: {selection_type}, request to sample data source ") - self.request_to_sample_data_source(selection_parm_1, selection_parm_2, selection_parm_3) - return 200 - else: - self.log(f"Shop For Data screen returned: {selection_type}.") - self._show_main_screen() - return selection_type - - if selection_type == "dictionary": - # data dictionary - self.log(f"Selected dictionary with qualified name: {selection_parm_1}") - self.build_dictionary_details(selection_parm_1, selection_parm_2) - elif selection_type == "domain": - # business domain - self.log(f"Selected business domain with qualified name: {selection_parm_1}") - self.build_domain_details(selection_parm_1, selection_parm_2) - elif selection_type == "catalog": - # data product catalog - self.log(f"Selected catalog with qualified name: {selection_parm_1}") - self.build_catalog_details(selection_parm_1, selection_parm_2) - elif selection_type == "glossary": - # glossary - self.log(f"Selected glossary with qualified name: {selection_parm_2}") - self.build_glossary_details(selection_parm_1, selection_parm_2) - elif selection_type == "collection": - # root collection - self.log(f"Selected Root Collection with qualified name: {selection_parm_1}") - self.build_root_collection_details(selection_parm_1, selection_parm_2) - else: - self.log(f"Unknown selection type: {selection_type}") - self.exit(429) - - def build_dictionary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a dictionary details screen""" - self.log(f"Building dictionary details for qualified name: {target_qualified_name}") - self.dictionary_qualified_name = target_qualified_name - self.dictionary_display_name = target_display_name - build_structure = {} - - try: - self.dictionary_details = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params={"search_string": self.dictionary_qualified_name, "filter_string": self.dictionary_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving dictionary details: {e!s}") - self.exit(420) - return (420) - self.log(f"Dictionary Details: {self.dictionary_details}") - if not self.dictionary_details or self.dictionary_details == None: - error_category = "Dictionary Details" - error_message = "No dictionary details found" - self.log(f"Error retrieving dictionary details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.dictionary_details.get("kind") == "empty": - dictionary_tree: Tree = Tree(label="Empty Dictionary", id="data_dictionary_tree") - dictionary_tree.root.expand() - dictionary_tree.root.content = "No dictionary terms found for this dictionary" - else: - dictionary_tree: Tree = Tree(label=self.dictionary_display_name, id="data_dictionary_tree") - dictionary_tree.root.expand() - dictionary_tree.auto_expand = True - self.dictionary_details_data = self.dictionary_details.get("data") - for term in self.dictionary_details_data: - self.log(f"Dictionary term: {term} being processed") - term_qualified_name = term.get("Qualified Name") or "" - term_subject = term.get("Subject Area") or "" - term_summary = term.get("Summary") or "" - # create dict structure for loading the tree - if term_subject not in build_structure: - build_structure[term_subject] = [] - build_structure[term_subject].append({term_qualified_name: term_summary}) - continue - # Once the structure is complete we can build the tree from it - for term_subject, terms in build_structure.items(): - self.log(f"Building tree for subject: {term_subject}, term: {terms}") - dictionary_branch = dictionary_tree.root.add(term_subject) - dictionary_branch.expand() - for term_dict in terms: - self.log(f"Adding term to tree: {term_dict}") - for term_qualified_name, term_summary in term_dict.items(): - dictionary_branch.add_leaf(term_summary, data=term_qualified_name) - dictionary_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("dictionary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=dictionary_tree), callback=self.overview_callback) - - def build_domain_details(self, target_qualified_name, target_type__name): - """ Build the details object for a business domain details screen""" - self.log(f"Building domain details for qualified name: {target_qualified_name}") - self.domain_qualified_name = target_qualified_name - self.domain_type__name = target_type__name - build_structure = {} - - try: - self.domain_details = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params={"search_string": self.domain_qualified_name, "filter_string": self.domain_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(420) - return (420) - self.log(f"domain_details: {self.domain_details}") - if not self.domain_details or self.domain_details == None: - error_category = "Business Domain Details" - error_message = "No domain details found" - self.log(f"Error retrieving business domain details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.domain_details.get("kind") == "empty": - domain_tree: Tree = Tree(label="Empty Business Domain", id="business_domain_tree") - domain_tree.root.expand() - domain_tree.root.content = "No domain details found for this business domain" - else: - self.domain_details_data = self.domain_details.get("data") - self.log(f"domain_details_data: {self.domain_details_data}") - self.domain_display_name = self.domain_details_data.get("Qualified Name") - domain_tree: Tree = Tree(label=self.domain_display_name, id="business_domain_tree") - domain_tree.root.expand() - domain_tree.auto_expand = True - for term in self.domain_details_data: - if term == None: - continue - term_qualified_name = term.get("Qualified Name") or "" - term_type = term.get("Type Name") or "" - term_GUID = term.get("GUID") or "" - term_members = term.get("Containing Members") - term_memberof = term.get("Member Of") - # create dict structure for loading the tree - build_structure[term_qualified_name] = { - "term_type": term_type, - "term_GUID": term_GUID, - "term_members": term_members, - "term_memberof": term_memberof - } - continue - # Once the structure is complete we can build the tree from it - for qualified_name, details in build_structure.items(): - domain_branch = domain_tree.root.add(qualified_name, id=qualified_name, data=[details["term_type"], details["term_GUID"]]) - if details["term_members"] != None: - domain_branch_members = domain_branch.add("Containing Members") - for member in details["term_members"]: - domain_branch_members.add_leaf(member) - if details["term_memberof"] != None: - for member in details["term_memberof"]: - domain_branch.add_leaf(member) - domain_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("domain", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=domain_tree), callback=self.overview_callback) - - def build_catalog_details(self, target_qualified_name, target_display_name): - """ Build the details object for a product catalog details screen""" - self.log(f"Building product catalog details for qualified name: {target_qualified_name}") - self.catalog_qualified_name = target_qualified_name - self.catalog_display_name = target_display_name - build_structure = {} - - try: - self.catalog_details = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params={"search_string": self.catalog_qualified_name, "filter_string": self.catalog_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving catalog details: {e!s}") - self.exit(420) - return (420) - self.log(f"catalog_details: {self.catalog_details}") - if not self.catalog_details or self.catalog_details == None: - error_category = "Catalog Details" - error_message = "No catalog details found" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.catalog_details.get("kind") == "empty": - catalog_tree: Tree = Tree(label="Empty Catalog", id="digital_product_catalog_tree") - catalog_tree.root.expand() - catalog_tree.root.content = "No catalog terms found for this catalog" - else: - catalog_tree: Tree = Tree(label=self.catalog_display_name, id="digital_product_catalog_tree") - catalog_tree.root.expand() - catalog_tree.auto_expand = True - self.catalog_details_data: list[dict] = self.catalog_details.get("data") - self.log(f"catalog_details_data: {self.catalog_details_data}") - if not self.catalog_details_data or self.catalog_details_data == None: - error_category = "Catalog Details" - error_message = "No catalog details found or the data dict entry is missing" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - for product in self.catalog_details_data: - self.log(f"product: {product}") - term_qualified_name = product.get("Qualified Name") or "" - term_subject = product.get("Display Name") or "" - term_summary = product.get("Description") or "" - # create dict structure for loading the tree - if term_subject not in build_structure: - build_structure[term_subject] = [] - build_structure[term_subject].append({term_qualified_name: term_summary}) - self.log(f"build_structure: {build_structure}") - continue - # Once the structure is complete we can build the tree from it - sample_data: list = [] - for instance, data_prods in build_structure.items(): - catalog_branch = catalog_tree.root.add(instance) - for data_prod in data_prods: - for term_qualified_name, term_summary in data_prod.items(): - catalog_branch.add_leaf(term_summary, data=term_qualified_name) - self.log(f"term_qualified_name: {term_qualified_name}, term summary: {term_summary}") - catalog_tree.root.expand() - - # get some sample data from the data source for each product - for product in self.catalog_details_data: - collection_memberships = product.get("Member Of") - if collection_memberships: - collection_membership_list = collection_memberships.split(",") - self.log(f"Membership List for product {product.get('Display Name')}: {collection_membership_list}") - for membership_qname in collection_membership_list: - self.log(f"Processing collection membership: {membership_qname}") - if "DigitalProduct" in membership_qname: - self.log(f"Processing dataset: {membership_qname}") - try: - # create client instance and retrieve data - dclient = Egeria(self.view_server, self.platform_url, self.user_name, - self.user_password) - token = dclient.create_egeria_bearer_token(self.user_name, self.user_password) - data_set_metadata = dclient.find_tabular_data_sets( - search_string=membership_qname, - start_from=0, - page_size=1, - output_format="DICT" - ) - self.log(f"Dataset metadata retrieved: {data_set_metadata}") - if isinstance(data_set_metadata, list) and len(data_set_metadata) > 0 and data_set_metadata != "No elements found": - data_set_guid = data_set_metadata[0].get("GUID") - if data_set_guid: - data_set_data = dclient.get_tabular_data_set( - tabular_data_set_guid=data_set_guid, - start_from_row=0, - max_row_count=10, - output_format="MD" - ) - self.log(f"Dataset data retrieved: {data_set_data}") - sample_data.append(data_set_data) - else: - sample_data.append(f"No GUID found for dataset {membership_qname}") - else: - sample_data.append(f"No metadata found for dataset {membership_qname}") - continue - except PyegeriaException as e: - self.log(f"Error retrieving dataset data: {e}") - print_basic_exception(e) - else: - continue - self.log(f"Sample data after product {product.get('Display Name')}: {sample_data}") - self.log(f"Final sample data length: {len(sample_data)}") - - self.push_screen(SelectionOverviewScreen("catalog", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=catalog_tree, - data_samples=sample_data), callback=self.overview_callback) - - def build_glossary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a glossary details screen""" - self.log(f"Building glossary details for qualified name: {target_qualified_name}") - self.glossary_qualified_name = target_qualified_name - self.glossary_display_name = target_display_name - - for glossary_instance in self.glossary_data_extract: - if glossary_instance.get("Qualified Name") == target_qualified_name: - self.glossary_folders = glossary_instance.get("Folders") or None - self.log(f"glossary_folders: {self.glossary_folders}") - category = "Uncategorised" - if self.glossary_folders != None: - glossary_tree: Tree = Tree(label=self.glossary_display_name, id="glossary_details_tree") - glossary_tree.root.expand() - glossary_tree.auto_expand = True - - folder_entries = [f.strip() for f in self.glossary_folders.split(',')] - nodes = {(): glossary_tree.root} - prefixes = ["GlossaryCategory", "GlossaryTerm", "CollectionFolder"] - - for entry in folder_entries: - parts = entry.split('::') - if len(parts) == 1: - parts = entry.split('/') - - is_leaf = False - path_parts = [] - full_id = "" - if any(parts[0].startswith(p) for p in prefixes): - type_prefix = parts[0] - path_parts = parts[1:] - is_leaf = "Term" in type_prefix - if is_leaf: - full_id = path_parts[-1] - else: - path_parts = parts - - current_path = () - for i, part in enumerate(path_parts): - parent_path = current_path - current_path = current_path + (part,) - - if current_path not in nodes: - parent_node = nodes[parent_path] - if is_leaf and i == len(path_parts) - 1: - nodes[current_path] = parent_node.add_leaf(part, data=full_id) - else: - new_node = parent_node.add(part, data=part) - new_node.expand() - nodes[current_path] = new_node - - glossary_tree.refresh() - else: - self.log(f"No glossary folders found in the glossary data extract") - folder_category = "Empty Glossary" - category = glossary_tree.root.add(folder_category) - folder_term = "No glossary terms found for this glossary" - category.add_leaf(folder_term) - - self.push_screen(SelectionOverviewScreen("glossary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=glossary_tree), callback=self.overview_callback) - - def build_root_collection_details(self, target_qualified_name, target_display_name): - """ Build the details object for a root collection details screen""" - member_tree: Tree = Tree(label="Root Collection", id="root_collection_members_tree") - member_tree.root.expand() - member_tree.auto_expand = True - - self.log(f"Building root collection details for qualified name: {target_qualified_name}") - self.root_collection_qualified_name = target_qualified_name - collection_branch = member_tree.root.add(self.root_collection_qualified_name, expand=True) - self.log(f"self_collections: {self.collections}") - collection = self.collections[0] - self.log(f"collection: {collection}") - if target_qualified_name == collection.get("Qualified Name"): - root_collection_contains: str = collection.get("Containing Members") or None - self.log(f"root_collection_contains: {root_collection_contains}") - folders = str.split(root_collection_contains, ', ') - for folder in folders: - collection_branch.add_leaf(folder) - continue - - self.push_screen(SelectionOverviewScreen("collection", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=member_tree), callback=self.overview_callback) - - - - def overview_callback(self, r_code): - """Callback function for handling overview screen actions.""" - if isinstance(r_code, list): - self.selected_item = r_code[1] - self.selected_tree = r_code[2] - r_code: int = r_code[0] - if r_code == 410: - error_category = "Collection Category" - error_message = "Unknown collection category returned" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 411: - error_category = "Glossary" - error_message = "query_one no matches found for glossary tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 412: - error_category = "Digital Product Catalog" - error_message = "query_one no matches found for digital product catalog tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 413: - error_category = "Data Dictionary" - error_message = "query_one no matches found for data dictionary tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 414: - error_category = "Business Domain" - error_message = "query_one no matches fround for business domain tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - elif r_code == 415: - error_category = "Root Collections" - error_message = "query_one no matches found for root collections tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - else: - # good return from overview screen - self.log (f"Overview screen callback, return code : {r_code}") - if r_code == 211: - # Subscribe to selected item - self.log(f"Subscribing to selected item: {self.selected_item} from {self.selected_tree}") - try: - s_client = ProductManager(self.view_server, self.platform_url, self.user_name, self.user_password) - s_client.create_egeria_bearer_token(self.user_name, self.user_password) - s_client.create_digital_subscription(self.selected_item) - except (PyegeriaException): - self.log(f"Error creating digital subscription: {self.selected_item} from {self.selected_tree}") - self.notify("Error creating digital subscription") - # more error handling required here - """ Request Body layout, not all fields are required to be completed - { - "class" : "NewAgreementRequestBody", - "isOwnAnchor" : true, - "anchorScopeGUID" : "", - "parentGUID" : "", - "parentRelationshipTypeName" : "CollectionMembership", - "parentAtEnd1": true, - "properties": { - "class" : "DigitalSubscriptionProperties", - "qualifiedName": "DigitalSubscription::", - "displayName" : "display name", - "description" : "Add description of the subscription here", - "userDefinedStatus" : "DRAFT", - "identifier" : "Add subscription identifier here", - "supportLevel" : "Add the level of support agreed/requested", - "serviceLevels" : { - "property1Name" : "property1Value", - "property2Name" : "property2Value" - }, - "additionalProperties": { - "property1Name" : "property1Value", - "property2Name" : "property2Value" - } - }, - "initialStatus" : "OTHER", - "externalSourceGUID": "add guid here", - "externalSourceName": "add qualified name here", - "effectiveTime" : "{{$isoTimestamp}}", - "forLineage" : false, - "forDuplicateProcessing" : false - }""" - self.push_screen(CreateSubscriptionRequestScreen(), callback=self.create_subscription_callback) - else: - self.push_screen(ShopForDataScreen(self.view_server, - self.platform_url, - self.user_name, self.user_password, - self.selected_item, - self.selected_tree), - callback=self.shop_for_data_callback) - - def create_subscription_callback(self, result): - """ Callback routine for create subscription request screen - This routine checks for valid input and then processes the subscription request """ - if result is None: - self.log("User cancelled subscription creation") - return - self.log(f"Subscription created: {result}") - - def extract_glossary_terms(self, text): - """ - Extracts GlossaryTerm items from a string structure. - Pattern: Starts with 'GlossaryTerm::', ends at the next ','. - """ - # Pattern explanation: - # GlossaryTerm:: - Matches the literal prefix - # ([^,\']+) - Captures one or more characters that are NOT a comma or a single quote - pattern = r"GlossaryTerm::([^,\']+)" - - # Find all matches in the text - matches = re.findall(pattern, text) - - self.log(f"Extracted {len(matches)} GlossaryTerm items from text") - - # Clean up whitespace and return as a list - return [match.strip() for match in matches] - - def display_glossary_term_details(self, term): - """ Displays the details of a GlossaryTerm item""" - self.target_term = term - try: - self.term_details = exec_report_spec(format_set_name="Glossary-Terms", - output_format="JSON", - params={"search_string": self.target_term, "filter_string": self.target_term}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving term details: {e!s}") - self.exit(440) - return (440) - - self.log(f"term_details: {self.term_details}") - if not self.term_details or self.term_details == None: - error_category = "Glossary Term Details" - error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return 440 - elif self.term_details.get("kind") == "empty": - self.log(f"No glossary term details found for qualified name: {self.target_term}") - error_category = "Glossary Term Details" - error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return 440 - - self.term_details_data = self.term_details.get("data") - - if not self.term_details_data or self.term_details_data == None: - error_category = "Glossary Term Details" - error_message = "No glossary term details found or the data dict entry is missing" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return 440 - else: - self.term_details_container: ScrollableContainer = self.screen.query_one(DataTable) - self.term_details_container.mount() - for data_item_key, data_item_value in self.term_details_data.items(): - Static(f"Field: {data_item_key}, Value: {data_item_value}").mount(self.term_details_container) - # Once all the fields are complete we can leave it to the screen processing to handle the rest - return (200) - - @on(DataTable.RowSelected, "#roles_table") - def handle_roles_table_row_selection(self, event): - role_table = event.data_table - selected_row_key = event.row_key - selected_row_data = role_table.get_row(selected_row_key) - selected_role_description = selected_row_data[2] - selected_role_name = selected_row_data[0] - selected_role_type = selected_row_data[1] - selected_role_guid = selected_row_data[3] - self.log(f"Selected role: {selected_row_data}") - team_members_list: list = [] - self.team_members = [] - - # provide list of team members for team leader or team member - if ("TeamLeader" in selected_role_name or "TeamLeader" in selected_role_type or - "TeamMember" in selected_role_name or "TeamMember" in selected_role_type): - self.log(f"Selected role is a TeamLeader or TeamMember: {selected_role_name}") - result = self.find_team_members(selected_role_name) - if not isinstance(result, (tuple, list)) or len(result) != 5: - return - team_members_list, team_display_name, team_qualified_name, team_category, team_description = result - else: - self.log(f"Selected role is not a TeamLeader or TeamMember: {selected_role_name}") - return (201) - - self.log(f"team_members_list: {team_members_list}") - # Process team data for display on screen - team_member_properties: list = [] - team_properties: list = [] - team_properties.append(team_display_name) - team_properties.append(team_qualified_name) - team_properties.append(team_category) - team_properties.append(team_description) - self.log(f"team_properties: {team_properties}") - # Process team member data for display on screen - for team_member in team_members_list: - if isinstance(team_member, list) and "Selection Error" in team_member[0]: - self.log(f"Team member details contain 'Selection Error': {team_member}") - self.team_members.append(team_member) - break - else: - self.log(f"Processing team member properties: {team_member}") - team_member_properties = [] - team_member_properties.append(team_member.get("Individual")) - team_member_properties.append(team_member.get("Assignment Type")) - team_member_properties.append(team_member.get("Individual GUID")) - self.log(f"team_member_properties: {team_member_properties}") - self.team_members.append(team_member_properties) - self.log(f"team_members: {self.team_members}") - continue - self.log(f"team_members: {self.team_members}") - self.log(f"team_properties: {team_properties}") - self.log(f"User name: {self.user_name}") - self.push_screen(MyTeam(self.team_members, team_properties, self.user_name), callback=self.my_team_callback) - - def find_team_members(self, role_name): - # common routine for finding team members given a role name - # extract the Department::nnn from the name to use as the search key - selected_role_name = role_name - team_members_list: list = [] - self.team_members = [] - - search_key_parts = selected_role_name.split("::") - role_search_key = '::'.join(search_key_parts[1:]) - self.log(f"Role search key: {role_search_key}") - # get the team member details for that department - team_member_data: dict = exec_report_spec(format_set_name="Team-Members", - output_format="DICT", - params={"search_string": role_search_key}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - self.log(f"team_member_data: {team_member_data}") - # team members? - if team_member_data.get("kind") != "empty": - team_members_data_struct = team_member_data.get("data") or [] - self.log(f"team members data extracted: {team_members_data_struct}") - else: - self.log(f"No team members found for role: {role_search_key}") - error_category = "Team Members" - error_message = "No team members found" - self.log(f"Error retrieving team members: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return ([], None, None, None, None) - team_members_list.clear() - team_display_name: str = "" - team_qualified_name: str = "" - team_category: str = "" - team_description: str = "" - for team_member in team_members_data_struct: - # get team details - team_display_name = team_member.get("Display Name", "") - team_qualified_name = team_member.get("Qualified Name", "") - team_category = team_member.get("Category", "") - team_description = team_member.get("Description", "") - # extract the team member entries - team_member_structure: list[dict] = team_member.get("Members", []) - - if team_member_structure is not None: - for entry in team_member_structure: - team_members_list.append(entry) - - if not team_members_list and not team_display_name: - error_category = "Team Member Details" - error_message = "No team member details found" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return ([], None, None, None, None) - - return [team_members_list, team_display_name, team_qualified_name, team_category, team_description] - - def my_team_callback(self, status) -> None: - self.log(f"Callback received with status: {status}") - if status == 200: - self.log("MyTeam screen completed successfully") - else: - self.log(f"Error in MyTeam screen: {status}") - self._show_main_screen() - - def search_for_term_callback(self, status) -> None: - self.log(f"Callback received with status: {status}") - if status == 200: - self.log("Search for term screen completed successfully") - self._show_main_screen() - elif status == 201: - glossary_table = self.query_one("g#glossary_table", DataTable) - digital_product_catalog_table = self.query_one("d#digital_product_catalog_table", DataTable) - data_dictionary_table = self.query_one("d#data_dictionary_table", DataTable) - business_domain_table = self.query_one("b#business_domain_table", DataTable) - data_specification_table = self.query_one("d#data_specification_table", DataTable) - self.log("No matches found for search term") - self.push_screen(ShopForDataScreen(glossary_table, - digital_product_catalog_table, - data_dictionary_table, - business_domain_table, - data_specification_table, - self.user_name, - self.user_password, - self.view_server, - self.platform_url), - callback=self.shop_for_data_callback) - - else: - self.log(f"Error in Search for term screen: {status}") - error_category = "Search for Term" - error_message = "Error retrieving Term details" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - def request_to_sample_data_source(self, selection_parm_1, selection_parm_2, selection_parm_3): - """ The user has requested to see a sample of the data from the selected data source """ - self.row_highlighted = selection_parm_1 - self.cursor_row_highlighted = selection_parm_2 - self.data_table_highlighted = selection_parm_3 - self.log(f"Data selected: row={selection_parm_1}, cursor_row={selection_parm_2}, data_table={selection_parm_3}") - selected_item = self.query_one("#"+self.data_table_highlighted, DataTable) - item_content = selected_item.get_row(self.row_highlighted) - - - def truncate_at_sequence(self, data, target="specificationMermaidGraph"): - """ - Recursively traverses data. Truncates strings containing the target - and drops all following sibling items within lists or dictionaries. - Returns (truncated_data, should_terminate_parent_container) - """ - # Handle Strings - if isinstance(data, str): - if target in data: - # Drop the sequence and everything after it - truncated_str = data.split(target)[0] - return truncated_str, True - return data, False - - # Handle Dictionaries - elif isinstance(data, dict): - new_dict = {} - for key, value in data.items(): - # First, check if the key itself contains the target - if isinstance(key, str) and target in key: - truncated_key = key.split(target)[0] - if truncated_key: # Keep key fragment if not empty - new_dict[truncated_key] = value - return new_dict, True - - # Recursively check the value - updated_value, terminate = self.truncate_at_sequence(value, target) - new_dict[key] = updated_value - if terminate: - return new_dict, True - return new_dict, False - - # Handle Lists and Tuples - elif isinstance(data, (list, tuple)): - new_list = [] - for item in data: - updated_item, terminate = self.truncate_at_sequence(item, target) - new_list.append(updated_item) - if terminate: - break - # Maintain original type integrity - return (tuple(new_list) if isinstance(data, tuple) else new_list), terminate - - # Handle Booleans, Numbers, None - return data, False - - # Helper wrapper for clean execution - def clean_structure(self, data, target="specificationMermaidGraph"): - result, _ = self.truncate_at_sequence(data, target) - return result - - def bools_to_strings(self, data): - """ - Recursively traverses data structures and converts - all Boolean values into their corresponding string format. - """ - # Check explicitly for bool first (since bool is a subclass of int in Python) - if isinstance(data, bool): - return str(data) - - # Handle Dictionaries - elif isinstance(data, dict): - return {key: self.bools_to_strings(value) for key, value in data.items()} - - # Handle Lists and Tuples - elif isinstance(data, (list, tuple)): - converted = [self.bools_to_strings(item) for item in data] - return tuple(converted) if isinstance(data, tuple) else converted - - # Return all other types unchanged (ints, floats, strings, None) - return data - - def user_identities_callback(self): - """ Callback roiutine for the user identities screen - the user has requested to exit the screen and so will will - push the main screen again""" - self._show_main_screen() - - def edit_identities_callback(self, rows_with_keys): - """ Callback for EditIdentitiesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#user_identity_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self._show_main_screen() - - def edit_communities_callback(self, rows_with_keys): - """ Callback for EditCommunitiesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#communities_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self._show_main_screen() - - def edit_roles_callback(self, rows_with_keys): - """ Callback for EditRolesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#roles_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self._show_main_screen() - - def edit_teams_callback(self, rows_with_keys): - """ Callback for EditTeamsScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#teams_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self._show_main_screen() - - def edit_profile_callback(self, return_c): - """ Callback routine for the edit profile screen """ - if isinstance(return_c, int): - if return_c == 200: - self._show_main_screen() - else: - self.log(f"Error returned from EditProfileScreen: {return_c}") - self.log("Returning to main screen") - self._show_main_screen() - elif isinstance(return_c, str): - if return_c == "identity": - main_screen = self.get_screen("main") - self.identities_table = main_screen.query_one("#user_identity_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.identities_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.identities_table.rows: - rows_with_keys.append((row_key.value, self.identities_table.get_row(row_key))) - self.push_screen(EditIdentitiesScreen(columns, rows_with_keys), callback=self.edit_identities_callback) - elif return_c == "community": - main_screen = self.get_screen("main") - self.communities_table = main_screen.query_one("#communities_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.communities_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.communities_table.rows: - rows_with_keys.append((row_key.value, self.communities_table.get_row(row_key))) - self.push_screen(EditCommunitiesScreen(columns, rows_with_keys), callback=self.edit_communities_callback) - elif return_c == "role": - main_screen = self.get_screen("main") - self.roles_table = main_screen.query_one("#roles_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.roles_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.roles_table.rows: - rows_with_keys.append((row_key.value, self.roles_table.get_row(row_key))) - self.push_screen(EditRolesScreen(columns, rows_with_keys), callback=self.edit_roles_callback) - elif return_c == "team": - main_screen = self.get_screen("main") - self.teams_table = main_screen.query_one("#teams_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.teams_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.teams_table.rows: - # row_key.value gets the string representation of the RowKey - # get_row() returns the list of cell values for that row - rows_with_keys.append((row_key.value, self.teams_table.get_row(row_key))) - self.push_screen(EditTeamsScreen(columns, rows_with_keys), callback=self.edit_teams_callback) - else: - self.log(f"Unexpected return type from EditProfileScreen: {type(return_c)}") - self._show_main_screen() - - def edit_tables(self, table_name, row_k): - """ Edit the selected table """ - self.log(f"Editing table: {table_name}, row: {row_k}") - if table_name == "roles_table": - self.push_screen(EditRolesScreen(), callback=self.edit_roles_callback) - elif table_name == "projects_table": - self.push_screen(EditProjectsScreen(), callback=self.edit_projects_callback) - elif table_name == "communities_table": - self.push_screen(EditCommunitiesScreen(), callback=self.edit_communities_callback) - elif table_name == "teams_table": - self.push_screen(EditTeamsScreen(), callback=self.edit_teams_callback) - elif table_name == "blogs_table": - self.push_screen(EditBlogsScreen(), callback=self.edit_blogs_callback) - elif table_name == "journal_table": - self.push_screen(EditJournalScreen(), callback=self.edit_journal_callback) - elif table_name == "todos_table": - self.push_screen(EditTodosScreen(), callback=self.edit_todos_callback) - elif table_name == "associations_table": - self.push_screen(EditAssociationsScreen(), callback=self.edit_assaociations_callback) - else: - self.log(f"Unexpected table name: {table_name}") - - def edit_projects_callback(self, return_c): - pass - - def edit_blogs_callback(self, return_c): - pass - - def edit_journal_callback(self, return_c): - pass - - def edit_todos_callback(self, return_c): - pass - - def edit_assaociations_callback(self, return_c): - pass - - def show_comments(self, table_name, row_k): - """ Show comments for the selected table """ - self.log(f"Showing comments for table: {table_name}, row: {row_k}") - # row_k might be a RowKey object, we want to ensure we pass something that extract_backend_identifier can use - self.push_screen(ShowCommentsScreen(table_name, row_k, - self.view_server, - self.platform_url, - self.user_name, - self.user_password), callback=self.show_comments_callback) - - def show_comments_callback(self, return_c): - """ Callback for ShowCommentsScreen """ - self.log(f"Return from ShowCommentsScreen: {return_c}") - if return_c is None: - self.log("No return from ShowCommentsScreen") - self._show_main_screen() - else: - self.log(f"Unexpected return type from ShowCommentsScreen: {type(return_c)}") - self._show_main_screen() - - def _show_main_screen(self): - """ Callback for ShowCommentsScreen """ - self.log("Returning to main screen") - try: - # query the main screen, if it already exists then switch to it - exists = self.get_screen("main") - self.switch_screen("main") - except KeyError: - # if it doesnt exist then push the main screen - self.log("Main screen does not exist") - self.push_screen("main") - return - - async def add_to_tables(self, selected_table, selected_row): - """ called from main screen when user selects to add a row to a table - This might not be a supported function for all tables""" - self.log(f"Adding row {selected_row} to table {selected_table}") - self.selected_table = selected_table - self.selected_row = selected_row - if self.selected_table == "roles_table": - await self.push_screen(AddRoleScreen(self.selected_table, self.user_GUID), callback=self.add_role_callback) - elif self.selected_table == "projects_table": - await self.push_screen(AddProjectScreen(self.selected_table, self.user_GUID), callback=self.add_project_callback) - elif self.selected_table == "communities_table": - await self.push_screen(AddCommunityScreen(self.selected_table, self.user_GUID), callback=self.add_community_callback) - elif self.selected_table == "my_team_table": - await self.push_screen(AddTeamScreen(self.selected_table, self.user_GUID), callback=self.add_team_callback) - elif self.selected_table == "blogs_table": - await self.push_screen(AddBlogEntryScreen(self.selected_table, self.user_GUID), callback=self.add_blog_entry_callback) - elif self.selected_table == "journal_table": - await self.push_screen(AddJournalEntryScreen(self.selected_table, self.user_GUID), callback=self.add_journal_entry_callback) - elif self.selected_table == "todos_table": - await self.push_screen(AddTodoScreen(self.selected_table, self.user_GUID), callback=self.add_todo_callback) - elif self.selected_table == "associations_table": - await self.push_screen(AddAssociationScreen(self.selected_table, self.user_GUID), callback=self.add_association_callback) - else: - self.log(f"Unexpected table name: {self.selected_table}") - - def add_role_callback(self, result): - """ called from AddRoleScreen when user adds a role """ - if result: - self.log(f"Added role {result}") - self.switch_screen("main") - return - - def add_association_callback(self, result): - """ called from AddAssociationScreen when user adds an association """ - if result: - self.log(f"Added association {result}") - self.switch_screen("main") - return - - def add_project_callback(self, result): - """ called from AddProjectScreen when user adds a project """ - if result: - self.log(f"Added project {result}") - self.switch_screen("main") - return - - def add_community_callback(self, result): - """ called from AddCommunityScreen when user adds a community """ - if result: - self.log(f"Added community {result}") - self.switch_screen("main") - return - - def add_team_callback(self, result): - """ called from AddTeamScreen when user adds a team """ - if result: - self.log(f"Added team {result}") - self.switch_screen("main") - return - - def add_blog_entry_callback(self, result): - """ called from AddBlogEntryScreen when user adds a blog entry """ - if result: - self.log(f"Added blog entry {result}") - self.switch_screen("main") - return - - def add_journal_entry_callback(self, result): - """ called from AddJournalEntryScreen when user adds a journal entry """ - if result: - self.log(f"Added journal entry {result}") - self.switch_screen("main") - return - - def add_todo_callback(self, result): - """ called from AddTodoScreen when user adds a todo """ - if result: - self.log(f"Added todo {result}") - self.switch_screen("main") - return - -if __name__ == "__main__": - app = MyProfileApp() - app.run() -# -# if __name__ == "__main__": -# main() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/sample-data b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/sample-data deleted file mode 100644 index b532c79e..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_monolithic/sample-data +++ /dev/null @@ -1,126 +0,0 @@ -[{'class': 'OpenMetadataRootElement', 'elementHeader': {'class': 'ElementHeader', - 'headerVersion': 0, - 'status': 'ACTIVE', - 'type': {'typeId': '1a226073-9c84-40e4-a422-fbddb9b84278', - 'typeName': 'Comment', - 'superTypeNames': ['Referenceable', 'OpenMetadataRoot'], - 'typeVersion': 1, - 'typeDescription': 'Descriptive feedback or discussion related to the attached element.', - 'typeCategory': 'ENTITY_DEF'}, - 'origin': {'sourceServer': 'qs-metadata-store', - 'originCategory': 'LOCAL_COHORT', - 'homeMetadataCollectionId': '9905c3cb-94c5-4494-9229-0d6f69c0b842', - 'homeMetadataCollectionName': 'qs-metadata-store'}, - 'versions': { 'createdBy': 'garygeeke', - 'updatedBy': 'garygeeke', - 'maintainedBy': ['garygeeke'], - 'createTime': '2026-07-27T18:49:13.892+00:00', - 'updateTime': '2026-07-27T18:49:13.892+00:00', - 'version': 1, - 'lastRequestId': '3db6185f-a523-42d4-b81e-bffd533e9257'}, - 'guid': 'a046df23-2fbf-4cd3-9ebd-bdeb9e2a03e8', - 'anchor': {'class': 'ElementClassification', - 'headerVersion': 0, - 'status': 'ACTIVE', - 'type': { 'typeId': 'aa44f302-2e43-4669-a1e7-edaae414fc6e', - 'typeName': 'Anchors', - 'typeVersion': 1, - 'typeDescription': 'Identifies the anchor entity for an element that is part of a large composite object such as an asset.', - 'typeCategory': 'CLASSIFICATION_DEF'}, - 'origin': { 'sourceServer': 'qs-metadata-store', - 'originCategory': 'LOCAL_COHORT', - 'homeMetadataCollectionId': '9905c3cb-94c5-4494-9229-0d6f69c0b842', - 'homeMetadataCollectionName': 'qs-metadata-store'}, - 'versions': { 'createdBy': 'garygeeke', - 'updatedBy': 'garygeeke', - 'maintainedBy': ['garygeeke'], - 'createTime': '2026-07-27T18:49:13.268+00:00', - 'updateTime': '2026-07-27T18:49:13.268+00:00', - 'version': 1, - 'lastRequestId': '3db6185f-a523-42d4-b81e-bffd533e9257'}, - 'classificationOrigin': 'ASSIGNED', - 'classificationName': 'Anchors', - 'classificationProperties': { 'class': 'AnchorsProperties', - 'typeName': 'Anchors', - 'anchorGUID': '749fb1ea-5df4-4183-98e0-46c939683623', - 'anchorTypeName': 'Community', - 'anchorDomainName': 'Community'}}}, - 'commentedOnElement': { 'class': 'RelatedMetadataElementSummary', - 'relationshipHeader': { 'class': 'ElementHeader', - 'headerVersion': 0, - 'status': 'ACTIVE', - 'type': { 'typeId': '0d90501b-bf29-4621-a207-0c8c953bdac9', - 'typeName': 'AttachedComment', - 'typeVersion': 1, - 'typeDescription': 'Links a comment to an item, or another comment.', - 'typeCategory': 'RELATIONSHIP_DEF'}, - 'origin': { 'sourceServer': 'qs-metadata-store', - 'originCategory': 'LOCAL_COHORT', - 'homeMetadataCollectionId': '9905c3cb-94c5-4494-9229-0d6f69c0b842', - 'homeMetadataCollectionName': 'qs-metadata-store'}, - 'versions': { 'createdBy': 'garygeeke', - 'updatedBy': 'garygeeke', - 'maintainedBy': ['garygeeke'], - 'createTime': '2026-07-27T18:49:13.926+00:00', - 'updateTime': '2026-07-27T18:49:13.926+00:00', - 'version': 1, - 'lastRequestId': '3db6185f-a523-42d4-b81e-bffd533e9257'}, - 'guid': '49b22433-f7da-45df-a1c5-9a88cbe5635e'}, - 'relatedElement': {'elementHeader': { 'class': 'ElementHeader', - 'headerVersion': 0, - 'status': 'ACTIVE', - 'type': { 'typeId': 'fbd42379-f6c3-4f08-b6f7-378565cda993', - 'typeName': 'Community', - 'superTypeNames': ['Referenceable', 'OpenMetadataRoot'], - 'typeVersion': 1, - 'typeDescription': 'A group of people with a common interest or skill.', - 'typeCategory': 'ENTITY_DEF'}, - 'origin': { 'sourceServer': 'qs-metadata-store', - 'originCategory': 'CONTENT_PACK', - 'homeMetadataCollectionId':'655b1965-4c29-4a0e-8a5d-3f55a37b3799', - 'homeMetadataCollectionName': 'Coco Pharmaceuticals Combination', - 'license': 'Apache 2.0'}, - 'versions': { 'createdBy': 'Egeria Project', - 'updatedBy': 'Egeria Project', - 'maintainedBy': ['Egeria Project'], - 'createTime': '2021-12-20T07:20:40.038+00:00', - 'updateTime': '2021-12-20T07:20:40.038+00:00', - 'version': 1783680123325}, - 'guid': '749fb1ea-5df4-4183-98e0-46c939683623'}, - 'properties': { 'class': 'CommunityProperties', - 'typeName': 'Community', - 'qualifiedName': 'Community::Egeria:ValidMetadataValue::domainIdentifier-(9)', - 'displayName': 'Sustainability Community', - 'description': 'Community supporting Sustainability that is lead by the governance domain leader and includes all the people supporting the domain.', - 'mission': 'To provide a mechanism for communication and coordination of work across Coco Pharmaceuticals that supports this governance domain.'}}, - 'relatedElementAtEnd1': True}, - 'relatedBy': {'relationshipHeader': {'class': 'ElementHeader', - 'headerVersion': 0, - 'status': 'ACTIVE', - 'type': { 'typeId': '0d90501b-bf29-4621-a207-0c8c953bdac9', - 'typeName': 'AttachedComment', - 'typeVersion': 1, - 'typeDescription': 'Links a comment to an item, or another comment.', - 'typeCategory': 'RELATIONSHIP_DEF'}, - 'origin': { 'sourceServer': 'qs-metadata-store', - 'originCategory': 'LOCAL_COHORT', - 'homeMetadataCollectionId': '9905c3cb-94c5-4494-9229-0d6f69c0b842', - 'homeMetadataCollectionName': 'qs-metadata-store'}, - 'versions': { 'createdBy': 'garygeeke', - 'updatedBy': 'garygeeke', - 'maintainedBy': ['garygeeke'], - 'createTime': '2026-07-27T18:49:13.926+00:00', - 'updateTime': '2026-07-27T18:49:13.926+00:00', - 'version': 1, - 'lastRequestId': '3db6185f-a523-42d4-b81e-bffd533e9257'}, - 'guid': '49b22433-f7da-45df-a1c5-9a88cbe5635e'}, - 'relationshipProperties': {'class': 'AttachedCommentProperties', - 'typeName': 'AttachedComment'}}, - 'properties': { 'class': 'CommentProperties', - 'typeName': 'Comment', - 'qualifiedName': 'Comment::749fb1ea-5df4-4183-98e0-46c939683623::garygeeke::1785178153', - 'description': 'Test comment with u/c type', - 'commentType': 'QUESTION'}, - 'mermaidGraph': '---\ntitle: Comment - Comment::749fb1ea-5df4-4183-98e0-46c939683623::garygeeke::1785178153 [a046df23-2fbf-4cd3-9ebd-bdeb9e2a03e8]\n---\nflowchart TD\n%%{init: {"flowchart": {"htmlLabels": false}}} - %%\n\n1@{ shape: rounded, label: "*Comment*\n**Comment::749fb1ea-5df4-4183-98e0-46c939683623::garygeeke::1785178153**"}\n2@{ shape: rounded, label: "*Community*\n**Sustainability Community**"}\n2==>|"Attached - Comment"|1\nstyle 1 color:#000000, fill:#e0ab18, stroke:#004563\nstyle 2 color:#000000, fill:#39add1, stroke:#004563\n'}] \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/Add_to_Elements_Screens.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/Add_to_Elements_Screens.py deleted file mode 100644 index f3a0445e..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/Add_to_Elements_Screens.py +++ /dev/null @@ -1,644 +0,0 @@ -"""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a screen for a user to add a comment to a community for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Horizontal -from textual.screen import ModalScreen -from textual.widgets import Header, Static, TextArea, Footer, Input, DataTable, Switch, Button -from pyegeria import Egeria - -class AddCommentScreen(ModalScreen): - """ Add a comment to a community the user belongs to """ - - BINDINGS = [ - ("s", "save_comment", "Save Comment"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, community_GUID): - super().__init__() - self.community_GUID = community_GUID - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add Comment to a Community" - - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddAssociationScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer( - Static("Enter your comment in the text area."), - Static("When complete use Save Comment to save current commment and add another"), - Static("Use Cancel to return to the previous screen"), - TextArea(id="community_comment-textarea"), - id="comment_textarea_container" - ) - yield Footer() - - def action_save_comment(self): - comment = self.query_one("#community_comment-textarea", TextArea) - if comment: - user_comment = comment.text - self.log(f"User comment: {user_comment}") - self.dismiss([self.community_GUID, user_comment]) - else: - container =self.query_one("#comment_textarea_container", ScrollableContainer) - container.mount(Static(f"[@b aquamarine]Please enter a comment prior to selecting Save Comment[/]")) - container.refresh() - - def action_cancel(self): - self.dismiss(200) - -class AddAssociationScreen(ModalScreen): - """ Add an association as an element the user is connected to """ - - BINDINGS = [ - ("s", "save_association", "Save Association"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, community_GUID): - super().__init__() - self.community_GUID = community_GUID - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add Comment to a Community" - - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddCommentScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer( - - id="association_input_container" - ) - yield Footer() - - def action_save_association(self): - # comment = self.query_one("#community_comment-textarea", TextArea) - # if comment: - # user_comment = comment.text - # self.log(f"User comment: {user_comment}") - # self.dismiss([self.community_GUID, user_comment]) - # else: - # container =self.query_one("#comment_textarea_container", ScrollableContainer) - # container.mount(Static(f"[@b aquamarine]Please enter a comment prior to selecting Save Comment[/]")) - # container.refresh() - pass - - def action_cancel(self): - self.dismiss(200) - - -class AddProjectScreen(ModalScreen): - """ Add a project as an element the user is connected to """ - - BINDINGS = [ - ("q", "quit", "Quit Edit Project"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, project_GUID): - super().__init__() - self.project_GUID = project_GUID - main_screen = self.app.get_screen("main") - self.project_table = main_screen.query_one("#project_table", DataTable()) - self.project_table.cursor_type = "row" - self.project_table.zebra_stripes = True - self.project_type = "" - self.project_name = "" - self.project_description = "" - self.project_id = "" - self.result = 0 - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add Project to Egeria" - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddProjectScreen.""" - yield Header(show_clock=True) - yield DataTable(id="self.project_table") - yield ScrollableContainer( - Static(f"[b]Please complete input fields before selecting Add Project[/b]"), - Static(f"Project Type - Campaign, StudyProject, Task, PersonalProject or Project."), - Input(placeholder="Enter Project Type", id="project_type_input"), - Input(placeholder="Enter Project Name (Display Name)", id="project_name_input"), - Input(placeholder="Enter Project Description", id="project_description_input"), - Input(placeholder="Enter a short business id for project", id="project_id_input"), - Static(f"Do you wish to link this project to your profile? [b]Default is True (link the project)[/b]"), - Switch(value=True, id="link_project_switch"), - id="association_input_container" - ) - yield Horizontal( - Button(label="Add Project", id="add_project_button", variant="primary"), - Button(label="Delete Project", id="delete_project_button", variant="error"), - id="project_buttons_container" - ) - yield Footer() - - @on(Input.Changed) - def handle_input_change(self, event: Input.Changed): - if event.control.id == "project_type_input": - self.project_type = event.value - elif event.control.id == "project_name_input": - self.project_name = event.value - elif event.control.id == "project_description_input": - self.project_description = event.value - elif event.control.id == "project_id_input": - self.project_id = event.value - - @on(DataTable.RowHighlighted) - def handle_row_hovered(self, event: DataTable.RowHighlighted): - self.table_row = event.row_key - - @on(DataTable.RowSelected) - def handle_row_selected(self, event: DataTable.RowSelected): - self.table_row = event.row_key - - @on(Button.Pressed, id="add_project_button") - def handle_add_project(self): - if not self.project_type or not self.project_name: - self.notify("At least Project Type and Project Name are required", timeout=12, severity="error") - else: - self.result =self.app.add_project(self.project_type, self.project_name, self.project_description, self.project_id) - if self.result == 200: - self.notify("Project added successfully", timeout=15, severity="information") - - else: - self.notify("Project addition to Egeria failed", timeout=20, severity="error") - - self.query_one("project_type_input", Input).clear() - self.query_one("project_name_input", Input).clear() - self.query_one("project_description_input", Input).clear() - self.query_one("project_id_input", Input).clear() - self.project_table.refresh() - - @on(Button.Pressed, id="delete_project_button") - def handle_delete_project(self): - if not self.table_row: - self.notify("Please select a project to delete before you press the delete button", timeout=12, severity="error") - else: - self.app.delete_project(self.table_row) - - def action_quit(self): - self.dismiss(200) - - def action_cancel(self): - self.dismiss(200) - - -class AddCommunityScreen(ModalScreen): - """ Add a community as an element the user is connected to """ - - BINDINGS = [ - ("s", "save_community", "Save Community"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, community_GUID): - super().__init__() - self.community_GUID = community_GUID - main_screen = self.app.get_screen("main") - self.community_table = main_screen.query_one("#community_table", DataTable()) - self.community_table.cursor_type = "row" - self.community_table.zebra_stripes = True - self.community_row_selected = "" - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add a Community" - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddCommunityScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer ( - DataTable(id="community_table") - ) - yield ScrollableContainer( - Static(f"Please complete all input fields before selecting the Add Community button"), - Static("Community Name"), - Input(id="community_name-input"), - Static("Community Description"), - Input(id="community_description-input"), - Horizontal ( - Button("Add Community", id="add_community_button", variant="primary") - ), - id="association_input_container" - ) - yield Static(f"Please select a community before you select one of the action buttons") - Horizontal ( - Button("Remove from my Profile", id="remove_community_link_button", variant="warning"), - Button("Delete Community", id="delete_community_button", variant="error") - ) - yield Footer() - - @on(DataTable.RowSelected, "#community_table") - def action_select_community(self, event: DataTable.RowSelected): - """ Once thew user has selected a community in the table, - this routine retrieves the community identity from the table row - and then call a delete community routine in the app to delete it from egeria """ - self.log(f"Row: {event} selected by user") - self.community_row_selected = event.row_key - - @on(DataTable.RowHighlighted) - def action_highlight_community(self, event: DataTable.RowHighlighted): - """ Once thew user has selected a community in the table, - this routine retrieves the community identity from the table row - and then call a delete community routine in the app to delete it from egeria """ - self.log(f"Row: {event} highlighted by user") - self.community_row_selected = event.row_key - - @on(Button.Pressed, "#add_community_button") - def action_add_community(self, event: Button.Pressed): - """ Once user input is complete, use that to create a new community in Egeria - This function calls back to an add community function in the app """ - self.log(f"Button: {event} selected by user") - self.community_name = self.query_one("#community_name_input", Input) - self.community_description = self.query_one("#community_description_input", Input) - if self.community_name and self.community_description: - return_code = self.app.add_community(self.community_name.value, self.community_description.value) - if return_code == 200: - self.notify(f"Community {self.community_name.value} created in Egeria", timeout=15, severity="information") - else: - self.notify(f"Failed to create community in Egeria, error in log") - self.dismiss(return_code) - else: - self.notify(f"please complete input before selecting Add Community", timeout=12, severity="warning") - - -@on(Button.Pressed, "#remove_community_link_button") -def remove_community(self, event: Button.Pressed): - """ Once thew user has selected a community in the table, - this routine retrieves the community identity from the table row - and then call a remove link to the user_profile routine in the app to delete it from egeria """ - self.log(f"Button: {event} selected by user") - if self.community_row_selected: - return_code = self.app.remove_link_to_community(self.community_row_selected) - if return_code == 200: - self.notify(f"Community {self.community_name.value} Link removed from Profile", timeout=15, severity="information") - else: - self.notify(f"Failed to remove community link from profile, error in log") - self.dismiss(return_code) - else: - self.notify(f"please select a community (table row) before selecting Remove Community", timeout=12, - severity="warning") - - @on(Button.Pressed, "#delete_community_button") - def delete_community(self, event: Button.Pressed): - """ Once thew user has selected a community in the table, - this routine retrieves the community identity from the table row - and then call a delete community routine in the app to delete it from egeria """ - self.log(f"Button: {event} selected by user") - if self.community_row_selected: - return_code = self.app.delete_community(self.community_row_selected) - if return_code == 200: - self.notify(f"Community {self.community_name.value} deleted in Egeria", timeout=15, severity="information") - else: - self.notify(f"Failed to delete community in Egeria, error in log") - self.dismiss(return_code) - else: - self.notify(f"please select a community (table row) before selecting Delete Community", timeout=12, severity="warning") - - def action_cancel(self): - self.log(f"Cancel hot key selected by user") - self.dismiss(200) - - -class AddRoleScreen(ModalScreen): - """ Add a role as an element the user is connected to """ - - BINDINGS = [ - ("s", "save_role", "Save Role"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, role_GUID): - super().__init__() - self.role_GUID = role_GUID - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add Comment to a Community" - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddRoleScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer( - - id="role_input_container" - ) - yield Footer() - - def action_save_role(self): - - # comment = self.query_one("#community_comment-textarea", TextArea) - # if comment: - # user_comment = comment.text - # self.log(f"User comment: {user_comment}") - # self.dismiss([self.community_GUID, user_comment]) - # else: - # container =self.query_one("#comment_textarea_container", ScrollableContainer) - # container.mount(Static(f"[@b aquamarine]Please enter a comment prior to selecting Save Comment[/]")) - # container.refresh() - pass - - def action_cancel(self): - self.dismiss(200) - - -class AddTeamScreen(ModalScreen): - """ Add an association to an element the user is connected to """ - - BINDINGS = [ - ("s", "save_team", "Save Team"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, team_GUID): - super().__init__() - self.team_GUID = team_GUID - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add Team" - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddTeamScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer( - - id="association_input_container" - ) - yield Footer() - - def action_save_team(self): - - # comment = self.query_one("#community_comment-textarea", TextArea) - # if comment: - # user_comment = comment.text - # self.log(f"User comment: {user_comment}") - # self.dismiss([self.community_GUID, user_comment]) - # else: - # container =self.query_one("#comment_textarea_container", ScrollableContainer) - # container.mount(Static(f"[@b aquamarine]Please enter a comment prior to selecting Save Comment[/]")) - # container.refresh() - pass - - def action_cancel(self): - self.dismiss(200) - - -class AddBlogEntryScreen(ModalScreen): - """ Add a Blog Entry as an element for the user """ - - BINDINGS = [ - ("s", "save_blog_entry", "Save Blog Entry"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, actor_profile_GUID): - super().__init__() - self.actor_profile_GUID = actor_profile_GUID - self.blog_selected = None - self.blog_entry_selected = None - self.blog_table = self.app.query_one("#blog_table") - self.blog_table.id="blog_table" - self.blog_table.cursor_type="row" - self.blog_table.zebra_stripes=True - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Blog and Blog Entry Processing" - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddBlogEntryScreen.""" - yield Header(show_clock=True) - yield DataTable(id="blog_table") - yield DataTable(id="blog_entry_table") - yield ScrollableContainer( - Static(f"Please complete input fields before selecting the Add Blog button", id="blog_static_1"), - Static("Please select a blog before selecting display blog entries", id="blog_static_2"), - Static("Please select a blog before selecting delete blog", id="blog_static_3"), - Static("Please select a blog entry before selecting delete blog entry", id="blog_static_4"), - Input(placeholder="Enter blog entry title", id="blog_entry_title"), - TextArea(id="blog_entry_text"), - Horizontal( - Button("Add Blog", variant="primary", id="add_blog_button"), - Button("Add Blog Entry", variant="primary", id="add_blog_entry_button"), - Button("Display Blog Entries", variant="primary", id="display_blog_entries_button"), - id="blog_button_container" - ), - id="blog_entry_input_container" - ) - yield Button("Delete Blog", variant="error", id="delete_blog_button") - yield Footer() - - @on(DataTable.RowHighlighted, "#blog_table") - def action_highlighted_blog(self, event: DataTable.RowHighlighted): - """ Highlight a blog""" - self.blog_selected = event.row_key - self.log(f"Blog Row Highlighted: {self.blog_selected}") - - @on(DataTable.RowSelected, "#blog_table") - def action_selected_blog(self, event: DataTable.RowSelected): - """Select a blog to delete""" - self.blog_selected = event.row_key - self.log(f"Blog Row Selected: {self.blog_selected}") - - @on(DataTable.RowHighlighted, "#blog_entry_table") - def action_highlighted_blog_entry(self, event: DataTable.RowHighlighted): - """ Highlight a blog entry to delete""" - self.blog_entry_selected = event.row_key - self.log(f"Blog Entry Row Highlighted: {self.blog_entry_selected}") - - @on(DataTable.RowSelected, "#blog_entry_table") - def action_selected_blog_entry(self, event: DataTable.RowSelected): - """Select a blog entry to delete""" - self.blog_selected = event.row_key - self.log(f"Blog Entry Row Selected: {self.blog_selected}") - - @on(Button.Pressed, "#add_blog_entry_button") - def action_add_blog_entry(self): - """ Verify that the input is complete and then call the - add blog function in the app """ - self.blog_entry_title = self.query_one("#blog_entry_title") - self.blog_entry_text = self.query_one("#blog_entry_text") - if self.blog_entry_title and self.blog_entry_text: - return_code = self.app.add_blog_entry(self.blog_entry_title, self.blog_entry_text) - if return_code == 200: - self.log(f"Blog entry added {self.blog_entry_title}") - self.notify(f"Blog entry added {self.blog_entry_title}") - else: - self.log(f"Blog entry not added {self.blog_entry_title}, return code {return_code}") - self.notify(f"Blog entry not added {self.blog_entry_title}, return code {return_code}") - - @on(Button.Pressed, "#delete_blog_button") - def action_delete_blog(self): - """Delete a blog, user must select the entry to delete""" - if self.blog_selected: - return_code = self.app.delete_blog(self.blog_selected) - if return_code == 200: - self.log(f"Blog entry deleted {self.blog_selected}") - self.notify(f"Blog entry deleted {self.blog_selected}") - else: - self.log(f"Blog entry not deleted {self.blog_selected}, return code {return_code}") - self.notify(f"Blog entry not deleted {self.blog_selected}, return code {return_code}") - - @on() - - def action_cancel(self): - self.dismiss(200) - - -class AddJournalEntryScreen(ModalScreen): - """ Add a Journal Entry to the actor profile """ - - BINDINGS = [ - ("s", "save_journal_entry", "Save Journal Entry"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, user_GUID): - super().__init__() - self.user_GUID = user_GUID - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add Journal Entry" - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddJournalEntryScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer( - - id="journal_entry_input_container" - ) - yield Footer() - - def action_save_journal_entry(self): - - # comment = self.query_one("#community_comment-textarea", TextArea) - # if comment: - # user_comment = comment.text - # self.log(f"User comment: {user_comment}") - # self.dismiss([self.community_GUID, user_comment]) - # else: - # container =self.query_one("#comment_textarea_container", ScrollableContainer) - # container.mount(Static(f"[@b aquamarine]Please enter a comment prior to selecting Save Comment[/]")) - # container.refresh() - pass - - def action_cancel(self): - self.dismiss(200) - - -class AddTodosScreen(ModalScreen): - """ Add an association to an element the user is connected to """ - - BINDINGS = [ - ("s", "save_todo", "Save Todo"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, user_GUID): - super().__init__() - self.user_GUID = user_GUID - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add Todos for an Actor" - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddTodosScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer( - - id="todo_input_container" - ) - yield Footer() - - def action_save_todo(self): - - # comment = self.query_one("#community_comment-textarea", TextArea) - # if comment: - # user_comment = comment.text - # self.log(f"User comment: {user_comment}") - # self.dismiss([self.community_GUID, user_comment]) - # else: - # container =self.query_one("#comment_textarea_container", ScrollableContainer) - # container.mount(Static(f"[@b aquamarine]Please enter a comment prior to selecting Save Comment[/]")) - # container.refresh() - pass - - def action_cancel(self): - self.dismiss(200) - - -class AddUserIdentityScreen(ModalScreen): - """ Add a user identity to the actor profile """ - - BINDINGS = [ - ("s", "save_user_identity", "Save User_Identity"), - ("c", "cancel", "Cancel") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, user_GUID): - super().__init__() - self.user_GUID = user_GUID - - def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add user identity to the actor profile" - - def compose(self) -> ComposeResult: - """This method composes the UI for the AddUserIdentityScreen.""" - yield Header(show_clock=True) - yield ScrollableContainer( - - id="user_identity_input_container" - ) - yield Footer() - - def action_save_user_identity(self): - - # comment = self.query_one("#community_comment-textarea", TextArea) - # if comment: - # user_comment = comment.text - # self.log(f"User comment: {user_comment}") - # self.dismiss([self.community_GUID, user_comment]) - # else: - # container =self.query_one("#comment_textarea_container", ScrollableContainer) - # container.mount(Static(f"[@b aquamarine]Please enter a comment prior to selecting Save Comment[/]")) - # container.refresh() - pass - - def action_cancel(self): - self.dismiss(200) \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/EditElementsScreens.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/EditElementsScreens.py deleted file mode 100644 index 7b379f15..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/EditElementsScreens.py +++ /dev/null @@ -1,992 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from typing import Any - -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Horizontal -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, DataTable, Static, Placeholder, Input, Button - -from pyegeria import Egeria, PyegeriaException, load_app_config - - -class EditBlogsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the blogs - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_blogs_table: DataTable = DataTable(id="blogs_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.blogs_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Blogs", classes="span-3", id="edit_blogs_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_blogs_container") - yield Footer() - - async def on_mount(self): - self.log("Edit blogs screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Blogs" - # Populate DataTable - self.my_blogs_table.clear(columns=True) - self.my_blogs_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_blogs_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit blogs container in the screen composition objects - self.blogs_container = self.query_one("#edit_blogs_container", ScrollableContainer) - self.log(f"Blogs container found: {self.blogs_container}") - # Mount the table into the container - await self.blogs_container.mount(self.my_blogs_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.blogs_container.refresh(layout=True) - self.my_blogs_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_blogs_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit blogs container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#blogs_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_blogs_table.rows: - rows_with_keys.append((row_key.value, self.my_blogs_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_blogs_table.remove_row(self.row_key) - self.my_blogs_table.refresh() - self.blogs_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.blogs_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.blogs_container.refresh(layout=True) - - -class EditCommunitiesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the communities - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("ctrl+c", "add_community", "Add Community"), - ("ctrl+r", "remove_link_to_community", "Remove Link to Community"), - ("ctrl+e", "delete_community", "Delete Community"), - ("ctrl+m", "add_comment", "Add Comment"), - ("ctrl+o", "show_comments", "Show Comments"), - ("ctrl+s", "show_team_members", "Show Team") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_communities_table: DataTable = DataTable(id="communities_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.communities_container: ScrollableContainer - self.new_name = "" - self.new_description = "" - self.new_mission = "" - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Communities", classes="span-3", id="edit_communities_title") - yield Static(" Please note you may only add or delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_communities_container") - yield Footer() - - async def on_mount(self): - self.log("Edit communities screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Communities" - # Populate DataTable - self.my_communities_table.clear(columns=True) - self.my_communities_table.add_columns(*self.columns) - self.my_communities_table.cursor_type="row" - for key_str, cell_values in self.rows_with_keys: - self.my_communities_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit communities container in the screen composition objects - self.communities_container = self.query_one("#edit_communities_container", ScrollableContainer) - self.log(f"Communities container found: {self.communities_container}") - # Mount the table into the container - await self.communities_container.mount(self.my_communities_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.communities_container.refresh(layout=True) - self.my_communities_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_communities_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit communities container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#communities_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_communities_table.rows: - rows_with_keys.append((row_key.value, self.my_communities_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_community(self): - """ The user has selected the delete row option """ - self.log(f"Delete community selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.row_content = self.my_communities_table.get_row(self.row_key) - # Delete the Community from Egeria, change this to call a function in the app. - self.log(f"Deleting community with content: {self.row_content}") - del_comm_rc = self.app.delete_community(self.row_content) - if del_comm_rc == 200: - self.my_communities_table.remove_row(self.row_key) - self.my_communities_table.refresh() - self.communities_container.refresh(layout=True) - else: - self.communities_container.mount(Static("Failed to delete community, please try again!")) - self.communities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by notifying the user - # and waiting for any further actions. - else: - self.notify("Please select a row to delete prior to using the hot key!") - self.communities_container.refresh(layout=True) - return - - def action_add_community(self): - """ The user has selected the add community option """ - self.log("Add community selected") - container = self.query_one("#edit_communities_container", ScrollableContainer) - container.mount( - Input("Name", id="new_community_name"), - Input("Description", id="new_community_description"), - Input("Mission", id="new_community_mission"), - Button("Add community", id="add_community_button") - ) - container.refresh() - new_name = self.query_one("#new_community_name", Input).focus=True - # self.communities_container.mount(Static("Add community functionality not yet implemented!")) - # self.communities_container.refresh(layout=True) - - def action_remove_link_to_community(self): - """ The user has selected the remove link to community option """ - self.log(f"Remove link to community selected: {self.row_key}") - - # If there is a row selected, remove the link to the actor profile and - # delete it from the DataTable and clear the row key variable - if self.row_key: - self.row_content = self.my_communities_table.get_row(self.row_key) - # Remove the link to the Community from the actor profile, - # to do this call a function in the app. - self.log(f"Deleting link to community with content: {self.row_content}") - rem_comm_rc = self.app.remove_link_to_community(self.row_content) - if rem_comm_rc == 200: - self.my_communities_table.remove_row(self.row_key) - self.my_communities_table.refresh() - self.communities_container.refresh(layout=True) - else: - self.communities_container.mount(Static("Failed to delete link to community, please try again!")) - self.communities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by notifying the user - # and waiting for any further actions. - else: - self.notify("Please select a row to remove the link to prior to using the hot key!") - self.communities_container.refresh(layout=True) - return - - def action_show_team_members(self): - """ The user has selected the show team members option """ - self.log("Show team members selected") - self.app.show_team(self.row_key) - - @on(Input.Changed, "#new_community_name") - def on_input_changed(self, event: Input.Changed): - self.log("Input changed") - self.new_name = event.value - - @on(Input.Changed, "#new_community_description") - def on_input_changed(self, event: Input.Changed): - self.log("Input changed") - self.new_description = event.value - - @on(Input.Changed, "#new_community_mission") - def on_input_changed(self, event: Input.Changed): - self.log("Input changed") - if event.control.id == "new_community_mission": - self.new_mission = event.value - elif event.control.id == "new_community_description": - self.new_description = event.value - elif event.control.id == "new_community_name": - self.new_name = event.value - - @on(Button.Pressed, "#add_community_button") - def on_button_pressed(self, event: Button.Pressed): - self.log("Button pressed") - self.add_community() - - def add_community(self): - if self.new_name and self.new_description and self.new_mission: - self.dismiss(["add", self.new_name, self.new_description, self.new_mission]) - else: - self.log("Missing required fields") - self.notify("Missing required fields, please complete all input before adding", timeout=5, severity="warning") - - -class EditIdentitiesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the identities - they have in Egeria.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_identities_table: DataTable = DataTable(id="identities_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.identities_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Identities", classes="span-3", id="edit_identities_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_identities_container") - yield Footer() - - async def on_mount(self): - self.log("Edit identities screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Identities" - # Populate DataTable - self.my_identities_table.clear(columns=True) - self.my_identities_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_identities_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit identities container in the screen composition objects - self.identities_container = self.query_one("#edit_identities_container", ScrollableContainer) - self.log(f"Identities container found: {self.identities_container}") - # Mount the table into the container - await self.identities_container.mount(self.my_identities_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.identities_container.refresh(layout=True) - self.my_identities_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_identities_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit identities container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#identities_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_identities_table.rows: - rows_with_keys.append((row_key.value, self.my_identities_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_identities_table.remove_row(self.row_key) - self.my_identities_table.refresh() - self.identities_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.identities_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.identities_container.refresh(layout=True) - - -class EditJournalScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the journal - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_journal_table: DataTable = DataTable(id="journal_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.journal_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Journal", classes="span-3", id="edit_journal_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_journal_container") - yield Footer() - - async def on_mount(self): - self.log("Edit journal screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Journal" - # Populate DataTable - self.my_journal_table.clear(columns=True) - self.my_journal_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_journal_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit journal container in the screen composition objects - self.journal_container = self.query_one("#edit_journal_container", ScrollableContainer) - self.log(f"Journal container found: {self.journal_container}") - # Mount the table into the container - await self.journal_container.mount(self.my_journal_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.journal_container.refresh(layout=True) - self.my_journal_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_journal_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit journal container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#journal_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_journal_table.rows: - rows_with_keys.append((row_key.value, self.my_journal_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_journal_table.remove_row(self.row_key) - self.my_journal_table.refresh() - self.journal_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.journal_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.journal_container.refresh(layout=True) - - -class EditProfileScreen(ModalScreen): - """Modal screen to create a new user profile in Egeria. - - Dismisses with: - 200 on success or a code for further editing of related collections - 4xx on failure - """ - - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("ctrl+c", "Edit_communities", "Edit communities"), - ("ctrl+i", "Edit_identities", "Edit identities"), - ("ctrl+r", "Edit_roles", "Edit roles"), - ("ctrl+t", "Edit_teams", "Edit teams"), - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, user, - password, - view_server, - platform_url, - karma_points, - user_profile, - user_GUID - ): - super().__init__() - load_app_config() - # config_logging() - self.user_name = user - self.user_password = password - self.view_server = view_server - self.platform_url = platform_url - self.karma_points = karma_points - self.user_profile = user_profile - self.user_GUID = user_GUID - print("Platform:", self.platform_url) - print("View Server:", self.view_server) - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Edit Egeria Profile for user: {self.user_name}" - # retrieve person profile details - - - def compose(self) -> ComposeResult: - self.job_title = str(self.user_profile.get("jobTitle", self.user_profile.get("Job Title", ""))) - self.log(f"Job Title: {self.job_title}") - self.given_names = str(self.user_profile.get("givenNames", "")) - self.log(f"Given Names: {self.given_names}") - self.surname = str(self.user_profile.get("surname", "")) - self.log(f"Surname: {self.surname}") - self.display_name = str(self.user_profile.get("displayName", "")) - self.log(f"Display Name: {self.display_name}") - self.pronouns = str(self.user_profile.get("pronouns", "")) - self.log(f"Pronouns: {self.pronouns}") - self.description = str(self.user_profile.get("description", "")) - self.log(f"Description: {self.description}") - self.employee_id = str(self.user_profile.get("employeeNumber", self.user_profile.get("employeeId", self.user_profile.get("Employee Number", "")))) - self.log(f"Employee ID: {self.employee_id}") - self.preferred_language = str(self.user_profile.get("preferredLanguage", "")) - self.log(f"Preferred Language: {self.preferred_language}") - self.resident_country = str(self.user_profile.get("residentCountry", "")) - self.log(f"Resident Country: {self.resident_country}") - self.time_zone = str(self.user_profile.get("timeZone", "")) - self.log(f"Time Zone: {self.time_zone}") - self.courtesy_title = str(self.user_profile.get("courtesyTitle", "")) - - yield Header(show_clock=True) - yield Static("Update your profile in Egeria", classes="span-3", id="edit_profile_title") - yield Static() - yield ScrollableContainer( - Static("Edit your information as needed:"), - Horizontal( - Static("Courtesy Title:"), - Input(value=self.courtesy_title, placeholder="Courtesy Title", id="user_title")), - Horizontal( - Static("Job Title:"), - Input(value=self.job_title, placeholder="Job Title", id="user_job_title")), - Horizontal( - Static("Given Names:"), - Input(value=self.given_names, placeholder="Given Names", id="user_given_names")), - Horizontal( - Static("Family/Surname:"), - Input(value=self.surname, placeholder="Surname", id="user_family_name")), - Horizontal( - Static("Preferred Name:"), - Input(value=self.display_name, placeholder="Preferred Name", id="user_preferred_name")), - Horizontal( - Static("Pronouns:"), - Input(value=self.pronouns, placeholder="Pronouns", id="user_pronouns")), - Horizontal( - Static("Description:"), - Input(value=self.description, placeholder="Description", id="user_description")), - Horizontal( - Static("Time Zone:"), - Input(value=self.time_zone, placeholder="Time Zone", id="user_time_zone")), - Horizontal( - Static("Employee ID:"), - Input(value=self.employee_id, placeholder="Employee ID", id="user_employee_id")), - Horizontal( - Static("Preferred Language:"), - Input(value=self.preferred_language, placeholder="Preferred Language", id="user_preferred_language")), - Horizontal( - Static("Resident Country:"), - Input(value=self.resident_country, placeholder="Resident Country", id="user_resident_country")), - Button("Edit Profile", id="edit_profile_btn"), - id="edit_profile_form", - ) - yield Static() - yield Footer() - - @on(Button.Pressed, "#edit_profile_btn") - def create_profile(self) -> Any: - """Update profile in Egeria from data provided in Input fields.""" - - input_q_name = "Person" + self.query_one("#user_employee_id", Input).value + \ - self.query_one("#user_resident_country", Input).value + \ - self.query_one("#user_given_names", Input).value + \ - self.query_one("#user_family_name", Input).value - - input_d_name = self.query_one("#user_preferred_name", Input).value - - input_c_title = self.query_one("#user_title", Input).value - - input_g_name = self.query_one("#user_given_names", Input).value - - input_s_name = self.query_one("#user_family_name", Input).value - - input_f_name = self.query_one("#user_preferred_name", Input).value - - input_pronouns = self.query_one("#user_pronouns", Input).value - - input_j_title = self.query_one("#user_job_title", Input).value - - input_e_num = self.query_one("#user_employee_id", Input).value - - input_p_lang = self.query_one("#user_preferred_language", Input).value - - input_r_country = self.query_one("#user_resident_country", Input).value - - input_t_z = self.query_one("#user_time_zone", Input).value - - input_desc = self.query_one("#user_description", Input).value - - self.log(f"Input q_name:{input_q_name}") - self.log(f"Input d_name:{input_d_name}") - self.log(f"Input j_title:{input_j_title}") - self.log(f"Input e_num:{input_e_num}") - self.log(f"Input p_lang:{input_p_lang}") - self.log(f"Input r_country:{input_r_country}") - self.log(f"Input t_z:{input_t_z}") - self.log(f"Input desc:{input_desc}") - - props: dict = {} - props.update({"class": "PersonProperties"}) - if input_q_name: - props.update({"qualifiedName": input_q_name}) - if input_d_name: - props.update({"displayName": input_d_name}) - if input_c_title: - props.update({"courtesyTitle": input_c_title}) - if input_g_name: - props.update({"givenNames": input_g_name}) - if input_s_name: - props.update({"surname": input_s_name}) - if input_f_name: - props.update({"fullName": input_f_name}) - if input_pronouns: - props.update({"pronouns": input_pronouns}) - if input_j_title: - props.update({"jobTitle": input_j_title}) - if input_e_num: - props.update({"employeeNumber": input_e_num}) - if input_p_lang: - props.update({"preferredLanguage": input_p_lang}) - if input_r_country: - props.update({"residentCountry": input_r_country}) - if input_t_z: - props.update({"timeZone": input_t_z}) - if input_desc: - props.update({"description": input_desc}) - - self.log(f"Props: {props}") - - self.update_element_request_body: dict = {"class": "UpdateElementRequestBody", - "isOwnAnchor": True, - "properties": props - } - try: - new_profile_inst = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) - new_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) - new_profile_inst.update_actor_profile(self.user_GUID, self.update_element_request_body) - self.log(f"Profile updated for GUID: {self.user_GUID}") - self.dismiss(200) - return (200) - except PyegeriaException as e: - self.log(f"Error creating profile: {e!s} | request={self.update_element_request_body}") - self.dismiss(401) - return (401) - - def action_quit(self) -> int: - self.dismiss(200) - return(200) - - def action_Edit_communities(self) -> str: - self.dismiss("community") - - def action_Edit_identities(self) -> str: - self.dismiss("identity") - - def action_Edit_roles(self) -> str: - self.dismiss("role") - - def action_Edit_teams(self) -> str: - self.dismiss("team") - - -class EditProjectsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the projects - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_projects_table: DataTable = DataTable(id="projects_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.projects_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Projects", classes="span-3", id="edit_projects_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_projects_container") - yield Footer() - - async def on_mount(self): - self.log("Edit projects screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Projects" - # Populate DataTable - self.my_projects_table.clear(columns=True) - self.my_projects_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_projects_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit projects container in the screen composition objects - self.projects_container = self.query_one("#edit_projects_container", ScrollableContainer) - self.log(f"Projects container found: {self.projects_container}") - # Mount the table into the container - await self.projects_container.mount(self.my_projects_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.projects_container.refresh(layout=True) - self.my_projects_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_projects_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit projects container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#projects_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_projects_table.rows: - rows_with_keys.append((row_key.value, self.my_projects_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_projects_table.remove_row(self.row_key) - self.my_projects_table.refresh() - self.projects_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.projects_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.projects_container.refresh(layout=True) - - -class EditRolesScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the roles - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("ctrl+a", "add_role", "Add Role"), - ("ctrl+d", "delete_role", "Delete Role"), - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_roles_table: DataTable = DataTable(id="roles_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.roles_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Roles", classes="span-3", id="edit_roles_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_roles_container") - yield Footer() - - async def on_mount(self): - self.log("Edit roles screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Roles" - # Populate DataTable - self.my_roles_table.clear(columns=True) - self.my_roles_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_roles_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit roles container in the screen composition objects - self.roles_container = self.query_one("#edit_roles_container", ScrollableContainer) - self.log(f"Roles container found: {self.roles_container}") - # Mount the table into the container - await self.roles_container.mount(self.my_roles_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.roles_container.refresh(layout=True) - self.my_roles_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_roles_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit roles container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#roles_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_roles_table.rows: - rows_with_keys.append((row_key.value, self.my_roles_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_role(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_roles_table.remove_row(self.row_key) - self.my_roles_table.refresh() - self.roles_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.roles_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.roles_container.refresh(layout=True) - - def action_add_role(self): - """ The user has selected the add role option """ - self.log("Add role selected") - self.roles_container.mount(Static("Add role functionality not yet implemented!")) - self.roles_container.refresh(layout=True) - - -class EditTeamsScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the teams - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("ctrl+t", "add_team", "Add Team"), - ("ctrl+r", "delete_team", "Delete Team") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_teams_table: DataTable = DataTable(id="teams_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.teams_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Teams", classes="span-3", id="edit_teams_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_teams_container") - yield Footer() - - async def on_mount(self): - self.log("Edit teams screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Teams" - # Polulate DataTable - self.my_teams_table.clear(columns=True) - self.my_teams_table.add_columns(*self.columns) - self.my_teams_table.cursor_type="row" - for key_str, cell_values in self.rows_with_keys: - self.my_teams_table.add_row(*cell_values, key=key_str) - try: - # Access the edit teams container in the screen composition objects - self.teams_container = self.query_one("#edit_teams_container", ScrollableContainer) - self.log(f"Teams container found: {self.teams_container}") - # Mount the table into the container - await self.teams_container.mount(self.my_teams_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.teams_container.refresh(layout=True) - self.my_teams_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_teams_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit teams container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#teams_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - self.table = event.data_table - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_teams_table.rows: - rows_with_keys.append((row_key.value, self.my_teams_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_team(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_teams_table.remove_row(self.row_key) - self.my_teams_table.refresh() - self.teams_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.teams_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.teams_container.refresh(layout=True) - - def action_add_team(self): - """ The user has selected the add team option """ - self.log("Add team selected") - self.teams_container.mount(Static("Add team functionality not yet implemented!")) - self.teams_container.refresh(layout=True) - - -class EditTodosScreen(ModalScreen): - """ Screen called during editing of a users profile to allow editing of the todos - they belong too.""" - - BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("d", "delete_row", "Delete Row") - ] - - def __init__(self, columns, rows_with_keys, *args, **kwargs): - super().__init__(*args, **kwargs) - self.my_todos_table: DataTable = DataTable(id="todos_destination") - self.row_key = None - self.columns = columns - self.rows_with_keys = rows_with_keys - self.todos_container: ScrollableContainer - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Edit Todos", classes="span-3", id="edit_todos_title") - yield Static(" Please note you may only delete one row at a time!", classes="span-3") - yield ScrollableContainer(id="edit_todos_container") - yield Footer() - - async def on_mount(self): - self.log("Edit todos screen mounted") - self.title = "Egeria - my_profile" - self.sub_title = "Edit Todos" - # Populate DataTable - self.my_todos_table.clear(columns=True) - self.my_todos_table.add_columns(*self.columns) - for key_str, cell_values in self.rows_with_keys: - self.my_todos_table.add_row(*cell_values, key=key_str) - - try: - # Access the edit todos container in the screen composition objects - self.todos_container = self.query_one("#edit_todos_container", ScrollableContainer) - self.log(f"Todos container found: {self.todos_container}") - # Mount the table into the container - await self.todos_container.mount(self.my_todos_table) - self.log("Table mounted into container") - # To make sure, refresh the container object on the display - self.todos_container.refresh(layout=True) - self.my_todos_table.refresh(layout=True) - self.focus() - self.call_after_refresh(self.my_todos_table.focus) - except NoMatches as e: - # If there is an error finding the container - self.log(f"Edit todos container not found - Error: {e}") - await self.dismiss(400) - - @on(DataTable.RowSelected, "#todos_destination") - def row_selected(self, event: DataTable.RowSelected): - """ When the user selects a row in the data table store the row key""" - self.row_key = event.row_key - - def action_exit_screen(self): - """ The user has requested to exit the screen, return the current table data """ - rows_with_keys = [] - for row_key in self.my_todos_table.rows: - rows_with_keys.append((row_key.value, self.my_todos_table.get_row(row_key))) - self.dismiss(rows_with_keys) - - def action_delete_row(self): - """ The user has selected the delete row option """ - self.log(f"Delete row selected, row key: {self.row_key}") - # If there is a row selected, delete it and clear the row key variable - if self.row_key: - self.my_todos_table.remove_row(self.row_key) - self.my_todos_table.refresh() - self.todos_container.refresh(layout=True) - self.row_key = None - # If there is no row key in the variable, inform the user by mounting a message into the container - # and wait for any further actions. - else: - self.todos_container.mount(Static("Please select a row to delete prior to using the hot key!")) - self.todos_container.refresh(layout=True) - - -class EditAssociationsScreen(ModalScreen): - """Edit the users associations """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - pass - - -class EditActivityScreen(ModalScreen): - """Edit the users activity """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - pass \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/MainScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/MainScreen.py deleted file mode 100644 index 71c0724e..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/MainScreen.py +++ /dev/null @@ -1,163 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import Screen -from textual.widgets import DataTable, OptionList, Header, Static, Footer -from textual.widgets._option_list import Option - - -class MainScreen(Screen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "app.quit", "Quit"), - ("ctrl+e", "edit_selected_table", "Edit Selected Table"), - ("ctrl+a", "add_note", "Add Note to Selected Item"), - ("ctrl+s", "show_notes", "Show Notes for Selected Item"), - ("ctrl+t", "show_team", "Show Team Members for Selected Role"), - ("r", "app.refresh", "Refresh") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, *args, **kwargs): - super().__init__(id="main_screen", *args, **kwargs) - self.title = "Egeria" - self.sub_title = "My Profile" - self.other_function_list: OptionList = OptionList(id="other_function_list") - self.selected_table = None - self.selected_row = None - - self.associatioons_table: DataTable = DataTable(id="associations_table") - self.associatioons_table.zebra_stripes = True - self.associatioons_table.cursor_type = "row" - - self.projects_table: DataTable = DataTable(id="projects_table") - self.projects_table.zebra_stripes = True - self.projects_table.cursor_type = "row" - - self.communities_table: DataTable = DataTable(id="communities_table") - self.communities_table.zebra_stripes = True - self.communities_table.cursor_type = "row" - - self.roles_table: DataTable = DataTable(id="roles_table") - self.roles_table.zebra_stripes = True - self.roles_table.cursor_type = "row" - - self.teams_table: DataTable = DataTable(id="teams_table") - self.teams_table.zebra_stripes = True - self.teams_table.cursor_type = "row" - - self.blogs_table: DataTable = DataTable(id="blogs_table") - self.blogs_table.zebra_stripes = True - self.blogs_table.cursor_type = "row" - - self.journal_table: DataTable = DataTable(id="journal_table") - self.journal_table.zebra_stripes = True - self.journal_table.cursor_type = "row" - - self.todos_table: DataTable = DataTable(id="todos_table") - self.todos_table.zebra_stripes = True - self.todos_table.cursor_type = "row" - - def compose(self) -> ComposeResult: - # place widgets into grid on screen, note sequence determines position! - yield Header(show_clock=True, id="main_header") - - yield ScrollableContainer( - Static("My Associations"), - DataTable(id="associations_table"), - id="main_associations_container" - ) - - yield ScrollableContainer( - Static("My Collections"), - DataTable(id="my_collections_table"), - id="main_my_collections_container" - ) - - yield ScrollableContainer( - Static(f"Other Functions"), - Static(f"[b]Select a function[/b]"), - OptionList( - Option("User Identities"), - Option("Catalogs/Shop for Data"), - Option("Edit Profile"), - Option("Subscriptions", disabled=True), - Option("Technology Types"), - Option("User Bookmarks", disabled=True), - id="other_function_list" - ), - id="other_function_container" - ) - - yield ScrollableContainer( - Static("Roles"), - DataTable(id="roles_table"), - id="main_roles_container" - ) - - yield ScrollableContainer( - Static("Teams"), - DataTable(id="teams_table"), - id="main_teams_container" - ) - - yield ScrollableContainer( - Static("Blogs"), - DataTable(id="blogs_table"), - Static("Journal"), - DataTable(id="journal_table"), - Static("To-Dos"), - DataTable(id="todos_table"), - id="main_activities_container" - ) - - yield ScrollableContainer( - Static("User Identity"), - DataTable(id="user_identity_table"), - id="main_identities_container" - ) - - yield Footer(id="main_footer") - - def action_show_team(self): - self.log(f"action_show_team, row_key: {self.row_key}, table: {self.table}") - if self.row_key and self.table == "roles_table": - self.app.show_team(self.row_key) - else: - self.notify(f"Team Members can only be displayed by selecting a role in the Roles table first") - - def on_data_table_row_selected(self, event: DataTable.RowSelected): - self.log(f"{event}, event.row_key: {event.row_key}, table: {event.data_table}") - self.row_key = event.row_key - self.table = event.data_table - - def action_edit_selected_table(self): - self.log(f"action_edit_selected_table, row_key: {self.row_key}, table: {self.table}") - if self.table: - self.app.action_edit_selected_table(self.table) - else: - self.notify(f"Please select a table first") - - def action_add_note(self): - self.log(f"action_add_note, row_key: {self.row_key}, table: {self.table}") - if self.row_key and self.table: - self.app.action_add_note(self.row_key, self.table) - else: - self.notify(f"Please select a table and row first") - - def action_show_notes(self): - self.log(f"action_show_notes, row_key: {self.row_key}, table: {self.table}") - if self.row_key and self.table: - self.app.action_show_notes(self.row_key, self.table) - else: - self.notify(f"Please select a table and row first") - diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/README.md b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/README.md deleted file mode 100644 index 09526031..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Parked: pre-merge profile app (Aug 26, 2026) - -These are the `My_Profile/` files as they stood on `pcoldico/egeria-python@main` -(commit `b1fec716`) immediately before `upstream/main` was merged in on -2026-08-26. - -## Why they are here - -`main` and `upstream/main` had each restructured the profile app, differently: - -- **upstream** carries the mixin split (`my_profile_app.py` reduced to ~574 lines, - behaviour moved into `elements_crud_handler.py`, `profile_utils.py`, - `team_roles_handler.py`, `tech_types_handler.py`, `shop_for_data_handler.py`). - That split originated here and was merged upstream as `bc826498`. -- **this fork's `main`** never received the split. It was still the monolithic - `my_profile_app.py` (2467 lines, all behaviour inline) with the Aug 23-26 - feature work layered on top. - -The merge resolved every conflicting file in favour of upstream's modular version, -so the modular architecture is the one that survives. These copies exist so the -Aug 23-26 feature work is not lost and can be ported into the mixins deliberately. - -## What still needs porting - -From `my_profile_app.py` and `EditElementsScreens.py` here: - -- `action_add_community`, `action_delete_community`, `action_remove_link_to_community` - -> `ElementsCrudMixin` -- `action_show_team_members` -> `TeamRolesMixin` -- `on_input_changed` / `on_button_pressed` handlers and `add_community` -- `MainScreen` action renames: `action_show_team`, `action_edit_selected_table`, - `action_add_note`, `action_show_notes` -- reconcile against upstream's generic - `action_edit_table` / `action_show_comments` / `action_add_to_table` dispatch - -`Add_to_Elements_Screens.py` here is a divergent copy of upstream's -`AddToElementsScreens.py` (note the differing name). Upstream's is the one -`my_profile_app.py` imports; diff the two before porting anything from this copy. - -## Full history - -Nothing here is the only copy. The complete pre-merge state is preserved at: - -- branch `backup/main-pre-sync-2026-08-26` -- tag `pre-upstream-sync-2026-08-26` - -Delete this folder once the port is finished. diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/ShowCommentsScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/ShowCommentsScreen.py deleted file mode 100644 index 87180701..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/ShowCommentsScreen.py +++ /dev/null @@ -1,210 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.css.query import NoMatches -from textual.screen import Screen, ModalScreen -from textual.widgets import DataTable, OptionList, Header, Static, Footer, Input, Button -from textual.widgets._option_list import Option - -from pyegeria import Egeria, PyegeriaException, EgeriaTech - - -class ShowCommentsScreen(ModalScreen): - """Main Screen for My Profile App.""" - - BINDINGS = [ - ("q", "quit", "Quit"), - ("ctrl+a", "add_comment", "Add Comment") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, table_name, table_row, view_server, platfgorm_url, user_name, user_password, *args, **kwargs): - super().__init__(*args, **kwargs) - self.table = table_name - self.row = table_row - self.view_server = view_server - self.platform_url = platfgorm_url - self.user_name = user_name - self.user_password = user_password - self.selected_row = "" - self.comment_text = "" - self.comment_type = "" - self.backend_id = "" - - def on_mount(self): - """On mount, find the GUID for the Row """ - self.title = "Egeria - My Profile" - self.sub_title = "Show Comments" - # Extract the correct string identifier - backend_id = self.extract_backend_identifier(self.table, self.row) - self.log(f"Backend identifier: {backend_id} extracted") - if backend_id: - self.notify(f"Accessing backend system with identifier: {backend_id}") - self.backend_id = backend_id - # replace this by using the report format comment-by-element to retrieve linked comments - try: - cclient = Egeria(self.view_server, - self.platform_url, - self.user_name, - self.user_password) - token = cclient.create_egeria_bearer_token(self.user_name, self.user_password) - comments_list = cclient.get_attached_comments(backend_id) - self.log(f"Retrieved: {comments_list} :for element {backend_id}, type: {type(comments_list)}") - if isinstance(comments_list, str) or comments_list == "No elements found": - self.log(f"processing str comment: {comments_list}") - comment_text = str(comments_list) - self.query_one("#show_comments_container", ScrollableContainer).mount(Static(comment_text)) - else: - self.log(f"processing list of: {len(comments_list)} comments") - for comment in comments_list: - comment_text = str(comment) - self.query_one("#show_comments_container", ScrollableContainer).mount(Static(comment_text)) - except PyegeriaException as e: - self.notify(f"No comments found: {str(e)}", severity="warning") - else: - self.notify("Selected table does not contain valid GUID or Qualified Name columns.", severity="error") - - def extract_backend_identifier(self, table_name, row_key): - try: - # Query the table from the current active screen (MainScreen) - # We look for the screen with id 'main_screen' if it's not the current one - try: - target_screen = self.app.get_screen("main_screen") - except KeyError: - target_screen = self.app.get_screen("main") - - table_addr = target_screen.query_one(f"#{table_name}", DataTable) - except (NoMatches, KeyError): - try: - # Fallback to current screen - table_addr = self.app.screen.query_one(f"#{table_name}", DataTable) - except NoMatches: - self.log(f"Table {table_name} not found on any relevant screen") - return None - - # Map the clean string representation of column labels to their index positions - column_mapping = {col.label.plain.strip(): idx for idx, col in enumerate(table_addr.columns.values())} - self.log(f"Column mapping for table {table_name}: {column_mapping}") - - # Case-insensitive check for target columns - upper_mapping = {k.upper(): v for k, v in column_mapping.items()} - idx = None - if "GUID" in upper_mapping: - idx = upper_mapping["GUID"] - elif "QUALIFIED NAME" in upper_mapping: - # use pyegeria to obtain a GUID for that Qualified Name entity and use it as the index (upper) - idx = upper_mapping["QUALIFIED NAME"] - self.log(f"Target column idx value: {idx}") - if idx is None: - self.log(f"Target columns GUID or Qualified Name not found in {list(column_mapping.keys())}") - return None - - self.selected_row = idx - self.log(f"Selected row index: {self.selected_row}") - - try: - # handle if row_key is RowKey object or string - actual_row_key = row_key - row_data = table_addr.get_row(actual_row_key) - self.log(f"Row data: {row_data}") - return str(row_data[idx]) - except Exception as e: - self.log(f"Error retrieving row {row_key} from table {table_name}: {e}") - return None - - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield ScrollableContainer( - Static(f"Show Comments for: {self.table}", id="show_comments_static"), - id="show_comments_container" - ) - yield Footer() - - def action_add_comment(self): - """ Add an attached Comment to thew selected row """ - if self.backend_id: - self.log(f"Selected row: {self.backend_id}") - container = (self.query_one("#show_comments_container", ScrollableContainer)) - container.remove_children() - container.mount(Static(f"[b]Add Comment[/b]")) - container.mount(Input(placeholder="Enter comment", id="add_comment_input")) - container.mount(Static(f"Valid comment types are: Question, Answer, Suggestion and Requirement")) - container.mount(Input(placeholder="Comment Type: Question, Suggestion, etc.", id="add_comment_type_input")) - container.mount(Button("Add", id="add_comment_button")) - else: - self.notify("No row selected, a selected row is required to add a comment!") - - def on_input_changed(self, event: Input.Changed): - self.log(f"Input detected: {event.input.id}, {event.input.value}") - if event.input.id == "add_comment_input": - self.log(f"Input changed: {event.input.value}") - self.comment_text = str(event.input.value) - elif event.input.id == "add_comment_type_input": - self.log(f"Input changed: {event.input.value}") - self.comment_type = str(event.input.value) - else: - self.notify("Input not recognized! Please try again") - return - self.log(f"Comment text: {self.comment_text}, Comment type: {self.comment_type}") - - def on_button_pressed(self, event: Button.Pressed): - if event.button.id == "add_comment_button": - self.log(f"Button pressed: {event.button.id}") - self.log(f"Comment text: {self.comment_text}, Comment type: {self.comment_type}") - if self.comment_text and self.comment_type: - self.add_comment() - else: - self.notify("Both comment text and comment type are required!") - return - else: - self.notify("Button not recognized! Please try again") - - def add_comment(self): - if not self.comment_text or not self.comment_type or not self.backend_id: - self.notify("A row must be selected and Both comment text and comment type are required!") - return - - try: - egeria_tech = EgeriaTech(self.view_server, - self.platform_url, - self.user_name, - self.user_password) - egeria_tech.create_egeria_bearer_token(self.user_name, self.user_password) - except PyegeriaException as e: - self.notify(f"Error creating Egeria Tech object: {e}", timeout=5, severity="error") - self.notify("Unable to connect to the Egeria Server, please check the server is running and your credentials and try again", - timeout=10, - severity="error") - return - - try: - # Add comment to selected element - self.comment_type = self.comment_type.upper() - self.log(f"Adding comment to element: {self.backend_id}, values: {self.comment_text}, {self.comment_type}") - response = egeria_tech.add_comment_to_element( - element_guid=self.backend_id, - comment=self.comment_text, - comment_type=self.comment_type - ) - self.log(f"Comment successfully added! New Comment GUID: {response}") - self.notify(f"Comment successfully added! New Comment GUID: {response}") - container = (self.query_one("#show_comments_container", ScrollableContainer)) - container.remove_children() - rc = 200 - except Exception as e: - self.log(f"Failed to add comment: {e}") - self.notify(f"Failed to add comment: {e} \n Please try again.") - rc = 400 - self.dismiss() - - def action_quit(self): - self.app.pop_screen() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/TechnologyTypeScreens.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/TechnologyTypeScreens.py deleted file mode 100644 index c0c5aa40..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/TechnologyTypeScreens.py +++ /dev/null @@ -1,662 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from typing import Any - -from textual.widgets._tree import TreeNode - -from pyegeria import load_app_config, settings -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Container -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Static, OptionList, Button, Footer, Input, TextArea, Tree -from textual.widgets._option_list import Option - - -class TechnologyTypeOptionsScreen(ModalScreen): - """ Modal screen to display a technology type's templates and processes.""" - BINDINGS = [("q", "dismiss", "Quit"), - ("b", "back", "Go_Back"), - # ("ctl+s", "Select", "tech_type_option_select") - ] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, tech_type_guid: str, - tech_type_name: str, - tech_type_description: str, - user_name: str, - user_pwd:str, - user_kpts: int, - tech_type_templates: list[dict], - tech_type_processes: list[dict]) -> None: - """Initialize the TechnologyTypeOptions screen with a technology type's templates and processes.""" - super().__init__() - self.selected_process_index = None - self.tech_type_guid = tech_type_guid - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.user_name = user_name - self.user_password = user_pwd - self.karma_points = user_kpts - self.tech_type_templates = tech_type_templates - self.tech_type_processes = tech_type_processes - self.selected_template_guid = None - self.selected_process_guid = None - self.option_type_selected = None - self.selected_template = None - self.selected_template_index = None - self.selected_template_data = None - self.selected_process = None - self.selected_process_data = None - - load_app_config("config/config.json") - app_config = settings.Environment - app_user = settings.User_Profile - - if not self.user_name: - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - - def compose(self) -> ComposeResult: - - """ Compose the UI components for the TechnologyTypeOptions screen.""" - yield Header(show_clock=True) - yield Static(f"Description: {self.tech_type_description}") - yield ScrollableContainer( - Static(f"Technology Type: {self.tech_type_name}"), - Container( - Static("Select a template or process to continue."), - Static(f"Templates", id="number_of_templates_label"), - ScrollableContainer( - Static("Available Templates", id="template_options_label"), - OptionList(id="template_options"), - Button("Select Template", id="select_template_btn"), - id="template_options_container"), - Static(f"Processes:", id="number_of_processes_label"), - ScrollableContainer( - Static("Available Processes",id="process_options_label"), - OptionList(id="process_options"), - id="process_options_container"), - Button("Select Process", - id="select_process_btn"))) - yield Footer() - - async def on_mount(self) -> int: - """Mount the TechnologyTypeOptions screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - self.log(f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}") - - for widget in self.query(): - self.log(f"Widgets: {widget}") - - self.log(f"Templates: {self.tech_type_templates}, Processes: {self.tech_type_processes}") - self.log(f"Templates Type: {type(self.tech_type_templates)}, Processes Type: {type(self.tech_type_processes)}") - - if self.tech_type_templates and self.tech_type_templates != "None": - for t in self.tech_type_templates: - try: - self.log(f"Template: {t}") - templates = self.query_one("#template_options", OptionList).add_option( - Option(t.get("displayName"))) - self.log(f"Added option: {t.get('Catalog Template Name')}") - await templates.mount(after=self.query_one("#template_options_label")) - self.log(f"Mounted option") - self.query_one("#select_template_btn", Button).disabled = False - self.log(f"Enabled the template button") - except NoMatches as e: - try: - templates: OptionList = OptionList(id="template_options") - await templates.mount(after=self.query_one("#template_options_label")) - templates.add_option(Option(t.get("displayName"))) - templates.refresh() - self.query_one("#select_template_btn", Button).disabled = False - continue - except Exception as e: - self.log(f"Error creating template option list: {e}, (410") - return (410) - except Exception as e: - self.log(f"Error creating template option list: {e} (411)") - return (411) - else: - continue - else: - try: - self.log(f" No Templates") - templates = self.query_one("#template_options", OptionList).add_option( - Option("No Templates Found for this Tech Type")) - await templates.mount(after=self.query_one("#template_options_label")) - self.query_one("#select_template_btn", Button).disabled = True - except NoMatches as e: - try: - templates: OptionList = OptionList(id="template_options") - await templates.mount(after=self.query_one("#template_options_label")) - templates.add_option(Option("No Templates found for this Tech Type")) - self.query_one("#select_template_btn", Button).disabled = True - except Exception as e: - self.log(f"Error creating template option list: {e}") - return (409) - except Exception as e: - self.log(f"Error creating template option list: {e}") - return (408) - - if self.tech_type_processes and self.tech_type_processes != "None": - for p in self.tech_type_processes: - try: - self.log(f"Process: {p}") - processes = self.query_one("#process_options", OptionList).add_option( - Option(p.get("displayName"))) - self.log(f"Process option added: {p.get('displayName')}") - await processes.mount(after=self.query_one("#process_options_label")) - self.query_one("#select_process_btn", Button).disabled = False - self.log(f"Enabled the process button") - except NoMatches as e: - try: - processes: OptionList = OptionList(id="process_options") - await processes.mount(after=self.query_one("#process_options_label")) - processes.add_option(Option(p.get("displayName"))) - processes.refresh() - self.query_one("#select_process_btn", Button).disabled = False - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (407) - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (406) - else: - continue - else: - try: - self.log(f" No Processes") - processes = self.query_one("#process_options", OptionList).add_option( - Option("No processes found for this Tech Type")) - await processes.mount(after=self.query_one("#process_options_label")) - self.query_one("#select_process_btn", Button).disabled = True - except NoMatches as e: - try: - processes: OptionList = OptionList(id="process_options") - await processes.mount(after=self.query_one("#process_options_label")) - processes.add_option(Option(" No Processes found for this Tech Type")) - self.query_one("#select_process_btn", Button).disabled = True - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (405) - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (404) - - return(200) - - @on(OptionList.OptionHighlighted, "#template_options") - def handle_template_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: - """Handle the highlighting of an option in the template option list.""" - self.log(f"Template option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.selected_template_index = event.option_index - selected_option = event.option - self.selected_template = selected_option.prompt - self.log(f"Highlighted Template: {self.selected_template}, index: {self.selected_template_index}") - - @on(OptionList.OptionSelected, "#template_options") - def handle_template_option_selected(self, event: OptionList.OptionSelected) -> None: - """Handle the selection of an option from the template or process option lists.""" - self.log(f"Template selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.option_type_selected = event.option_list.name - self.selected_template_index = event.option_index - selected_option = event.option - self.selected_template = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_template}, index: {self.selected_template_index}") - - @on(OptionList.OptionHighlighted, "#process_options") - def handle_process_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: - """Handle the highlighting of an option in the process option list.""" - self.log(f"Process option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.selected_process_index = event.option_index - selected_option = event.option - self.selected_process = selected_option.prompt - self.log(f"Highlighted Process: {self.selected_process}, index: {self.selected_process_index}") - - @on(OptionList.OptionSelected, "#process_options") - def handle_process_option_selected(self, event: OptionList.OptionSelected) -> None: - """Handle the selection of an option from the template or process option lists.""" - self.log(f"Process selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.option_type_selected = event.option_list.name - self.selected_process_index = event.option_index - selected_option = event.option - self.selected_process = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_process}, index: {self.selected_process_index}") - - def action_go_back(self) -> None: - """ Handle the back key press.""" - self.dismiss("back") - - def action_Quit(self) -> None: - """ Handle the quit key press""" - self.dismiss(200) - - @on(Button.Pressed, "#select_template_btn") - def handle_template_selected(self, event: Button.Pressed) -> None: - """Handle the selection of a template option.""" - if self.selected_template is None: - return - self.log(f"Template option selected: {self.selected_template}") - self.dismiss(["template", self.selected_template]) - - @on(Button.Pressed, "#select_process_btn") - def handle_process_selected(self, event: Button.Pressed) -> None: - """Handle the selection of a process option.""" - if self.selected_process is None: - return - self.log(f"Process option selected: {self.selected_process}") - self.dismiss(["process", self.selected_process]) - - -class TechnologyTypeProcessesScreen(ModalScreen[Any]): - """Modal screen to display technology type processes in Egeria.""" - BINDINGS = [("q", "quit", "Quit"), - ("b", "back", "Go back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "my_profile.tcss" - - def __init__ (self, - user_name: str, - user_kpts: int, - tech_type_name: str, - tech_type_description: str, - selected_t_option, - tech_type_option_selected, - tech_type_processes - ) -> None: - """Initialize the TechnologyTypeProcesses screen.""" - super().__init__() - self.user_name = user_name - self.karma_points = user_kpts - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.selected_t_option = selected_t_option - self.selected_t_option_selected = tech_type_option_selected - self.tech_type_processes = tech_type_processes - self.full_process = None - self.selected_process = None - load_app_config() - - async def on_mount(self) -> None: - """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - - if self.selected_t_option == "process": - self.log(f"Processing processes, with data: {self.selected_t_option_selected}") - # get selected process from the tech_type data - self.log(f"Technology Type Process: {self.tech_type_processes}") - if isinstance(self.tech_type_processes, list): - for process in self.tech_type_processes: - self.log(f"Process: {process.get("displayName")}, Selected: {self.selected_t_option_selected}") - if self.selected_t_option_selected == process.get("displayName"): - self.full_process = process - self.selected_t_process = process - self.log(f"Selected Process: {self.selected_t_process}") - break - else: - continue - self.log(f"Selected Process: {self.selected_t_process}, type: {type(self.selected_t_process)}") - - if self.selected_t_process is None: - self.log("No process selected, skipping placeholder display") - return - else: - specification = self.selected_t_process.get("specification") - if specification is None: - self.log("Selected process has no specification, skipping placeholder display") - return - else: - placeholderProperties = specification.get("supportedRequestParameter") - if placeholderProperties is None: - self.log("Selected process has no req uest parameters, skipping display") - return - else: - for parameter in placeholderProperties: - if parameter.get("class") != "SupportedRequestParameter": - continue - else: - name = parameter.get("name") or None - Description = parameter.get("description") or None - Type = parameter.get("dataType") - Example = parameter.get("example") - Required = parameter.get("required") - # governance_action_steps = self.full_process.get("Governance Action Steps") or None - - # Sanitize the name for use as a CSS ID - safe_name = name.replace(" ", "_") if name else f"placeholder_{id(self.selected_t_process)}" - safe_name = safe_name.replace("::", "_") - safe_name = safe_name.replace(":", "_") - self.log(f"Safe name = {safe_name}") - placeholder_text: TextArea = TextArea( - f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", - id=f"{safe_name}_placeholder_text_area", - read_only=True - ) - # Ensure TextArea is visible - placeholder_text.styles.height = 8 - - placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") - - # Mount the TextArea and the associated Input field into the ScrollableContainer - try: - load_point = self.query_one("#technology_type_processes_input") - await load_point.mount(placeholder_text, before="#process_submit_button") - await load_point.mount(placeholder_input, before="#process_submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") - continue - except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") - self.app.dismiss(416) - - - def compose(self) -> ComposeResult: - """ Compose the UI components for the Technology_Type_Processes screen.""" - yield Header(show_clock=True) - yield Static("Please complete the required fields and any optional fields you prefer:") - yield ScrollableContainer( - Static("Technology Type Process Input"), - Button("Submit", variant="primary", id="process_submit_button"), - id="technology_type_processes_input" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200", ) - - @on(Button.Pressed, "#process_submit_button") - def handle_submit_button_pressed(self, event: Button.Pressed) -> None: - """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") - save_input_data:dict = {} - for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") - save_input_data.update({input_widget.id: input_widget.value}) - self.log(f"Save input data: {save_input_data}") - self.dismiss(["input", save_input_data, self.full_process]) - - @on(Input.Changed, "#technology_type_processes_input") - def handle_input_changed(self, event: Input.Changed) -> None: - """The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") - - -class TechnologyTypesScreen(ModalScreen): - """Modal screen to display technology types in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("g", "go_back", "Back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "my_profile.tcss" - - def __init__(self, ttlist, user_name, user_pwd, user_kpts): - """Initialize the TechnologyTypes screen with a list of technology types.""" - super().__init__() - self.tech_type_list = ttlist - self.user_name = user_name - self.user_password = user_pwd - self.karma_points = user_kpts - self.tech_type_tree: Tree[str] = Tree(label="Technology Types", id="technology_types_tree") - self.tech_type_tree.root.expand() - self.tech_type_tree.auto_expand = True - self.selected_t_node = None - self.selected_t_node_label = None - self.node_id = None - self.node_status = "expanded" - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}" - self.sub_title = "Select a technology type" - - def compose(self) -> ComposeResult: - """ Compose and display the technology type screen""" - self.tech_type_tree.refresh() - if self.tech_type_list: - self.log(f"Technology types: {self.tech_type_list}, type: {type(self.tech_type_list)}") - self.render_tech_type_hierarchy_to_tree(self.tech_type_list, self.tech_type_tree) - else: - self.tech_type_tree.root.add("No technology types found", expand=True) - self.tech_type_tree.refresh() - self.log(f"Technology types tree: {self.tech_type_tree}") - - yield Header(show_clock=True) - yield ScrollableContainer( - Static("Display technology types in Egeria"), - self.tech_type_tree, - # Button("Select", id="select_tech_type_btn"), - id="technology_types_table" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200") - - def action_go_back(self) -> None: - """ The back option in the footer has been selected. Dismiss the screen.""" - self.dismiss("201") - - @on(Tree.NodeSelected) - def handle_tree_node_selected(self, event: Tree.NodeSelected) -> None: - """ The user has selected a node in the tree. """ - self.log(f"Tree node selected, node: {event.node}") - self.selected_t_node = event.node - self.selected_t_node_label = event.node.label - self.log(f"Selected node: {self.selected_t_node}, label: {self.selected_t_node.label}") - self.dismiss(str(self.selected_t_node.label)) - - @on(Tree.NodeCollapsed) - def handle_tree_node_collapsed(self, event: Tree.NodeCollapsed) -> None: - # handle the twisty to close a node in the tree - # logger.debug(f"TreeNodeCollapsed: {event.node.id}") - self.node_id = str(event.node.id) - self.node_status = "collapsed" - - @on(Tree.NodeExpanded) - def handle_tree_node_expanded(self, event: Tree.NodeExpanded) -> None: - # handle the twisty to open a node in the tree - # logger.debug(f"TreeNodeExpanded: {event.node.id}") - self.node_id = str(event.node.id) - self.node_status = "expanded" - - def action_expand(self): - # handle the hot key to expand/collapse all nodes in the tree (see bindings) - self.tech_type_tree = self.query_one("#technology_types_tree", Tree) - if self.node_status == "collapsed": - self.tech_type_tree.root.expand_all() - else: - self.tech_type_tree.root.collapse_all() - return - - def render_tech_type_hierarchy_to_tree(self, - data: dict | list[dict], - tree_or_node: Tree[str] | TreeNode[str], - label_attr: str = "displayName", - guid_attr: str = "guid", - children_attr: str = "subTypes" - ) -> None: - """Recursively render a DICT) hierarchy into a Textual Tree. - - Args: - data: The data structure from pyegeria.get_tech_type_hierarchy. - tree_or_node: The Tree object or a TreeNode to add children to. - label_attr: The attribute in the data for the node label. - guid_attr: The attribute in the data for the node data (GUID). - children_attr: The attribute in the data containing the list of children. - """ - if isinstance(data, list): - for item in data: - self.render_tech_type_hierarchy_to_tree(item, tree_or_node, label_attr, guid_attr, children_attr) - return - - if not isinstance(data, dict): - return - - label = str(data.get(label_attr) or data.get("Display Name") or "Unknown") - guid = str(data.get(guid_attr) or data.get("GUID") or "") - - # If it's a Tree, we add to root. If it's a TreeNode, we add to it. - if isinstance(tree_or_node, Tree): - node = tree_or_node.root.add(label, data=guid, expand=True) - else: - node = tree_or_node.add(label, data=guid, expand=True) - - children = data.get(children_attr) - if children and isinstance(children, list): - for child in children: - self.render_tech_type_hierarchy_to_tree(child, node, label_attr, guid_attr, children_attr) - - -class TechnologyTypeTemplatesScreen(ModalScreen[Any]): - """Modal screen to display technology type templates in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "back", "Go back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "my_profile.tcss" - - def __init__ (self, - user_name: str, - user_kpts: int, - tech_type_name: str, - tech_type_description: str, - selected_t_option, - tech_type_option_selected, - tech_type_templates - ) -> None: - """Initialize the Technology_Type_Templatess screen.""" - super().__init__() - self.user_name = user_name - self.karma_points = user_kpts - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.selected_t_option = selected_t_option - self.selected_t_option_selected = tech_type_option_selected - self.tech_type_templates = tech_type_templates - self.full_template = None - self.selected_t_template = None - load_app_config() - - async def on_mount(self) -> None: - """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - - if self.selected_t_option == "template": - self.log(f"Processing templates, with data: {self.selected_t_option_selected}") - # get selected template from the tech_type data - self.log(f"Technology Type Templates: {self.tech_type_templates}") - if isinstance(self.tech_type_templates, list): - for template in self.tech_type_templates: - self.log(f"Template: {template.get('displayName')}, Selected: {self.selected_t_option_selected}") - if self.selected_t_option_selected == template.get("displayName"): - self.full_template = template - self.selected_t_template = template - self.log(f"Selected Template: {self.selected_t_template}") - break - else: - continue - self.log(f"Selected Template: {self.selected_t_template}") - - if self.selected_t_template is None: - self.log("No template selected, skipping placeholder display") - return - else: - specification = self.selected_t_template.get("specification") - if specification is None: - self.log("Selected template has no specification, skipping placeholder display") - return - else: - placeholderProperties = specification.get("placeholderProperty") - if placeholderProperties is None: - self.log("Selected template has no placeholder properties, skipping display") - return - else: - for placeholder in placeholderProperties: - if placeholder.get("class") != "PlaceholderProperty": - continue - else: - name = placeholder.get("name") or None - Description = placeholder.get("description") or None - Type = placeholder.get("dataType") or None - Example = placeholder.get("example") or None - Required = placeholder.get("required") or False - - # Sanitize the name for use as a CSS ID - safe_name = name.replace(" ", "_") if name else f"placeholder_{id(placeholder)}" - placeholder_text: TextArea = TextArea( - f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", - id=f"{safe_name}_placeholder_text_area", - read_only=True - ) - # Ensure TextArea is visible - placeholder_text.styles.height = 8 - - placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") - - # Mount the TextArea and the associated Input field into the ScrollableContainer - try: - load_point = self.query_one("#technology_type_templates_input") - await load_point.mount(placeholder_text, before="#template_submit_button") - await load_point.mount(placeholder_input, before="#template_submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") - continue - except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") - self.app.dismiss(416) - - def compose(self) -> ComposeResult: - """ Compose the UI components for the Technology_Type_Templatess screen.""" - yield Header(show_clock=True) - yield Static("Please complete the required fields and any optional fields you prefer:") - yield ScrollableContainer( - Static("Technology Type Template Input"), - Button("Submit", id="template_submit_button"), - id="technology_type_templates_input" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200", ) - - @on(Button.Pressed, "#template_submit_button") - def handle_submit_button_pressed(self, event: Button.Pressed) -> None: - """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") - save_input_data: dict = {} - for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") - save_input_data.update({input_widget.id: input_widget.value}) - self.log(f"Save input data: {save_input_data}") - self.dismiss(["input", save_input_data, self.full_template]) - - @on(Input.Changed, "#technology_type_templates_input") - def handle_input_changed(self, event: Input.Changed) -> None: - """ The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/__init__.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/my_profile_app.py b/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/my_profile_app.py deleted file mode 100644 index b94cc2d9..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/My_Profile_parked_aug26/my_profile_app.py +++ /dev/null @@ -1,2468 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -import datetime -import re -import json -import sys -from pathlib import Path -from typing import Any - -# Add the project root to sys.path to allow running this script from any directory -root_path = Path(__file__).resolve().parents[4] -if str(root_path) not in sys.path: - sys.path.append(str(root_path)) - -from pyegeria import load_app_config, settings, MyProfile, PyegeriaException, print_basic_exception, exec_report_spec, \ - AutomatedCuration, ProductManager, \ - PyegeriaInvalidParameterException, PyegeriaAPIException, DataEngineer -from pyegeria import Egeria -from textual import on -from textual.app import App, ComposeResult -from textual.containers import ScrollableContainer -from textual.widgets import DataTable, OptionList, Header, Static, Footer, Tree - -from CreateProfileScreen import CreateProfileScreen -from ShowCommentScreen import ShowCommentScreen -from TechnologyTypeScreens import (TechnologyTypesScreen, - TechnologyTypeOptionsScreen, - TechnologyTypeTemplatesScreen, - TechnologyTypeProcessesScreen) -from StatusScreen import StatusScreen -from ShopForDataScreen import ShopForDataScreen -from SelectionOverviewScreen import SelectionOverviewScreen -from MyTeam import MyTeam -from MainScreen import MainScreen -from SearchForTermScreen import SearchForTermScreen -from CreateSubscriptionRequestScreen import CreateSubscriptionRequestScreen -from UserIdentitiesScreen import UserIdentitiesScreen -from Add_to_Elements_Screens import (AddCommentScreen, - AddUserIdentityScreen, - AddJournalEntryScreen, - AddTeamScreen, - AddBlogEntryScreen, - AddTodosScreen, - AddCommunityScreen, - AddProjectScreen, - AddRoleScreen, - AddCommentScreen) - -from EditElementsScreens import (EditIdentitiesScreen, - EditTeamsScreen, - EditCommunitiesScreen, - EditRolesScreen, - EditProfileScreen, - EditIdentitiesScreen, - EditAssociationsScreen, - EditProjectsScreen, - EditBlogsScreen, - EditActivityScreen, - EditJournalScreen, - EditTodosScreen,) - -class MyProfileApp(App): - """My Profile App. - - Retrieves a user's profile from Egeria and displays current work items. - If no profile is found, offers a UI to create one. - """ - - BINDINGS = [("q", "quit", "Quit"), - ("r", "refresh", "Refresh Data"), - ] - - CSS_PATH = "my_profile.tcss" - - SCREENS = { - "main": MainScreen, - "_default": MainScreen, - "create_profile": CreateProfileScreen, - "edit_profile": EditProfileScreen, - "edit_communities": EditCommunitiesScreen, - "add_comment": AddCommentScreen, - "show_comment": ShowCommentScreen, - "edit_associations": EditAssociationsScreen, - "edit_activity": EditActivityScreen, - "edit_identities": EditIdentitiesScreen, - "edit_roles": EditRolesScreen, - "edit_teams": EditTeamsScreen, - "tech_types": TechnologyTypesScreen, - "tech_type_options": TechnologyTypeOptionsScreen, - "tech_type_templates": TechnologyTypeTemplatesScreen, - "tech_type_processes": TechnologyTypeProcessesScreen, - "status": StatusScreen, - "shop_4_data": ShopForDataScreen, - "search_for_term": SearchForTermScreen, - "overview": SelectionOverviewScreen, - "create_subscription": CreateSubscriptionRequestScreen, - "my_team": MyTeam - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.contribution_record = None - self.heading = "My_Profile" - self.subheading = "Egeria Profile for current user" - self.description = "Display the user related items for the current user." - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - print("Platform:", app_config.egeria_platform_url) - print("View Server:", app_config.egeria_view_server) - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - - # Ensure compose() is safe before data loads - self.actor_profile: dict = {} - self.projects = [] - self.communities = [] - self.roles = [] - self.actions = [] - self.teams = [] - self.other_function_list = [] - self.tech_type_json: str = "" - self.tech_type_response = None - self.tech_type_list = [] - self.tech_type_guid = "" - self.tech_type_name = "" - self.tech_type_description = "" - self.selected_t_node = None - self.selected_t_node_label = None - self.karma_points = 0 - self.tech_type_templates = [{}] - self.tech_type_processes = [{}] - self.full_template = None - self.glossary_data_extract = None - self.business_glossary_data_extract = None - self.display_glossary_data_extract = None - self.digital_glossary_data_extract = None - self.team_members: list[list] = [] - self.max_mermaid_node_count = 0 # This is to tell egeria we dont want mermaid graphs in the response packet. - self.graph_query_depth = 0 # This tell egeria not to include relationships in the response packet - self.user_GUID = "" - self.user_data = {} - self.row_guid = "" - - def compose(self) -> ComposeResult: - yield Header() - yield Footer() - - async def on_mount(self) -> None: - """Load profile; if missing, prompt to create it; then populate tables.""" - # DOMInfo.attach_to(self) - - await self._load_or_create_profile() - await self._populate_tables() - - - async def _load_or_create_profile(self) -> None: - try: - #instantiate the class - self.my_profile_inst = MyProfile(self.view_server, self.platform_url, self.user_name, self.user_password) - self.my_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) - #retrieve the profile - self.my_profile_data = await self.my_profile_inst._async_get_my_profile( - report_spec="My-User-MD", - output_format="DICT", - ) - self.log(f"retrieve profile result: {self.my_profile_data}") - except (PyegeriaException) as e: - self.log(f"Error retrieving profile: {e!s}") - print_basic_exception(e) - self.exit(402) - return - - if self.my_profile_data == []: - self.log(f"Error retrieving profile. Prompting to create one...") - self.log(f"To create a profile you must have a valid userid in the system, please contact your system administrator to create one if needed") - await self.push_screen(CreateProfileScreen( - self.user_name, - self.user_password, - self.view_server, - self.platform_url, - ), callback = self.new_profile_return) - else: - self.new_profile_return(200) - - def new_profile_return(self, result: int) -> None: - """ This function handles either the return from the create new profile screen or - when the user already has a profile continue processing """ - self.log(f"Profile creation result: {result}") - if not result or result != 200: - self.log(f"Profile creation cancelled/failed; return: {result}, exiting.") - self.exit(403) - return - - self.result = result - - # Retry after creation if necessary - try: - self.user_profile_struct = self.my_profile_inst.get_my_profile( - output_format="DICT", - report_spec="My-User-MD", - ) - self.log(f"Profile retrieved successfully: {self.user_profile_struct}") - self.push_screen("main") - except PyegeriaException as e2: - self.log(f"Error retrieving user profile: {e2!s}") - self.exit(412) - return - if self.user_profile_struct is []: - self.log(f"Error retrieving user profile. Exiting.") - self.exit(413) - return - - # strip out the individual profile elements - self.user_profile = self.user_profile_struct[0] - self.contribution_record = self.user_profile.get("Contribution Record") or {} - for c in self.contribution_record: - self.karma_points = c.get("Karma Points") or 0 - break - self.my_projects_data = self.user_profile.get("Projects") or [] - self.my_teams_data = self.user_profile.get("Teams") or [] - self.my_communities_data = self.user_profile.get("Communities") or [] - self.my_roles_data = self.user_profile.get("Roles") or [] - self.my_actions_data = self.user_profile.get("Actions") or [] - self.log(f"Contribution Record: {self.contribution_record}") - self.log(f"Karma Points: {self.karma_points}") - self.log(f"my_projects_data: {self.my_projects_data}") - self.log(f"my_teams_data: {self.my_teams_data}") - self.log(f"my_communities_data: {self.my_communities_data}") - self.log(f"my_roles_data: {self.my_roles_data}") - self.log(f"my_actions_data: {self.my_actions_data}") - # User Identities - try: - self.user_identities = self.my_profile_inst.get_my_profile( - report_spec="User-Identities", - output_format="DICT", - ) - self.log(f"User-Identities: {self.user_identities}, type: {type(self.user_identities)}") - except PyegeriaException as e: - self.log(f"Error retrieving User-Identities: {e!s}") - self.user_identities = {} - - # User GUID — resolve self-scoped; never let a missing/empty lookup crash - # the app (this runs in on_mount, before first paint, so an unguarded - # TypeError here kills the whole session). - self.user_GUID = "" - # Preferred: the current user's own profile already carries its GUID when - # the My-User-MD format set includes it — no directory search required. - if isinstance(self.user_profile, dict) and self.user_profile.get("GUID"): - self.user_GUID = self.user_profile.get("GUID") - else: - # Fallback: actor-profile directory lookup. Egeria's Actor-Profiles - # search matches the display *name*, so a userId search legitimately - # returns {'kind': 'empty'} → guard against None / empty data. - try: - actor = exec_report_spec(format_set_name="Actor-Profiles", - output_format="DICT", - params={"search_string": self.user_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving actor profile: {e!s}") - actor = None - data = actor.get("data") if isinstance(actor, dict) else None - if data: - self.user_GUID = data[0].get("GUID") or "" - else: - self.log("Actor-Profiles lookup returned no data; user_GUID left unset.") - self.log(f"User GUID retrieved: {self.user_GUID!r}") - - # Normalize expected keys - self.full_name = self.user_profile.get("Full Name") or "" - self.sub_title = f"{self.full_name} ({self.user_profile.get('User ID')}, Karma Points: {self.karma_points})" - self.projects = self.my_projects_data or self.user_profile.get("Projects") or [] - self.communities = self.my_communities_data or [] - self.roles = self.my_roles_data or [] - self.blogs = self.user_profile.get("Blogs") or [] - self.activity = self.user_profile.get("Activity") or [] - self.journal = self.user_profile.get("Journal") or [] - # self.actions = self.user_profile.get("actions") or [] - self.teams = self.my_teams_data or [] - if isinstance(self.user_identities, list): - self.user_identity = self.user_identities - else: - self.user_identity = self.user_identities.get("User-Identities") or [] - - async def _populate_tables(self) -> None: - """Populates tables from normalized profile data""" - main_screen = self.get_screen("main") - self.associations_table = main_screen.query_one("#associations_table", DataTable) - self.projects_table = main_screen.query_one("#projects_table", DataTable) - self.communities_table = main_screen.query_one("#communities_table", DataTable) - self.roles_table = main_screen.query_one("#roles_table", DataTable) - self.blog_table = main_screen.query_one("#blog_table", DataTable) - self.blog_entry_table = (DataTable(id="blog_entry_table")) - self.activity_table = main_screen.query_one("#activity_table", DataTable) - self.journal_table = main_screen.query_one("#journal_table", DataTable) - # self.actions_table = main_screen.query_one("#actions_table", DataTable) - self.user_identity_table = main_screen.query_one("#user_identity_table", DataTable) - self.teams_table = main_screen.query_one("#teams_table", DataTable) - - self.projects_table.clear(columns=True) - self.projects_table.add_columns("Project Name", "Description", "Qualified Name") - self.projects_table.zebra_stripes = True - self.projects_table.cursor_type = "row" - - self.communities_table.clear(columns=True) - self.communities_table.add_columns("Assignment Type", "Community Name", "Description", "GUID") - self.communities_table.zebra_stripes = True - self.communities_table.cursor_type = "row" - - self.roles_table.clear(columns=True) - self.roles_table.add_columns("Role Name", "Role Type","Description", "GUID") - self.roles_table.zebra_stripes = True - self.roles_table.cursor_type = "row" - - self.teams_table.clear(columns=True) - self.teams_table.add_columns("Assignment Type", "Team Name", "Description","GUID") - self.teams_table.zebra_stripes = True - self.teams_table.cursor_type = "row" - - self.journal_table.clear(columns=True) - self.journal_table.add_columns("Journal Entry", "Date", "Description") - self.journal_table.zebra_stripes = True - self.journal_table.cursor_type = "row" - - self.blog_table.clear(columns=True) - self.blog_table.add_columns("Blog Title", "Date", "Description") - self.blog_table.zebra_stripes = True - self.blog_table.cursor_type = "row" - - self.blog_entry_table.clear(columns=True) - self.blog_entry_table.add_columns("Blog Entry Title", "Date", "Description") - self.blog_entry_table.zebra_stripes = True - self.blog_entry_table.cursor_type = "row" - - self.activity_table.clear(columns=True) - self.activity_table.add_columns("Activity Name", "Date", "Description") - self.activity_table.zebra_stripes = True - self.activity_table.cursor_type = "row" - - self.user_identity_table.clear(columns=True) - self.user_identity_table.add_columns("Display Name", "User ID", "Distinguished Name") - self.user_identity_table.zebra_stripes = True - self.user_identity_table.cursor_type = "row" - - self.associations_table.clear(columns=True) - self.associations_table.add_columns("Association Type", "Name", "Description", "Qualified Name") - self.associations_table.zebra_stripes = True - self.associations_table.cursor_type = "row" - - # Populate rows - for p in self.projects if isinstance(self.projects, list) else []: - self.projects_table.add_row( - str(p.get("Name", "")), - str(p.get("Description", "")), - str(p.get("Qualified Name", "")), - ) - - for c in self.communities if isinstance(self.communities, list) else []: - self.communities_table.add_row( - str(c.get("Assignment Type", "")), - str(c.get("Name", "")), - str(c.get("Description", "")), - str(c.get("GUID", "")) - ) - - for r in self.roles if isinstance(self.roles, list) else []: - self.roles_table.add_row( - str(r.get("Name", "")), - str(r.get("Type", "")), - str(r.get("Description", "")), - str(r.get("GUID", "")), - ) - - for b in self.blogs if isinstance(self.blogs, list) else []: - self.blog_table.add_row( - str(b.get("title", "")), - str(b.get("time", "")), - str(b.get("text", "")), - ) - - for j in self.journal if isinstance(self.journal, list) else []: - self.journal_table.add_row( - str(j.get("title", "")), - str(j.get("time", "")), - str(j.get("text", "")), - ) - - for a in self.activity if isinstance(self.activity, list) else []: - self.activity_table.add_row( - str(a.get("title", "")), - str(a.get("time", "")), - str(a.get("text", "")), - ) - - for t in self.teams if isinstance(self.teams, list) else []: - self.teams_table.add_row( - str(t.get("Assignment Type", "")), - str(t.get("Name", "")), - str(t.get("Description", "")), - str(t.get("GUID", "")), - ) - - self.associations_table.add_row("Projects", "", "") - for ap in self.projects if isinstance(self.projects, list) else []: - self.associations_table.add_row( - str(ap.get("Name", "")), - str(ap.get("Description", "")), - str(ap.get("Qualified Name", "")), - ) - self.associations_table.add_row("Communications", "", "") - for ac in self.communities if isinstance(self.communities, list) else []: - self.associations_table.add_row( - str(ac.get("Assignment Type", "")), - str(ac.get("Name", "")), - str(ac.get("Description", "")), - str(ac.get("GUID", "")) - ) - - def action_quit(self) -> Any: - # quit selected by user, so exit app - self.exit(200) - - async def action_refresh(self) -> None: - # refresh requested by User - self.log("Refreshing data...") - await self._load_or_create_profile() - await self._populate_tables() - self.log("Data refresh completed.") - - @on(DataTable.RowSelected, "#communities_table") - async def handle_community_table_row_selected(self, event: DataTable.RowSelected): - self.log(f"Community table row selected: {event.row_key}") - self.sel_row_key = event.row_key - main_screen = self.get_screen("main") - self.communities_table=main_screen.query_one("#communities_table", DataTable) - selected_row = self.communities_table.get_row(event.row_key) - self.log(f"Selected Community row: {selected_row}") - selected_GUID = selected_row[3] - self.log(f"Selected Community GUID: {selected_GUID}") - try: - fclient=Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) - token = fclient.create_egeria_bearer_token(self.user_name, self.user_password) - attached_messages=(fclient.get_attached_comments(selected_GUID)) - except(PyegeriaException) as e: - self.log(f"Error retrieving attached messages : {e}") - attached_messages = None - await self.push_screen(ShowCommentScreen(selected_GUID, attached_messages), callback=self.show_comment_callback) - - def add_comment_callback(self, result): - self.log(f"Comment to be added: {result}") - self.result = result - self.community_GUID = result[0] - self.comment = result[1] - fclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) - token = fclient.create_egeria_bearer_token(self.user_name, self.user_password) - addresult = fclient.add_comment_to_element(self.community_GUID, self.comment) - self.log(f"Comment added: {addresult}") - self.refresh() - - async def show_comment_callback(self, result): - self.result = result - self.rc = result[0] - self.selected_GUID = result[1] - if self.rc != 200: - return(self.rc) - else: - if self.selected_GUID: - await self.push_screen(AddCommentScreen(self.selected_GUID), callback=self.add_comment_callback) - else: - return(self.rc) - - - @on(OptionList.OptionSelected, "#other_function_list") - async def handle_option_selected(self, event: OptionList.OptionSelected): - """ Process the selected option from the Other Functions OptionList. """ - # option selected by user - selected_option = event.option.prompt - selected_option_id = event.option.id - self.log(f"Selected option: {selected_option} ({selected_option_id})") - if selected_option == "Technology Types": - self.log("Fetching technology types...") - await self.fetch_technology_types() - self.log(f"Tech Type Response: {self.tech_type_response} | {self.tech_type_list}") - if self.tech_type_response == []: - self.log("No technology types found.") - self.exit(200) - elif len(self.tech_type_response) == 3 and int(self.tech_type_response) >= 400: - self.log("Error fetching technology types.") - self.exit(int(self.tech_type_response)) - self.log("Technology types fetched successfully.") - self.log("Displaying technology types...") - await self.push_screen(TechnologyTypesScreen(self.tech_type_list, - self.user_name, - self.user_password, - self.karma_points), - callback=self.tech_type_callback) - self.log("Technology types displayed successfully.") - elif selected_option == "User Identities": - await self.push_screen(UserIdentitiesScreen(self.user_name, - self.user_password, - self.karma_points, - self.user_identities), - callback=self.user_identities_callback) - elif selected_option == "Catalogs/Shop for Data": - """ Push new Screen, Show Glossaries, Digital Product Catalogs, Data Dictionaries and - Business Domains, allow the user to select from one of the 4 categories and use that selection to - display a list of available collections of the chosen type and allow the user to subscribe to them - """ - - # start by gathering the data using Pyegeria to access the Egeria backend servers - - # Glossaries - glossary_table: DataTable = DataTable(id="glossary_table") - glossary_table.add_columns("Glossary Name", "Description", "Qualified Name") - glossary_table.cursor_type = "row" - glossary_table.zebra_stripes = True - try: - self.glossary_data = exec_report_spec(format_set_name="Glossaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving glossary details: {e!s}") - self.exit(420) - return(420) - self.log(f"Glossary data returned: {self.glossary_data}") - self.glossary_data_extract = self.glossary_data.get("data") or [] - self.log(f"Glossary data extracted: {self.glossary_data_extract}") - if self.glossary_data_extract == []: - self.log(f"No glossary data found for search string: {self.selected_t_node}") - glossary_table.add_row("No glossaries found", "No data returned from Egeria", "") - else: - for g in self.glossary_data_extract: - glossary_table.add_row(g.get("Display Name"), g.get("Description"), g.get("Qualified Name")) - continue - - # Digital Product Catalogs - digital_product_catalog_table: DataTable = DataTable(id="digital_product_catalog_table") - digital_product_catalog_table.add_columns("Digital Product Catalog Name", "Description", "Qualified Name") - digital_product_catalog_table.cursor_type = "row" - digital_product_catalog_table.zebra_stripes = True - try: - self.digital_product_catalog_data = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params = {"search_string" : "*", - "metadata_element_subtypes": ["DigitalProduct", "DigitalProductFamily"]}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving digital product catalog details: {e!s}") - self.exit(421) - return(421) - self.log(f"Digital Product Catalog data returned: {self.digital_product_catalog_data}") - self.digital_product_catalog_data_extract = self.digital_product_catalog_data.get("data") or [] - self.log(f"Digital Product Catalog data extracted: {self.digital_product_catalog_data_extract}") - if self.digital_product_catalog_data_extract == []: - self.log(f"No digital product catalog data found for user: {self.user_name}") - digital_product_catalog_table.add_row("No digital product catalogs found", "No data returned from Egeria", "") - else: - for catalog_item in self.digital_product_catalog_data_extract: - digital_product_catalog_table.add_row(catalog_item["Display Name"], - catalog_item["Description"], - catalog_item["Qualified Name"]) - continue - - # Data Dictionaries - data_dictionary_table: DataTable = DataTable(id="data_dictionary_table") - data_dictionary_table.add_columns("Data Dictionary Name", "Description", "Qualified Name") - data_dictionary_table.cursor_type = "row" - data_dictionary_table.zebra_stripes = True - try: - self.data_dictionary_data = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving data dictionary details: {e!s}") - self.exit(422) - return(422) - self.data_dictionary_data_extract = self.data_dictionary_data.get("data") or [] - if self.data_dictionary_data_extract == []: - self.log(f"No data dictionary details found for user: {self.user_name}") - data_dictionary_table.add_row("No data dictionaries found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.data_dictionary_data_extract} data dictionaries for user {self.user_name}") - data_dictionary_table.add_row("Display Name", "Description", "Qualified Name") - for dictionary in self.data_dictionary_data_extract: - data_dictionary_table.add_row(dictionary.get("Display Name", ""), - dictionary.get("Description", ""), - dictionary.get("Qualified Name", "")) - continue - - # Business Domains - business_domain_table: DataTable = DataTable(id="business_domain_table") - business_domain_table.add_columns("Business Area Name", "Type Name", "GUID") - business_domain_table.cursor_type = "row" - business_domain_table.zebra_stripes = True - try: - self.business_domain_data = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(423) - return(423) - self.business_domain_data_extract = self.business_domain_data.get("data") or [] - if self.business_domain_data_extract == []: - self.log(f"No business domains found for user {self.user_name}") - business_domain_table.add_row("No business domains found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.business_domain_data_extract} business domains for user {self.user_name}") - # business_domain_table.add_row("Business Domain Name", "Description", "GUID") - for domain in self.business_domain_data_extract: - business_domain_table.add_row(domain.get("Qualified Name", ""), - domain.get("Type Name", ""), - domain.get("GUID", "")) - continue - - # Root Collections - - self.root_collection_table = DataTable(id="root_collection_table") - self.root_collection_table.add_columns("Root Collection Name", "Description", "GUID") - self.root_collection_table.cursor_type = "row" - self.root_collection_table.zebra_stripes = True - - try: - self.collections = exec_report_spec(format_set_name="BasicCollections", - output_format="DICT", - params = {"search_string": "RootCollection"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - - except PyegeriaException as e: - print_basic_exception(e) - self.collections = "Error retrieving collections: " + str(e) - self.log(f"Found {len(self.collections)} root collections for user {self.user_name}") - self.log(f"Root collections: {self.collections}") - if isinstance(self.collections, str): - self.root_collection_table.add_row("No root collections found", self.collections, "") - elif isinstance(self.collections, dict) and self.collections.get("kind") == "json": - self.collections:list[dict] = self.collections.get("data") - for collection in self.collections: - self.root_collection_table.add_row(collection.get("Qualified Name"), collection.get("Type Name"), collection.get("GUID")) - continue - elif isinstance(self.collections, list): - for collection in self.collections: - self.root_collection_table.add_row(collection.get("Qualified Name"), collection.get("Type Name"), collection.get("GUID")) - continue - else: - self.log(f"Unexpected data type returned from Egeria: {type(self.collections)}") - self.root_collection_table.add_row("Invalid data type", "Unexpected data type returned from Egeria", "") - - # hand the data to the Screen for displaying - await self.push_screen(ShopForDataScreen(glossary_table, - digital_product_catalog_table, - data_dictionary_table, - business_domain_table, - # data_specification_table, - self.root_collection_table, - self.user_name, - self.user_password, - self.view_server, - self.platform_url), - callback = self.shop_for_data_callback) - elif selected_option == "Edit Profile": - await self.push_screen(EditProfileScreen( - self.user_name, - self.user_password, - self.view_server, - self.platform_url, - self.karma_points, - self.user_profile, - self.user_GUID - ), - callback = self.edit_profile_callback) - elif selected_option == "User Bookmarks": - pass - elif selected_option == "Subscriptions": - pass - else: - pass - - async def tech_type_callback(self, result) -> int: - """ Callback for Technology Types screen - If the result is int (4xx) it indicates an error in the screen - if the result is str it contains the GUID of the selected technology type - """ - #clear local data fields - tech_type_description = "" - self.tech_type_data = {} - self.tech_type_data_extracted = {} - self.tech_type_templates = [] - self.tech_type_processes = [] - # check that we got a valid result from the screen and process accordingly - if not result or isinstance(result, int) : - self.log(f"Technology Types screen cancelled or failed; return: {result}.") - await self.push_screen(MainScreen()) - return (result) - self.selected_t_node = str(result) - self.log(f"Technology Types screen returned: {self.selected_t_node}") - # Request details for selected tech type - try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) - self.tech_type_data = p_client.get_tech_type_detail(filter_string=self.selected_t_node, - output_format="JSON", - # body={"maxMermaidNodeCount": self.max_mermaid_node_count, - # "graphQueryDepth": self.graph_query_depth} - ) - except PyegeriaException as e: - print_basic_exception(e) - error_category = "Technology Type Process Detail Retrieval" - error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") - await self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - self.log(f"Technology Type Data: {self.tech_type_data}, type: {type(self.tech_type_data)}") - - if "specificationMermaidGraph" in self.tech_type_data: - del self.tech_type_data["specificationMermaidGraph"] - self.log(f"Technology Type Mermaid Graph Removed") - - self.tech_type_guid = self.tech_type_data.get("technologyTypeGUID") - self.tech_type_name = self.tech_type_data.get("displayName") - self.tech_type_templates = self.tech_type_data.get("catalogTemplates") - self.tech_type_templates = self.clean_structure(self.tech_type_templates) - self.tech_type_templates = self.bools_to_strings(self.tech_type_templates) - self.log(f"Templates: {self.tech_type_templates}, type: {type(self.tech_type_templates)}") - self.tech_type_processes = self.tech_type_data.get("governanceActionProcesses") - self.tech_type_processes = self.clean_structure(self.tech_type_processes) - self.tech_type_processes = self.bools_to_strings(self.tech_type_processes) - self.log(f"Processes: {self.tech_type_processes}, type: {type(self.tech_type_processes)}") - self.tech_type_description = self.tech_type_data.get("description") - - self.log(f"Technology Type GUID: {self.tech_type_guid}") - self.log(f"Technology Type Name: {self.tech_type_name}") - self.log(f"Technology Type Description: {self.tech_type_description}") - self.log(f"Technology Type Templates: {self.tech_type_templates}" or [{"templates": "None"}]) - self.log(f"Technology Type Processes: {self.tech_type_processes}" or [{"processes": "None"}]) - - await self.push_screen(TechnologyTypeOptionsScreen(self.tech_type_guid, - self.tech_type_name, - self.tech_type_description, - self.user_name, - self.user_password, - self.karma_points, - self.tech_type_templates, - self.tech_type_processes), callback = self.tech_type_options_callback) - return(200) - - async def tech_type_options_callback(self, result:list): - self.log(f"Technology Type Options screen returned: {result}") - self.selected_t_option = None - self.selected_t_option_selected = None - - if not result or result == None: - self.log(f"Technology Type Options screen returned no input; return: {result}.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) or isinstance(result, str): - if not result or result == "back": - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) and result == 200: - self.log("Technology Type Options screen returned successfully.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) and result != 200: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - self.exit(415) - else: - self.log(f"Technology Type Options screen returned unexpected result: {result}, type: {type(result)}") - elif isinstance(result, list): - self.log(f"Input is a list: {result}, type: {type(result)}") - self.log(f"Result 0 = {result[0]}, 1 = {result[1]}") - if not result[0] or isinstance(result[0], int): - if isinstance(result[0], int) and result[0] == 200: - self.log("Technology Type Options screen returned successfully.") - await self.push_screen(MainScreen()) - elif isinstance(result, list) and result[0] == "back": - await self.push_screen(MainScreen()) - else: - self.selected_t_option = str(result[0]) - self.selected_t_option_selected = str(result[1]) - self.log(f"Technology Type Options screen returned: {self.selected_t_option} | {self.selected_t_option_selected}") - else: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, type: {type(result)}.") - await self.push_screen(MainScreen()) - - # display the screen so the objects we need to mount to are created in the DOM - # then we can build the display elements for the placeholder properties - # and mount them in the appropriate containers on the screen - - self.log(f"Selected: {self.selected_t_option} | {self.selected_t_option_selected}") - if self.selected_t_option == "template": - await self.push_screen(TechnologyTypeTemplatesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_templates), - callback=self.tech_type_templates_callback) - elif self.selected_t_option == "process": - await self.push_screen(TechnologyTypeProcessesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_processes), - callback=self.tech_type_processes_callback) - else: - self.log(f"Technology Type Options screen returned invalid option: {self.selected_t_option_selected}") - self.exit(416) - return(200) - - async def fetch_technology_types(self) -> int: - self.tech_type_list: list = [{}] - try: - self.autoc = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - # retrieve the tech type data - self.log(f"Fetching technology type hierarchy for tech_type='*'") - self.tech_type_response = await self.autoc._async_get_tech_type_hierarchy(filter_string = "*" ) - except Exception as e: - self.log(f"Exception in get_tech_type_hierarchy: {e}") - self.log(print_basic_exception(e)) - self.tech_type_list = [{}] - self.exit(416) - return(416) - - self.log (f"tech_type_response: {self.tech_type_response}") - # Copy the data into a working variable for the extraction routine - self.tech_type_list = self.tech_type_response - return(200) - - def unpack_egeria_data(self) -> int: - """ Unpack the data returned from Egeria """ - output_data: list[dict] = [{}] - output_data.clear() - if isinstance(self.tech_type_data, dict): - if "data" in self.tech_type_data: - output_data = self.tech_type_data.get("data") - else: - output_data = [self.tech_type_data] - elif isinstance(self.tech_type_data, list): - for entry in self.tech_type_data: - if isinstance(entry, dict): - output_data = entry.get("data") - elif isinstance(entry, list): - for subentry in entry: - if isinstance(subentry, dict): - output_data = [subentry] - else: - self.log(f"error unknown data structure for Tech Type data") - return(417) - else: - self.log(f"error unknown outer data structure for Tech Type data") - return (417) - else: - self.log(f"Tech Type data not dict or list") - return(417) - - self.log(f"output_data: {output_data}, {type(output_data)}") - # return the extracted data (dict) - self.tech_type_list = output_data - return (200) - - def tech_type_templates_callback(self, result): - """ Callback for Technology Type Templates screen - result contains up to 3 elements: - [0] = return code or 'input', [1] input data, [2] full template""" - # take the input data and use it in the form and - # create from template - self.log(f"Technology Type Templates screen returned: {result}") - # Check for return code - if isinstance(result, int): - self.log(f"Technology Type Templates screen returned: {result}, exiting.") - return(result) - # Check for unexpected return - if not result or result[0] != "input": - self.log(f"Technology Type Templates screen cancelled/failed; return: {result}, exiting.") - return(418) - - # make the keys to the input data match the keys in the template structure - # take each key and use the data value to replace the placeholder value in the template - # then use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - # make a working version of the full template if it is returned - if isinstance(result[2], dict): - self.full_template = result[2] - # create an empty dict for the returned data - my_placeholderPropertyValues: dict = {} - # load in the returned placeholder user data - if isinstance(result[1], dict): - self.placeholder_input = result[1] - # clean up the keys of the user data to match the template keys - if isinstance(self.placeholder_input, dict): - # for each data input item in the dict - for input_item, input_value in self.placeholder_input.items(): - self.log(f"input_item: {input_item}, input_value: {input_value}") - input_fix1 = input_item.replace("_", " ") - self.log(f"input_item after underscore removal: {input_fix1}") - input_fix2 = input_fix1.replace(" placeholder input", "") - self.log(f"input_item after placeholder removal: {input_fix2}") - - self.log(f"fixed input_item: {input_fix2}, {input_value}") - # build the placeholderPropertyValues dict - my_placeholderPropertyValues.update({input_fix2: input_value}) - continue - else: - self.log(f"placeholder_input is not a dict: {self.placeholder_input}") - return(419) - - # create a dict structure that matches the template structure - # we have the input data in my_placeholderPropertyValues and - # the selected template in self.full_template - # so we can use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - self.log(f"my_placeholderPropertyValues: {my_placeholderPropertyValues}") - self.log(f"self.placeholder_input: {self.placeholder_input}") - self.log(f"self.full_template: {self.full_template}") - - # copy the retrieved template into the request body - request_body:dict = { - "class" : "TemplateRequestBody", - "externalSourceGUID" : self.full_template.get("externalSourceGUID") or "", - "externalSourceName" : self.full_template.get("externalSourceName") or "", - "typeName" : self.full_template.get("typeName") or "", - "templateGUID" : self.full_template.get("Catalog Template GUID"), - "anchorGUID" : self.full_template.get("anchorGUID"), - "isOwnAnchor" : "false", - "effectiveFrom" : "2026-01-01", - "effectiveTo": "2030-12-31", - "replacementProperties" : self.full_template.get("replacementProperties") or {}, - "placeholderPropertyValues" : {}, - "parentGUID" : None, - "parentRelationshipTypeName" : None, - "parentRelationshipProperties" : None, - "parentAtEnd1" : self.full_template.get("parentAtEnd1") or True, - "effectiveTime" : self.full_template.get("effectiveTime") or datetime.datetime.now().isoformat(), - } - self.log(f"request_body: {request_body}") - # update the request body with the user provided input - for key, value in my_placeholderPropertyValues.items(): - request_body["placeholderPropertyValues"].update({key: value}) - continue - self.log(f"request_body after update: {request_body}") - # now instantiate and call the function to create the element from the template - try: - tokendata = self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - my_md_instance = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password, tokendata) - # new_guid = my_md_instance.create_metadata_element_from_template(self.template_guid, body = request_body) - new_guid = my_md_instance.initiate_gov_action_process(body=request_body) - except Exception as e: - self.log(f"Exception in create_element_from_template: {e}") - if isinstance(e, PyegeriaException): - self.log(print_basic_exception(e)) - else: - self.log(f"Exception in create_element_from_template: {e}") - self.push_screen(StatusScreen(f"Error creating element from Template/n{print_basic_exception(e)}"), callback=self.status_callback) - return(420) - - self.log(f"new_guid: {new_guid}") - self.push_screen(StatusScreen(f"Element created from Template/Metadata element created with GUID: '{new_guid}'"), callback=self.status_callback) - - def status_callback(self, status_callback_rc) -> None: - """ Callback routine from the status screen, - this screen is typically displayed at the end of a process - to indicate success or failure of the process.""" - - self.log(f"Status screen returned: {status_callback_rc}") - self.exit(status_callback_rc) - - def tech_type_processes_callback(self, result): - """ Callback for Technology Type Processes screen""" - # take the input data and use it to get the template for running the process, display it and allow - #the user to complete the data in the placeholders in order to run the process - self.type = None - self.full_process = None - self.result = result - if isinstance(result, int): - self.log(f"Technology Type Processes Screen returned: {result}, exiting.") - return(result) - if isinstance(self.result, list): - self.type = result[0] - self.input_data = result[1] - self.full_process = result[2] - self.log(f"Received process type: {self.type}, data: {self.input_data}, full process: {self.full_process}") - else: - self.log(f"Unexpected result type from TechnologyTypeProcessesScreen: {type(result)}, exiting.") - return(418) - if self.type != "input": - self.log(f"Process type is not 'input', {self.type}, display error screen") - error_category = "Governance Action Process" - error_message = "Unknown response from TechnologyTypeProcessesScreen: "+self.type - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - my_process_property_values: list[dict] = [{}] - if not isinstance(self.result, dict): - self.log("No process Request Parameter input data returned, this is possibly valid, but unlikely") - self.log(f" Input data type: {type(self.result)}, data: {self.result}") - self.log("Processing will proceed with no Supported Request Parameters for the process") - self.result = {None:None} - else: - for input_key, input_value in self.result.items(): - self.log(f"Processing input value: {input_key} with value: {input_value}") - input_fix1 = input_key.replace("_", " ") - input_fix2 = input_fix1.replace(" process input", "") - self.log(f"Process input key after cleaning: {input_fix2}") - my_process_property_values.append ( - {"class":"SupportedRequestParameter", - "specificationPropertyType": "SUPPORTED_REQUEST_PARAMETER", - "name": input_fix2, - "value": input_value, - "Description": "", - "dataType": "string", - "example": "", - "required": "False"}) - continue - - self.log(f"Process property values: {my_process_property_values}") - self.log(f"Original input: {self.result}") - self.log(f"full process: {self.full_process}") - - request_body: dict ={ - "displayName": self.result.get("displayName"), - "description": self.result.get("description"), - "additionalProperties": - {"templateGUID": self.result.get("additionalProperties")("templateGUID")}, - "specification": - {"supportedRequestParameter": - my_process_property_values - } - } - - def tech_type_processes_details(self, tech_type, selected_t_option_selected): - """ Retrieve process data for Technology Type Processes screen""" - # ************************************************************************************************** - # take the input data and use it to get the template for running the process, display it and allow - # the user to complete the data in the placeholders in order to run the process - # with the change to retrieve the JSON this specific processing is no longer needed - # ************************************************************************************************** - self.type = None - self.full_process = None - self.selected_q_name = None - required_process = None - self.type = str(tech_type) - self.selected_q_name = str(selected_t_option_selected) - self.log(f"Received process type: {self.type}, selected: {self.selected_q_name}") - - if self.selected_q_name != None: - self.log(f"Selected qualified name: {self.selected_q_name}") - try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) - governance_actions = p_client.get_tech_type_detail(filter_string=self.selected_q_name, output_format="JSON") - except PyegeriaException as e: - print_basic_exception(e) - error_category = "Technology Type Process Detail Retrieval" - error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - self.log(f"Egeria returned: {governance_actions}, type: {type(governance_actions)}") - # load Json data - if isinstance(governance_actions, str): - if governance_actions != "No elements found": - technology_response = json.loads(governance_actions) - else: - technology_response = None - else: - technology_response = governance_actions - - self.log(f"Tech type process data retrieved: {technology_response}") - self.governance_action_processes = technology_response.get("governanceActionProcesses", []) - self.log(f"Retrieved {len(self.governance_action_processes)} governance action processes for {self.selected_q_name}") - self.log(f"Data: {self.governance_action_processes}") - for process in self.governance_action_processes: - self.log(f"Process: {process['name']}, Description: {process['description']}") - if process['name'] == self.selected_q_name: - self.log(f"Found requested process: {process['description']}") - # temporary end of function, more code needed here. - required_process = process - break - else: - continue - - return(required_process) - else: - self.log(f"selected_q_name in process_details function is None, returnng None to caller: {self.selected_q_name}") - return(None) - - async def shop_for_data_callback(self, result): - """ Callback for Shop For Data screen""" - if isinstance(result[0], int): - selection_type = int(result) - selection_parm_1 = None - selection_parm_2 = None - selection_parm_3 = None - self.log(f"Shop For Data screen returned: {selection_type}, type: {type(selection_type)}.") - else: - self.log(f"Shop For Data screen returned: {result}, type: {type(result)}.") - selection_type = result[0] - selection_parm_1 = result[1] - selection_parm_2 = result[2] - # selection_parm_3 = result[3] - self.log(f"Shop For Data screen returned: {selection_type}, with parameters: {selection_parm_1}, {selection_parm_2}") - - if isinstance(selection_type, int): - if selection_type == 200: - self.log(f"Shop For Data screen returned successfully.") - return(200) - elif selection_type == 201: - self.log(f"Shop For Data screen returned: {selection_type}, request to search for a term ") - await self.push_screen(SearchForTermScreen(self.user_name, - self.user_password, - self.view_server, - self.platform_url), callback=self.search_for_term_callback) - return(200) - elif selection_type == 211: - self.log(f"Shop For Data screen returned: {selection_type}, request to subscribe to data source ") - return(211) - elif selection_type == 212: - self.log(f"Shop For Data screen returned: {selection_type}, request to sample data source ") - self.request_to_sample_data_source(selection_parm_1, selection_parm_2, selection_parm_3) - else: - self.log(f"Shop For Data screen returned: {selection_type}.") - return(selection_type) - - if selection_type == "dictionary": - # data dictionary - self.log(f"Selected dictionary with qualified name: {selection_parm_1}") - self.build_dictionary_details(selection_parm_1, selection_parm_2) - elif selection_type == "domain": - # business domain - self.log(f"Selected business domain with qualified name: {selection_parm_1}") - self.build_domain_details(selection_parm_1, selection_parm_2) - elif selection_type == "catalog": - # data product catalog - self.log(f"Selected catalog with qualified name: {selection_parm_1}") - self.build_catalog_details(selection_parm_1, selection_parm_2) - elif selection_type == "glossary": - # glossary - self.log(f"Selected glossary with qualified name: {selection_parm_2}") - self.build_glossary_details(selection_parm_1, selection_parm_2) - elif selection_type == "collection": - # root collection - self.log(f"Selected Root Collection with qualified name: {selection_parm_1}") - self.build_root_collection_details(selection_parm_1, selection_parm_2) - else: - self.log(f"Unknown selection type: {selection_type}") - self.exit(429) - - def build_dictionary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a dictionary details screen""" - self.log(f"Building dictionary details for qualified name: {target_qualified_name}") - self.dictionary_qualified_name = target_qualified_name - self.dictionary_display_name = target_display_name - build_structure = {} - - try: - self.dictionary_details = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params={"search_string": self.dictionary_qualified_name, "filter_string": self.dictionary_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving dictionary details: {e!s}") - self.exit(420) - return (420) - self.log(f"Dictionary Details: {self.dictionary_details}") - if not self.dictionary_details or self.dictionary_details == None: - error_category = "Dictionary Details" - error_message = "No dictionary details found" - self.log(f"Error retrieving dictionary details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.dictionary_details.get("kind") == "empty": - dictionary_tree: Tree = Tree(label="Empty Dictionary", id="data_dictionary_tree") - dictionary_tree.root.expand() - dictionary_tree.root.content = "No dictionary terms found for this dictionary" - else: - dictionary_tree: Tree = Tree(label=self.dictionary_display_name, id="data_dictionary_tree") - dictionary_tree.root.expand() - dictionary_tree.auto_expand = True - self.dictionary_details_data = self.dictionary_details.get("data") - for term in self.dictionary_details_data: - self.log(f"Dictionary term: {term} being processed") - term_qualified_name = term.get("Qualified Name") or "" - term_subject = term.get("Subject Area") or "" - term_summary = term.get("Summary") or "" - # create dict structure for loading the tree - if term_subject not in build_structure: - build_structure[term_subject] = [] - build_structure[term_subject].append({term_qualified_name: term_summary}) - continue - # Once the structure is complete we can build the tree from it - for term_subject, terms in build_structure.items(): - self.log(f"Building tree for subject: {term_subject}, term: {terms}") - dictionary_branch = dictionary_tree.root.add(term_subject) - dictionary_branch.expand() - for term_dict in terms: - self.log(f"Adding term to tree: {term_dict}") - for term_qualified_name, term_summary in term_dict.items(): - dictionary_branch.add_leaf(term_summary, data=term_qualified_name) - dictionary_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("dictionary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=dictionary_tree), callback=self.overview_callback) - - def build_domain_details(self, target_qualified_name, target_type__name): - """ Build the details object for a business domain details screen""" - self.log(f"Building domain details for qualified name: {target_qualified_name}") - self.domain_qualified_name = target_qualified_name - self.domain_type__name = target_type__name - build_structure = {} - - try: - self.domain_details = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params={"search_string": self.domain_qualified_name, "filter_string": self.domain_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(420) - return (420) - self.log(f"domain_details: {self.domain_details}") - if not self.domain_details or self.domain_details == None: - error_category = "Business Domain Details" - error_message = "No domain details found" - self.log(f"Error retrieving business domain details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.domain_details.get("kind") == "empty": - domain_tree: Tree = Tree(label="Empty Business Domain", id="business_domain_tree") - domain_tree.root.expand() - domain_tree.root.content = "No domain details found for this business domain" - else: - self.domain_details_data = self.domain_details.get("data") - self.log(f"domain_details_data: {self.domain_details_data}") - self.domain_display_name = self.domain_details_data.get("Qualified Name") - domain_tree: Tree = Tree(label=self.domain_display_name, id="business_domain_tree") - domain_tree.root.expand() - domain_tree.auto_expand = True - for term in self.domain_details_data: - if term == None: - continue - term_qualified_name = term.get("Qualified Name") or "" - term_type = term.get("Type Name") or "" - term_GUID = term.get("GUID") or "" - term_members = term.get("Containing Members") - term_memberof = term.get("Member Of") - # create dict structure for loading the tree - build_structure[term_qualified_name] = { - "term_type": term_type, - "term_GUID": term_GUID, - "term_members": term_members, - "term_memberof": term_memberof - } - continue - # Once the structure is complete we can build the tree from it - for qualified_name, details in build_structure.items(): - domain_branch = domain_tree.root.add(qualified_name, id=qualified_name, data=[details["term_type"], details["term_GUID"]]) - if details["term_members"] != None: - domain_branch_members = domain_branch.add("Containing Members") - for member in details["term_members"]: - domain_branch_members.add_leaf(member) - if details["term_memberof"] != None: - for member in details["term_memberof"]: - domain_branch.add_leaf(member) - domain_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("domain", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=domain_tree), callback=self.overview_callback) - - def build_catalog_details(self, target_qualified_name, target_display_name): - """ Build the details object for a product catalog details screen""" - self.log(f"Building product catalog details for qualified name: {target_qualified_name}") - self.catalog_qualified_name = target_qualified_name - self.catalog_display_name = target_display_name - build_structure = {} - - try: - self.catalog_details = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params={"search_string": self.catalog_qualified_name, "filter_string": self.catalog_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving catalog details: {e!s}") - self.exit(420) - return (420) - self.log(f"catalog_details: {self.catalog_details}") - if not self.catalog_details or self.catalog_details == None: - error_category = "Catalog Details" - error_message = "No catalog details found" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.catalog_details.get("kind") == "empty": - catalog_tree: Tree = Tree(label="Empty Catalog", id="digital_product_catalog_tree") - catalog_tree.root.expand() - catalog_tree.root.content = "No catalog terms found for this catalog" - else: - catalog_tree: Tree = Tree(label=self.catalog_display_name, id="digital_product_catalog_tree") - catalog_tree.root.expand() - catalog_tree.auto_expand = True - self.catalog_details_data: list[dict] = self.catalog_details.get("data") - self.log(f"catalog_details_data: {self.catalog_details_data}") - if not self.catalog_details_data or self.catalog_details_data == None: - error_category = "Catalog Details" - error_message = "No catalog details found or the data dict entry is missing" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - for product in self.catalog_details_data: - self.log(f"product: {product}") - term_qualified_name = product.get("Qualified Name") or "" - term_subject = product.get("Display Name") or "" - term_summary = product.get("Description") or "" - # create dict structure for loading the tree - if term_subject not in build_structure: - build_structure[term_subject] = [] - build_structure[term_subject].append({term_qualified_name: term_summary}) - self.log(f"build_structure: {build_structure}") - continue - # Once the structure is complete we can build the tree from it - sample_data: list = [] - for instance, data_prods in build_structure.items(): - catalog_branch = catalog_tree.root.add(instance) - for data_prod in data_prods: - for term_qualified_name, term_summary in data_prod.items(): - catalog_branch.add_leaf(term_summary, data=term_qualified_name) - self.log(f"term_qualified_name: {term_qualified_name}, term summary: {term_summary}") - catalog_tree.root.expand() - - # get some sample data from the data source for each product - for product in self.catalog_details_data: - collection_memberships = product.get("Member Of") - if collection_memberships: - collection_membership_list = collection_memberships.split(",") - self.log(f"Membership List for product {product.get('Display Name')}: {collection_membership_list}") - for membership_qname in collection_membership_list: - self.log(f"Processing collection membership: {membership_qname}") - if "DigitalProduct" in membership_qname: - self.log(f"Processing dataset: {membership_qname}") - try: - # create client instance and retrieve data - dclient = DataEngineer(self.view_server, self.platform_url, self.user_name, - self.user_password) - token = dclient.create_egeria_bearer_token(self.user_name, self.user_password) - data_set_metadata = dclient.find_tabular_data_sets( - search_string=membership_qname, - start_from=0, - page_size=1, - output_format="DICT" - ) - self.log(f"Dataset metadata retrieved: {data_set_metadata}") - if isinstance(data_set_metadata, list) and len(data_set_metadata) > 0: - data_set_guid = data_set_metadata[0].get("GUID") - if data_set_guid: - data_set_data = dclient.get_tabular_data_set( - tabular_data_set_guid=data_set_guid, - start_from_row=0, - max_row_count=10, - output_format="MD" - ) - self.log(f"Dataset data retrieved: {data_set_data}") - sample_data.append(data_set_data) - else: - sample_data.append(f"No GUID found for dataset {membership_qname}") - else: - sample_data.append(f"No metadata found for dataset {membership_qname}") - continue - except PyegeriaException as e: - self.log(f"Error retrieving dataset data: {e}") - print_basic_exception(e) - else: - continue - self.log(f"Sample data after product {product.get('Display Name')}: {sample_data}") - self.log(f"Final sample data length: {len(sample_data)}") - - self.push_screen(SelectionOverviewScreen("catalog", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=catalog_tree, - data_samples=sample_data), callback=self.overview_callback) - - def build_glossary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a glossary details screen""" - self.log(f"Building glossary details for qualified name: {target_qualified_name}") - self.glossary_qualified_name = target_qualified_name - self.glossary_display_name = target_display_name - - for glossary_instance in self.glossary_data_extract: - if glossary_instance.get("Qualified Name") == target_qualified_name: - self.glossary_folders = glossary_instance.get("Folders") or None - self.log(f"glossary_folders: {self.glossary_folders}") - category = "Uncategorised" - if self.glossary_folders != None: - glossary_tree: Tree = Tree(label=self.glossary_display_name, id="glossary_details_tree") - glossary_tree.root.expand() - glossary_tree.auto_expand = True - - folder_entries = [f.strip() for f in self.glossary_folders.split(',')] - nodes = {(): glossary_tree.root} - prefixes = ["GlossaryCategory", "GlossaryTerm", "CollectionFolder"] - - for entry in folder_entries: - parts = entry.split('::') - if len(parts) == 1: - parts = entry.split('/') - - is_leaf = False - path_parts = [] - full_id = "" - if any(parts[0].startswith(p) for p in prefixes): - type_prefix = parts[0] - path_parts = parts[1:] - is_leaf = "Term" in type_prefix - if is_leaf: - full_id = path_parts[-1] - else: - path_parts = parts - - current_path = () - for i, part in enumerate(path_parts): - parent_path = current_path - current_path = current_path + (part,) - - if current_path not in nodes: - parent_node = nodes[parent_path] - if is_leaf and i == len(path_parts) - 1: - nodes[current_path] = parent_node.add_leaf(part, data=full_id) - else: - new_node = parent_node.add(part, data=part) - new_node.expand() - nodes[current_path] = new_node - - glossary_tree.refresh() - else: - self.log(f"No glossary folders found in the glossary data extract") - folder_category = "Empty Glossary" - category = glossary_tree.root.add(folder_category) - folder_term = "No glossary terms found for this glossary" - category.add_leaf(folder_term) - - self.push_screen(SelectionOverviewScreen("glossary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=glossary_tree), callback=self.overview_callback) - - def build_root_collection_details(self, target_qualified_name, target_display_name): - """ Build the details object for a root collection details screen""" - member_tree: Tree = Tree(label="Root Collection", id="root_collection_members_tree") - member_tree.root.expand() - member_tree.auto_expand = True - - self.log(f"Building root collection details for qualified name: {target_qualified_name}") - self.root_collection_qualified_name = target_qualified_name - collection_branch = member_tree.root.add(self.root_collection_qualified_name, expand=True) - self.log(f"self_collections: {self.collections}") - collection = self.collections[0] - self.log(f"collection: {collection}") - if target_qualified_name == collection.get("Qualified Name"): - root_collection_contains: str = collection.get("Containing Members") or None - self.log(f"root_collection_contains: {root_collection_contains}") - folders = str.split(root_collection_contains, ', ') - for folder in folders: - collection_branch.add_leaf(folder) - continue - - self.push_screen(SelectionOverviewScreen("collection", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=member_tree), callback=self.overview_callback) - - - - def overview_callback(self, r_code): - """Callback function for handling overview screen actions.""" - if isinstance(r_code, list): - self.selected_item = r_code[1] - self.selected_tree = r_code[2] - r_code: int = r_code[0] - if r_code == 410: - error_category = "Collection Category" - error_message = "Unknown collection category returned" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 411: - error_category = "Glossary" - error_message = "query_one no matches found for glossary tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 412: - error_category = "Digital Product Catalog" - error_message = "query_one no matches found for digital product catalog tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 413: - error_category = "Data Dictionary" - error_message = "query_one no matches found for data dictionary tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 414: - error_category = "Business Domain" - error_message = "query_one no matches fround for business domain tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - elif r_code == 415: - error_category = "Root Collections" - error_message = "query_one no matches found for root collections tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - else: - # good return from overview screen - self.log (f"Overview screen callback, return code : {r_code}") - if r_code == 211: - # Subscribe to selected item - self.log(f"Subscribing to selected item: {self.selected_item} from {self.selected_tree}") - try: - s_client = ProductManager(self.view_server, self.platform_url, self.user_name, self.user_password) - s_client.create_egeria_bearer_token(self.user_name, self.user_password) - s_client.create_digital_subscription(self.selected_item) - except (PyegeriaInvalidParameterException, PyegeriaAPIException, PyegeriaException): - self.log(f"Error creating digital subscription: {self.selected_item} from {self.selected_tree}") - # more error handling required here - """ Request Body layout, not all fields are required to be completed - { - "class" : "NewAgreementRequestBody", - "isOwnAnchor" : true, - "anchorScopeGUID" : "", - "parentGUID" : "", - "parentRelationshipTypeName" : "CollectionMembership", - "parentAtEnd1": true, - "properties": { - "class" : "DigitalSubscriptionProperties", - "qualifiedName": "DigitalSubscription::", - "displayName" : "display name", - "description" : "Add description of the subscription here", - "userDefinedStatus" : "DRAFT", - "identifier" : "Add subscription identifier here", - "supportLevel" : "Add the level of support agreed/requested", - "serviceLevels" : { - "property1Name" : "property1Value", - "property2Name" : "property2Value" - }, - "additionalProperties": { - "property1Name" : "property1Value", - "property2Name" : "property2Value" - } - }, - "initialStatus" : "OTHER", - "externalSourceGUID": "add guid here", - "externalSourceName": "add qualified name here", - "effectiveTime" : "{{$isoTimestamp}}", - "forLineage" : false, - "forDuplicateProcessing" : false - }""" - self.push_screen(CreateSubscriptionRequestScreen(), callback=self.create_subscription_callback) - else: - self.push_screen(ShopForDataScreen(self.view_server, self.platform_url, self.user_name, self.user_password, self.selected_item, self.selected_tree), callback=self.shop_for_data_callback) - - def create_subscription_callback(self, result): - """ Callback routine for create subscription request screen - This routine checks for valid input and then processes the subscription request """ - if result is None: - self.log("User cancelled subscription creation") - return - self.log(f"Subscription created: {result}") - - def extract_glossary_terms(self, text): - """ - Extracts GlossaryTerm items from a string structure. - Pattern: Starts with 'GlossaryTerm::', ends at the next ','. - """ - # Pattern explanation: - # GlossaryTerm:: - Matches the literal prefix - # ([^,\']+) - Captures one or more characters that are NOT a comma or a single quote - pattern = r"GlossaryTerm::([^,\']+)" - - # Find all matches in the text - matches = re.findall(pattern, text) - - self.log(f"Extracted {len(matches)} GlossaryTerm items from text") - - # Clean up whitespace and return as a list - return [match.strip() for match in matches] - - def display_glossary_term_details(self, term): - """ Displays the details of a GlossaryTerm item""" - self.target_term = term - try: - self.term_details = exec_report_spec(format_set_name="Glossary-Terms", - output_format="JSON", - params={"search_string": self.target_term, "filter_string": self.target_term}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving term details: {e!s}") - self.exit(440) - return (440) - - self.log(f"term_details: {self.term_details}") - if not self.term_details or self.term_details == None: - error_category = "Glossary Term Details" - error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.term_details.get("kind") == "empty": - self.log(f"No glossary term details found for qualified name: {self.target_term}") - error_category = "Glossary Term Details" - error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - self.term_details_data = self.term_details.get("data") - - if not self.term_details_data or self.term_details_data == None: - error_category = "Glossary Term Details" - error_message = "No glossary term details found or the data dict entry is missing" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - else: - self.term_details_container: ScrollableContainer = self.screen.query_one(DataTable) - self.term_details_container.mount() - for data_item_key, data_item_value in self.term_details_data.items(): - Static(f"Field: {data_item_key}, Value: {data_item_value}").mount(self.term_details_container) - continue - # Once all the fields are complete we can leave it to the screen processing to handle the rest - return (200) - - # def on_data_table_row_selected(self, event: DataTable.RowSelected): - # self.selected_data_table = event.data_table - # self.selected_row_key = event.row_key - # self.selected_row_data = self.selected_data_table.get_row(self.selected_row_key) - # self.log(f"Selected role: {self.selected_row_data}") - - # @on(DataTable.RowSelected, "#roles_table") - # def handle_roles_table_row_selection(self, event): - # def action_edit_roles(self): - # main_screen = self.get_screen("main") - # role_table = main_screen.query_one("#role_table", DataTable) - # self.push_screen(EditRolesScreen(role_table=role_table), callback=self.status_callback) - - def show_team(self, row_key): - """ called from the edit user screen to display team members for a given role""" - row_data = [] - try: - role_table = self.query_one("#roles_table", DataTable) - row_data = role_table.get_row(row_key) - except PyegeriaException as e: - self.notify(f"Error displaying team members: {e}") - return - self.log(f"Selected role: {row_data}") - if row_data: - selected_role_description = row_data[2] - selected_role_name = row_data[0] - selected_role_type = row_data[1] - selected_role_guid = row_data[3] - else: - self.notify(f"Displaying team members is only supported for Role Table Entries") - return() - team_members_list: list = [] - self.team_members = [] - - # provide list of team members for team leader or team member - if ("TeamLeader" in selected_role_name or "TeamLeader" in selected_role_type or - "TeamMember" in selected_role_name or "TeamMember" in selected_role_type): - self.log(f"Selected role is a TeamLeader or TeamMember: {selected_role_name}") - result = self.find_team_members(selected_role_name) - if not isinstance(result, tuple) or len(result) != 5: - return - team_members_list, team_display_name, team_qualified_name, team_category, team_description = result - else: - self.notify(f"Selected role is not a TeamLeader or TeamMember: {selected_role_name}") - return (201) - - self.log(f"team_members_list: {team_members_list}") - # Process team data for display on screen - team_member_properties: list = [] - team_properties: list = [] - team_properties.append(team_display_name) - team_properties.append(team_qualified_name) - team_properties.append(team_category) - team_properties.append(team_description) - self.log(f"team_properties: {team_properties}") - # Process team member data for display on screen - for team_member in team_members_list: - if isinstance(team_member, list) and "Selection Error" in team_member[0]: - self.log(f"Team member details contain 'Selection Error': {team_member}") - self.team_members.append(team_member) - break - else: - self.log(f"Processing team member properties: {team_member}") - team_member_properties = [] - team_member_properties.append(team_member.get("Individual")) - team_member_properties.append(team_member.get("Assignment Type")) - team_member_properties.append(team_member.get("Individual GUID")) - self.log(f"team_member_properties: {team_member_properties}") - self.team_members.append(team_member_properties) - self.log(f"team_members: {self.team_members}") - continue - self.log(f"team_members: {self.team_members}") - self.log(f"team_properties: {team_properties}") - self.log(f"User name: {self.user_name}") - self.push_screen(MyTeam(self.team_members, team_properties, self.user_name), callback=self.my_team_callback) - - def find_team_members(self, role_name): - # common routine for finding team members given a role name - # extract the Department::nnn from the name to use as the search key - selected_role_name = role_name - team_members_list: list = [] - self.team_members = [] - - search_key_parts = selected_role_name.split("::") - role_search_key = '::'.join(search_key_parts[1:]) - self.log(f"Role search key: {role_search_key}") - # get the team member details for that department - team_member_data: dict = exec_report_spec(format_set_name="Team-Members", - output_format="DICT", - params={"search_string": role_search_key}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - self.log(f"team_member_data: {team_member_data}") - # team members? - if team_member_data.get("kind") != "empty": - team_members_data_struct: list[dict] = team_member_data.get("data") - self.log(f"team members data extracted: {team_members_data_struct}") - else: - self.log(f"No team members found for role: {role_search_key}") - error_category = "Team Members" - error_message = "No team members found" - self.log(f"Error retrieving team members: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return ([], None, None, None, None) - team_members_list.clear() - for team_member in team_members_data_struct: - # get team details - team_display_name: str = team_member.get("Display Name", None) - team_qualified_name: str = team_member.get("Qualified Name", None) - team_category: str = team_member.get("Category", None) - team_description: str = team_member.get("Description", None) - # extract the team member entries - team_member_structure: list[dict] = team_member.get("Members", None) - - if team_member_structure != None: - for entry in team_member_structure: - team_members_list.append(entry) - - # if this is for a team leader then: - # get team members profile for contact information and contribution record (karma points) - - # if "TeamLeader" in selected_role_name: - # member_guid = entry.get("Individual GUID", None) - # - - continue - return(team_members_list, team_display_name, team_qualified_name, team_category, team_description) - else: - error_category = "Team Member Details" - error_message = "No team member details found" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return ([], None, None, None, None) - - def my_team_callback(self, status) -> None: - self.log(f"Callback received with status: {status}") - if status == 200: - self.log("MyTeam screen completed successfully") - else: - self.log(f"Error in MyTeam screen: {status}") - self.push_screen("main") - - def search_for_term_callback(self, status) -> None: - self.log(f"Callback received with status: {status}") - if status == 200: - self.log("Search for term screen completed successfully") - self.push_screen("main") - elif status == 201: - glossary_table = self.query_one("g#glossary_table", DataTable) - digital_product_catalog_table = self.query_one("d#digital_product_catalog_table", DataTable) - data_dictionary_table = self.query_one("d#data_dictionary_table", DataTable) - business_domain_table = self.query_one("b#business_domain_table", DataTable) - data_specification_table = self.query_one("d#data_specification_table", DataTable) - self.log("No matches found for search term") - self.push_screen(ShopForDataScreen(glossary_table, - digital_product_catalog_table, - data_dictionary_table, - business_domain_table, - data_specification_table, - self.user_name, - self.user_password, - self.view_server, - self.platform_url), - callback=self.shop_for_data_callback) - - else: - self.log(f"Error in Search for term screen: {status}") - error_category = "Search for Term" - error_message = "Error retrieving Term details" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - def request_to_sample_data_source(self, selection_parm_1, selection_parm_2, selection_parm_3): - """ The user has requested to see a sample of the data from the selected data source """ - self.row_highlighted = selection_parm_1 - self.cursor_row_highlighted = selection_parm_2 - self.data_table_highlighted = selection_parm_3 - self.log(f"Data selected: row={selection_parm_1}, cursor_row={selection_parm_2}, data_table={selection_parm_3}") - selected_item = self.query_one("#"+self.data_table_highlighted, DataTable) - item_content = selected_item.get_row(self.row_highlighted) - - - def truncate_at_sequence(self, data, target="specificationMermaidGraph"): - """ - Recursively traverses data. Truncates strings containing the target - and drops all following sibling items within lists or dictionaries. - Returns (truncated_data, should_terminate_parent_container) - """ - # Handle Strings - if isinstance(data, str): - if target in data: - # Drop the sequence and everything after it - truncated_str = data.split(target)[0] - return truncated_str, True - return data, False - - # Handle Dictionaries - elif isinstance(data, dict): - new_dict = {} - for key, value in data.items(): - # First, check if the key itself contains the target - if isinstance(key, str) and target in key: - truncated_key = key.split(target)[0] - if truncated_key: # Keep key fragment if not empty - new_dict[truncated_key] = value - return new_dict, True - - # Recursively check the value - updated_value, terminate = self.truncate_at_sequence(value, target) - new_dict[key] = updated_value - if terminate: - return new_dict, True - return new_dict, False - - # Handle Lists and Tuples - elif isinstance(data, (list, tuple)): - new_list = [] - for item in data: - updated_item, terminate = self.truncate_at_sequence(item, target) - new_list.append(updated_item) - if terminate: - break - # Maintain original type integrity - return (tuple(new_list) if isinstance(data, tuple) else new_list), terminate - - # Handle Booleans, Numbers, None - return data, False - - # Helper wrapper for clean execution - def clean_structure(self, data, target="specificationMermaidGraph"): - result, _ = self.truncate_at_sequence(data, target) - return result - - def bools_to_strings(self, data): - """ - Recursively traverses data structures and converts - all Boolean values into their corresponding string format. - """ - # Check explicitly for bool first (since bool is a subclass of int in Python) - if isinstance(data, bool): - return str(data) - - # Handle Dictionaries - elif isinstance(data, dict): - return {key: self.bools_to_strings(value) for key, value in data.items()} - - # Handle Lists and Tuples - elif isinstance(data, (list, tuple)): - converted = [self.bools_to_strings(item) for item in data] - return tuple(converted) if isinstance(data, tuple) else converted - - # Return all other types unchanged (ints, floats, strings, None) - return data - - def edit_user_identity_callback(self): - """ Callback routine for the user identities screen - the user has requested to exit the screen and so will will - push the main screen again""" - self.push_screen("main") - - def edit_identities_callback(self, rows_with_keys): - """ Callback for EditIdentitiesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#user_identity_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_communities_callback(self, response): - """ Callback for EditCommunitiesScreen """ - self.response = response - if isinstance(self.response, list): - self.action = self.response[0] - if self.action == "add": - self.new_name = self.response[1] - self.new_description = self.response[2] - self.new_mission = self.response[3] - pass - # if isinstance(rows_with_keys, list): - # main_screen = self.get_screen("main") - # table = main_screen.query_one("#communities_table", DataTable) - # table.clear() - # for key_str, cell_values in rows_with_keys: - # table.add_row(*cell_values, key=key_str) - # self.push_screen("main") - - def edit_roles_callback(self, rows_with_keys): - """ Callback for EditRolesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#roles_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_teams_callback(self, rows_with_keys): - """ Callback for EditTeamsScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#teams_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_profile_callback(self, return_c): - """ Callback routine for the edit profile screen """ - if isinstance(return_c, int): - if return_c == 200: - self.push_screen("main") - else: - self.log(f"Error returned from EditProfileScreen: {return_c}") - self.log("Returning to main screen") - self.push_screen("main") - elif isinstance(return_c, str): - if return_c == "identity": - main_screen = self.get_screen("main") - self.identities_table = main_screen.query_one("#user_identity_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.identities_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.identities_table.rows: - rows_with_keys.append((row_key.value, self.identities_table.get_row(row_key))) - self.push_screen(EditIdentitiesScreen(columns, rows_with_keys), callback=self.edit_identities_callback) - elif return_c == "community": - main_screen = self.get_screen("main") - self.communities_table = main_screen.query_one("#communities_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.communities_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.communities_table.rows: - rows_with_keys.append((row_key.value, self.communities_table.get_row(row_key))) - self.push_screen(EditCommunitiesScreen(columns, rows_with_keys), callback=self.edit_communities_callback) - elif return_c == "role": - main_screen = self.get_screen("main") - self.roles_table = main_screen.query_one("#roles_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.roles_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.roles_table.rows: - rows_with_keys.append((row_key.value, self.roles_table.get_row(row_key))) - self.push_screen(EditRolesScreen(columns, rows_with_keys), callback=self.edit_roles_callback) - elif return_c == "team": - main_screen = self.get_screen("main") - self.teams_table = main_screen.query_one("#teams_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.teams_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.teams_table.rows: - rows_with_keys.append((row_key.value, self.teams_table.get_row(row_key))) - self.push_screen(EditTeamsScreen(columns, rows_with_keys), callback=self.edit_teams_callback) - else: - self.log(f"Unexpected return type from EditProfileScreen: {type(return_c)}") - self.push_screen("main") - - def edit_selected_table(self, table_name): - """Edit selected table based on the provided table name.""" - self.selected_table = table_name - if self.selected_table == "associations_table": - self.push_screen(EditAssociationsScreen(), callback=self.edit_associations_callback) - elif self.selected_table == "projects_table": - self.push_screen(EditProjectsScreen(), callback=self.edit_projects_callback) - elif self.selected_table == "communities_table": - self.push_screen(EditCommunitiesScreen(), callback=self.edit_communities_callback) - elif self.selected_table == "roles_table": - self.push_screen(EditRolesScreen(), callback=self.edit_roles_callback) - elif self.selected_table == "blog_table": - self.push_screen(EditBlogsScreen(), callback=self.edit_blog_callback) - elif self.selected_table == "activity_table": - self.push_screen(EditActivityScreen(), callback=self.edit_activity_callback) - elif self.selected_table == "journals_table": - self.push_screen(EditJournalScreen(), callback=self.edit_journals_callback) - elif self.selected_table == "user_identity_table": - self.push_screen(EditIdentitiesScreen(), callback=self.edit_user_identity_callback) - elif self.selected_table == "teams_table": - self.push_screen(EditTeamsScreen(), callback=self.edit_teams_callback) - elif self.selected_table == "todos_table": - self.push_screen(EditTodosScreen(), callback=self.edit_todos_callback - ) - - def add_note(self): - pass - - def show_notes(self): - pass - - def delete_community(self, row_key) -> int: - """ Delete Community Record from Egeria - Successful deletion returns - 200 - Unsuccessful deletion returns - 400 """ - - self.log(f"Delete Community Record from Egeria", {"row_key": row_key}) - self.row_key= row_key - main_screen = self.get_screen("main") - del_community_table = main_screen.query_one("#community_table", DataTable) - del_community_data = del_community_table.get_row(self.row_key) - self.log(f"Delete Community Record from Egeria", {"del_community_data": del_community_data}) - self_row_guid = del_community_data[3] - self.log(f"Delete Community Record from Egeria", {"self_row_guid": self_row_guid}) - eclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - try: - # --- API call (show at minimum the required params; document optional ones) --- - body = { - "class": "DeleteCommunityRequestBody", - "properties": { - "communityGuid": self.row_guid - } - } - response = eclient.delete_metadata_element(body) - self.log(f"Community record deleted successfully: {self_row_guid}, {response}") - del_comm_rc = 200 - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error deleting community record: {self.row_guid}, error: {e}") - del_comm_rc = 400 - finally: - eclient.close_session() - return(del_comm_rc) - - def remove_link_to_community(self, row_key) -> int: - """ Delete link to Community from the Actor Profile - Successful deletion returns - 200 - Unsuccessful deletion returns - 400 """ - - self.log(f"Remove link to Community from Egeria", {row_key}) - self.row_key = row_key - main_screen = self.get_screen("main") - rem_community_table = main_screen.query_one("#community_table", DataTable) - rem_community_data = rem_community_table.get_row(self.row_key) - self.log(f"Remove Community Link from Profile", {"rem_community_data": rem_community_data}) - self_row_guid = rem_community_data[3] - self.log(f"Remove Community Link from Profile", {"self_row_guid": self_row_guid}) - - eclient = Egeria( - view_server=self.view_server, - platform_url=self.platform_url, - user_id=self.user_name, - user_pwd=self.user_password - ) - token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - try: - # Remove a link to a community from the actor profile for the current user - body = { - "class": "NewElementRequestBody", - "isOwnAnchor": True, - "effectiveFrom": "{{$isoTimestamp}}", - "effectiveTo": "{{$isoTimestamp}}", - "properties": { - "class": "ActorProfileProperties", - "typeName": self.actor_profile["Type Name"], - "qualifiedName": eclient.__create_qualified_name__(self.actor_profile["GUID"]), - "displayName": self.actor_profile["Name"], # short name - "description": self.actor_profile["Description"] - } - } - #have to call egeria for the guid of the link to remove - # Fetch all relationships linked to the Community - relationships = eclient.get_relationships_for_entity( - userId=self.user_name, - entityGUID=self.row_guid, - relationshipTypeGUID=None, # Leave None to fetch all types, or pass the specific type GUID if known - fromRelationshipElement=0, - pageSize=50 - ) - - link_guid = "" - - for rel in relationships: - # Check if the relationship points to Actor Profile GUID - # Egeria relationships store the endpoints in entityOneProxy and entityTwoProxy - entity_one_guid = rel.get('entityOneProxy', {}).get('GUID') - entity_two_guid = rel.get('entityTwoProxy', {}).get('GUID') - if entity_one_guid == self.user_GUID or entity_two_guid == self.user_GUID: - # The unique identifier for the relationship/link itself - link_guid = rel.get('GUID') - break - - if not link_guid: - self.log("No relationship found between this Community and Actor Profile.") - return(400) - - link_to_community_guid = link_guid - - eclient.remove_actor_profile_link( - body=body, - link_type="CommunityLink", - guid=link_to_community_guid - ) - del_comm_rc = 200 - self.communities_table.refresh() - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error removing community link: error: {e}") - del_comm_rc = 400 - finally: - eclient.close_session() - return(del_comm_rc) - - def edit_associations_callback(self): - """Callback for editing associations""" - pass - - def edit_projects_callback(self): - """Callback for editing projects""" - pass - - def edit_blog_callback(self): - """Callback for editing blog""" - pass - - def edit_activity_callback(self): - """Callback for editing activity""" - pass - - def edit_journals_callback(self): - """Callback for editing associations""" - pass - - def edit_user_identity_callback(self): - """Callback for editing user identity""" - pass - - def add_comment(self): - """ Adding a comment """ - pass - - def add_UserIdentity(self): - """ Adding a user identity """ - pass - - def AddJournalEntry(self): - """ Adding a journal entry """ - pass - - def add_team(self): - """ Adding a team """ - pass - - def add_blog_entry(self, title, text): - """ Adding a blog entry """ - self.title = title - self.text = text - - eclient = Egeria(self.user_name, - self.user_password, - self.view_server, - self.platform_url) - - token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - - try: - text = self.text - display_name = self.title - body = { - 'class': 'NewAttachmentRequestBody', - 'properties': { - 'class': 'NotificationProperties', - 'typeName': 'Notification', - 'qualifiedName': eclient.make_feedback_qn('Blog', self.user_name, display_name), - 'displayName': display_name, - 'description': text - } - } - response = eclient.blog_my_activity( - text=text, # The content of the blog post. - display_name=display_name, # A short name for this activity (e.g. "My Blog Post"). - body=body # An optional dictionary or NewAttachmentRequestBody containing additional properties - # to be associated with the notification. - ) - except PyegeriaException as e: - print_basic_exception(e) - finally: - eclient.close_session() - - def delete_blog_entry(self, table_row): - """ Deleting a project """ - self.row_to_delete = table_row - row_data = self.blog_entry_table.get_row(self.row_to_delete) - self.project_qualified_name = row_data[2] - eclient = Egeria( - self.user_name, - self.user_password, - self.view_server, - self.platform_url - ) - eclient.create_egeria_bearer_token() - # get the projects guid using its qualified name - try: - # 1. Retrieve the element by its unique qualified name - asset_element = eclient.get_asset_by_name(qualified_name=self.project_qualified_name) - # 2. Extract the GUID from the returned object wrapper - if asset_element and hasattr(asset_element, 'element_header'): - self.element_guid = asset_element.element_header.GUID - self.log(f"The GUID for {self.project_qualified_name} is: {self.element_guid}") - else: - # Alternate fallbacks depend on the specific Pyegeria object dictionary type - self.element_guid = asset_element.get('GUID') or asset_element.get('elementHeader', {}).get('GUID') - self.log(f"The GUID for {self.project_qualified_name} is: {self.element_guid}") - except PyegeriaException as e: - self.log(f"Error retrieving project GUID: {e}") - print_basic_exception(e) - return(401) - finally: - eclient.close_session() - #now we have the GUID we can use it to delete the project - try: - body = { - "class": "DeleteClassificationRequestBody", - "externalSourceGUID": self.element_guid, - "externalSourceName": self.project_qualified_name, - "effectiveTime": "{{$isoTimestamp}}", - "forLineage": False, - "forDuplicateProcessing": False - } - eclient.clear_project_classification( body=body) - self.action_refresh() - except PyegeriaException as e: - print_basic_exception(e) - finally: - # --- Clean up --- - eclient.close_session() - - def add_todos(self): - """ Adding a todo """ - pass - - def add_community(self, display_name, description): - """ Adding a community """ - eclient = Egeria(self.user_name, - self.user_password, - self.view_server, - self.platform_url) - eclient.create_egeria_bearer_token(self.user_name, self.user_password) - try: - # --- API call (show at minimum the required params; document optional ones) --- - body = { - "class": "NewCommunityRequestBody", - "properties": { - "typeName": "Community", # the actual Egeria type - "qualifiedName": eclient.__create_qualified_name__("Community", display_name), - "displayName": display_name, - "description": description, - "domainIdentifier": 0, # 0 = all domains - } - } - community_guid = eclient.create_community(body) - self.notify(f"Created Community: {community_guid}") - return_code = 200 - except PyegeriaException as e: - print_basic_exception(e) - self.notify(f"Failed to create community, error: {e}") - return_code = 400 - finally: - eclient.close_session() - return return_code - - def add_project(self, project_type, project_name, project_description, project_id) -> int: - """ Adding a project """ - self.project_type = project_type - self.project_name = project_name - self.project_description = project_description - self.project_id = project_id - - # Create a new project - body_create_project = { - "class": "NewElementRequestBody", - "properties": { - "classificationName": self.project_type, - # Project type - Campaign, StudyProject, Task, PersonalProject or Project. - "displayName": self.project_name, # Display name of the project. - "description": self.project_description, # Description of the project. Defaults to None - "identifier": self.project_id # Business identifier for the project. Defaults to None - } - } - - eclient=Egeria(self.user_name, - self.user_password, - self.view_server, - self.platform_url) - - token = eclient.create_egeria_bearer_token() - try: - guid = eclient.create_project( - anchor_guid=None, # Optional - The identity of the anchor element for the project. - parent_guid=None, # Optional - The identity of the parent element for the project. - parent_relationship_type_name="Project", - # Optional - Type of relationship to the parent element. Defaults to None - parent_at_end1=False, # Optional - True if the parent is at end 1 of the relationship. Defaults to False - # display_name="", # Display name of the project. Defaults to None. - # description="My new project", # Description of the project. Defaults to None. - # classification_name="Project", - # identifier=None, # Business identifier for the project. Defaults to None - is_own_anchor=False, - status=None, # Project status. Defaults to None - phase=None, # Project phase. Defaults to None - health=None, # Project health. Defaults to None - start_date=None, # Start date of the project. Defaults to None. - planned_end_date=None, # Planned completion date of the project. Defaults to None. - body=body_create_project # A dict representing the details of the project to create. - ) - self.action_refresh() - return_code = 200 - except PyegeriaException as e: - print_basic_exception(e) - return_code = 400 - finally: - eclient.close_session() - return(return_code) - - def delete_project(self, table_row): - """ Deleting a project """ - self.row_to_delete = table_row - row_data = self.projects_table.get_row(self.row_to_delete) - self.project_qualified_name = row_data[2] - eclient = Egeria( - self.user_name, - self.user_password, - self.view_server, - self.platform_url - ) - eclient.create_egeria_bearer_token() - # get the projects guid using its qualified name - try: - # 1. Retrieve the element by its unique qualified name - asset_element = eclient.get_asset_by_name(qualified_name=self.project_qualified_name) - # 2. Extract the GUID from the returned object wrapper - if asset_element and hasattr(asset_element, 'element_header'): - self.element_guid = asset_element.element_header.GUID - self.log(f"The GUID for {self.project_qualified_name} is: {self.element_guid}") - else: - # Alternate fallbacks depend on the specific Pyegeria object dictionary type - self.element_guid = asset_element.get('GUID') or asset_element.get('elementHeader', {}).get('GUID') - self.log(f"The GUID for {self.project_qualified_name} is: {self.element_guid}") - except PyegeriaException as e: - self.log(f"Error retrieving project GUID: {e}") - print_basic_exception(e) - return(401) - finally: - eclient.close_session() - #now we have the GUID we can use it to delete the project - try: - body = { - "class": "DeleteClassificationRequestBody", - "externalSourceGUID": self.element_guid, - "externalSourceName": self.project_qualified_name, - "effectiveTime": "{{$isoTimestamp}}", - "forLineage": False, - "forDuplicateProcessing": False - } - eclient.clear_project_classification( body=body) - self.action_refresh() - except PyegeriaException as e: - print_basic_exception(e) - finally: - # --- Clean up --- - eclient.close_session() - - def add_role(self): - """ Adding a role """ - pass - -if __name__ == "__main__": - app = MyProfileApp() - app.run() -# -# if __name__ == "__main__": -# main() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/ShopForDataScreenOld.py b/my_egeria/my_egeria/DemoCode/Deprecated/ShopForDataScreenOld.py deleted file mode 100644 index 35a011ff..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/ShopForDataScreenOld.py +++ /dev/null @@ -1,164 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.events import Action -from textual.screen import Screen -from textual.widgets import DataTable, Header, TextArea, Static, Footer - - -class ShopForDataScreen(Screen): - """ Screen to Present a choice of different data sources to the user.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("s", "sample_data_source", "Sample data source"), - ("b", "back", "Go back")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__ (self, glossary_table, digital_product_catalog_table, data_dictionary_table, business_domain_table, root_collection_table, - user_name, user_password, view_server, platform_url): - """Initialize the ShopForDataScreen screen.""" - self.glossary_table: DataTable = glossary_table - self.digital_product_catalog_table: DataTable = digital_product_catalog_table - self.data_dictionary_table: DataTable = data_dictionary_table - self.business_domain_table: DataTable = business_domain_table - self.root_collection_table: DataTable = root_collection_table - self.user_name = user_name - self.user_password = user_password - self.view_server = view_server - self.platform_url = platform_url - super().__init__() - - def compose(self) -> ComposeResult: - """ Compose the UI components for the ShopForDataScreen screen.""" - yield Header(show_clock=True) - yield TextArea(f"\n\n\nPlease click on a line to select a data source:") - yield ScrollableContainer( - Static("Glossary"), - self.glossary_table) - yield ScrollableContainer( - Static("Digital Product Catalog"), - self.digital_product_catalog_table) - yield ScrollableContainer( - Static("Data Dictionary"), - self.data_dictionary_table) - yield ScrollableContainer( - Static("Business Domain"), - self.business_domain_table) - yield ScrollableContainer( - Static("Root Collection"), - self.root_collection_table) - yield Footer() - - - def on_mount(self) -> None: - self.header = f"Egeria Data Sources for user {self.user_name}" - self.sub_header = "Shop for Data" - - @on(DataTable.RowHighlighted) - def handle_data_table_highlight(self, event: DataTable.RowHighlighted): - row_highlighted = event.row_key - cursor_row_highlighted = event.cursor_row - data_table_highlighted = event.data_table - self.log(f"Row highlighted: {row_highlighted}") - - @on(DataTable.RowSelected, "#glossary_table") - def handle_glossary_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.glossary_table.get_row(row_selected) - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["glossary", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#digital_product_catalog_table") - def handle_digital_product_catalog_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - self.log(f"Digital product row selected: {row_selected}, event: {event}") - # row_values = self.glossary_table.get_row(event.row_key) - row_values = self.query_one("#digital_product_catalog_table", DataTable).get_row(event.row_key) - self.log(f"Digital product row values: {row_values}") - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["catalog", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#data_dictionary_table") - def handle_data_dictionary_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#data_dictionary_table", DataTable).get_row(event.row_key) - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["dictionary", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#business_domain_table") - def handle_business_domain_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#business_domain_table", DataTable).get_row(event.row_key) - row_qualified_name = row_values[0] - row_type_name = row_values[1] - row_guid = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, type name: {row_type_name}, guid: {row_guid}") - self.dismiss (["domain", row_qualified_name, row_type_name]) - - # @on(DataTable.RowSelected, "#data_specification_table") - # def handle_data_specification_table_selection(self, event: DataTable.RowSelected): - # row_selected = event.row_key - # row_values = self.query_one("#data_specification_table", DataTable).get_row(event.row_key) - # row_display_name = row_values[0] - # row_description = row_values[1] - # row_qualified_name = row_values[2] - # self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - # self.dismiss (["specification", row_qualified_name, row_display_name]) - - @on (DataTable.RowSelected, "#root_collection_table") - def handle_root_collection_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#root_collection_table", DataTable).get_row(event.row_key) - row_qualified_name = row_values[0] - row_description = row_values[1] - row_GUID = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, description: {row_description}, GUID: {row_GUID}") - self.dismiss (["collection", row_qualified_name, row_description]) - - # @on(DataTable.RowHighlighted) - # def handle_row_highlighted(self, event: DataTable.RowHighlighted) -> None: - # """ Handle the highlighting of a row in the DataTable.""" - # table = event.data_table - # row_key = event.row_key - # row_values = self.query_one("#"+str(table), DataTable).get_row(row_key) - # row_qualified_name = row_values[0] - # row_description = row_values[1] - # row_GUID = row_values[2] - # self.log(f"Table {table} Row highlighted: {row_key}, values: {row_values}, qualified name: {row_qualified_name}, description: {row_description}, GUID: {row_GUID}") - - def action_back(self) -> None: - """ The back option in the footer has been selected. Dismiss the screen.""" - self.dismiss(200) - - def action_search_for_term(self) -> None: - """ The search for term option in the footer has been selected. Dismiss the screen.""" - self.dismiss(201) - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss(210) - - def action_subscribe_to_data(self) -> None: - """ The subscribe to data option in the footer has been selected.""" - self.dismiss(211) - - def action_sample_data_source(self): - """ The sample data source option in the footer has been selected.""" - self.dismiss(212) \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeOptionsScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeOptionsScreen.py deleted file mode 100644 index 9161258a..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeOptionsScreen.py +++ /dev/null @@ -1,258 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from pyegeria import load_app_config, settings -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Container -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Static, OptionList, Button, Footer -from textual.widgets._option_list import Option - - -class TechnologyTypeOptionsScreen(ModalScreen): - """ Modal screen to display a technology type's templates and processes.""" - BINDINGS = [("q", "dismiss", "Quit"), - ("b", "back", "Go_Back"), - # ("ctl+s", "Select", "tech_type_option_select") - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, tech_type_guid: str, - tech_type_name: str, - tech_type_description: str, - user_name: str, - user_pwd:str, - user_kpts: int, - tech_type_templates: list[dict], - tech_type_processes: list[dict]) -> None: - """Initialize the TechnologyTypeOptions screen with a technology type's templates and processes.""" - super().__init__() - self.selected_process_index = None - self.tech_type_guid = tech_type_guid - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.user_name = user_name - self.user_password = user_pwd - self.karma_points = user_kpts - self.tech_type_templates = tech_type_templates - self.tech_type_processes = tech_type_processes - self.selected_template_guid = None - self.selected_process_guid = None - self.option_type_selected = None - self.selected_template = None - self.selected_template_index = None - self.selected_template_data = None - self.selected_process = None - self.selected_process_data = None - - load_app_config("config/config.json") - app_config = settings.Environment - app_user = settings.User_Profile - - if not self.user_name: - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - - def compose(self) -> ComposeResult: - - """ Compose the UI components for the TechnologyTypeOptions screen.""" - yield Header(show_clock=True) - yield Static(f"Description: {self.tech_type_description}") - yield ScrollableContainer( - Static(f"Technology Type: {self.tech_type_name}"), - Container( - Static("Select a template or process to continue."), - Static(f"Templates", id="number_of_templates_label"), - ScrollableContainer( - Static("Available Templates", id="template_options_label"), - OptionList(id="template_options"), - Button("Select Template", id="select_template_btn"), - id="template_options_container"), - Static(f"Processes:", id="number_of_processes_label"), - ScrollableContainer( - Static("Available Processes",id="process_options_label"), - OptionList(id="process_options"), - id="process_options_container"), - Button("Select Process", - id="select_process_btn"))) - yield Footer() - - async def on_mount(self) -> int: - """Mount the TechnologyTypeOptions screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - self.log(f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}") - - for widget in self.query(): - self.log(f"Widgets: {widget}") - - self.log(f"Templates: {self.tech_type_templates}, Processes: {self.tech_type_processes}") - self.log(f"Templates Type: {type(self.tech_type_templates)}, Processes Type: {type(self.tech_type_processes)}") - - if self.tech_type_templates and self.tech_type_templates != "None": - for t in self.tech_type_templates: - try: - self.log(f"Template: {t}") - templates = self.query_one("#template_options", OptionList).add_option( - Option(t.get("displayName"))) - self.log(f"Added option: {t.get('Catalog Template Name')}") - await templates.mount(after=self.query_one("#template_options_label")) - self.log(f"Mounted option") - self.query_one("#select_template_btn", Button).disabled = False - self.log(f"Enabled the template button") - except NoMatches as e: - try: - templates: OptionList = OptionList(id="template_options") - await templates.mount(after=self.query_one("#template_options_label")) - templates.add_option(Option(t.get("displayName"))) - templates.refresh() - self.query_one("#select_template_btn", Button).disabled = False - continue - except Exception as e: - self.log(f"Error creating template option list: {e}, (410") - return (410) - except Exception as e: - self.log(f"Error creating template option list: {e} (411)") - return (411) - else: - continue - else: - try: - self.log(f" No Templates") - templates = self.query_one("#template_options", OptionList).add_option( - Option("No Templates Found for this Tech Type")) - await templates.mount(after=self.query_one("#template_options_label")) - self.query_one("#select_template_btn", Button).disabled = True - except NoMatches as e: - try: - templates: OptionList = OptionList(id="template_options") - await templates.mount(after=self.query_one("#template_options_label")) - templates.add_option(Option("No Templates found for this Tech Type")) - self.query_one("#select_template_btn", Button).disabled = True - except Exception as e: - self.log(f"Error creating template option list: {e}") - return (409) - except Exception as e: - self.log(f"Error creating template option list: {e}") - return (408) - - if self.tech_type_processes and self.tech_type_processes != "None": - for p in self.tech_type_processes: - try: - self.log(f"Process: {p}") - processes = self.query_one("#process_options", OptionList).add_option( - Option(p.get("displayName"))) - self.log(f"Process option added: {p.get('displayName')}") - await processes.mount(after=self.query_one("#process_options_label")) - self.query_one("#select_process_btn", Button).disabled = False - self.log(f"Enabled the process button") - except NoMatches as e: - try: - processes: OptionList = OptionList(id="process_options") - await processes.mount(after=self.query_one("#process_options_label")) - processes.add_option(Option(p.get("displayName"))) - processes.refresh() - self.query_one("#select_process_btn", Button).disabled = False - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (407) - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (406) - else: - continue - else: - try: - self.log(f" No Processes") - processes = self.query_one("#process_options", OptionList).add_option( - Option("No processes found for this Tech Type")) - await processes.mount(after=self.query_one("#process_options_label")) - self.query_one("#select_process_btn", Button).disabled = True - except NoMatches as e: - try: - processes: OptionList = OptionList(id="process_options") - await processes.mount(after=self.query_one("#process_options_label")) - processes.add_option(Option(" No Processes found for this Tech Type")) - self.query_one("#select_process_btn", Button).disabled = True - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (405) - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (404) - - return(200) - - @on(OptionList.OptionHighlighted, "#template_options") - def handle_template_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: - """Handle the highlighting of an option in the template option list.""" - self.log(f"Template option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.selected_template_index = event.option_index - selected_option = event.option - self.selected_template = selected_option.prompt - self.log(f"Highlighted Template: {self.selected_template}, index: {self.selected_template_index}") - - @on(OptionList.OptionSelected, "#template_options") - def handle_template_option_selected(self, event: OptionList.OptionSelected) -> None: - """Handle the selection of an option from the template or process option lists.""" - self.log(f"Template selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.option_type_selected = event.option_list.name - self.selected_template_index = event.option_index - selected_option = event.option - self.selected_template = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_template}, index: {self.selected_template_index}") - - @on(OptionList.OptionHighlighted, "#process_options") - def handle_process_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: - """Handle the highlighting of an option in the process option list.""" - self.log(f"Process option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.selected_process_index = event.option_index - selected_option = event.option - self.selected_process = selected_option.prompt - self.log(f"Highlighted Process: {self.selected_process}, index: {self.selected_process_index}") - - @on(OptionList.OptionSelected, "#process_options") - def handle_process_option_selected(self, event: OptionList.OptionSelected) -> None: - """Handle the selection of an option from the template or process option lists.""" - self.log(f"Process selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.option_type_selected = event.option_list.name - self.selected_process_index = event.option_index - selected_option = event.option - self.selected_process = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_process}, index: {self.selected_process_index}") - - def action_go_back(self) -> None: - """ Handle the back key press.""" - self.dismiss("back") - - def action_Quit(self) -> None: - """ Handle the quit key press""" - self.dismiss(200) - - @on(Button.Pressed, "#select_template_btn") - def handle_template_selected(self, event: Button.Pressed) -> None: - """Handle the selection of a template option.""" - if self.selected_template is None: - return - self.log(f"Template option selected: {self.selected_template}") - self.dismiss(["template", self.selected_template]) - - @on(Button.Pressed, "#select_process_btn") - def handle_process_selected(self, event: Button.Pressed) -> None: - """Handle the selection of a process option.""" - if self.selected_process is None: - return - self.log(f"Process option selected: {self.selected_process}") - self.dismiss(["process", self.selected_process]) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeProcessesScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeProcessesScreen.py deleted file mode 100644 index 8d09f6e3..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeProcessesScreen.py +++ /dev/null @@ -1,153 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from typing import Any - -from pyegeria import load_app_config -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer, Container -from textual.screen import ModalScreen -from textual.widgets import TextArea, Input, Header, Static, Button, Footer - - -class TechnologyTypeProcessesScreen(ModalScreen[Any]): - """Modal screen to display technology type processes in Egeria.""" - BINDINGS = [("q", "quit", "Quit"), - ("b", "back", "Go back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__ (self, - user_name: str, - user_kpts: int, - tech_type_name: str, - tech_type_description: str, - selected_t_option, - tech_type_option_selected, - tech_type_processes - ) -> None: - """Initialize the TechnologyTypeProcesses screen.""" - super().__init__() - self.user_name = user_name - self.karma_points = user_kpts - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.selected_t_option = selected_t_option - self.selected_t_option_selected = tech_type_option_selected - self.tech_type_processes = tech_type_processes - self.full_process = None - self.selected_process = None - load_app_config() - - async def on_mount(self) -> None: - """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - - if self.selected_t_option == "process": - self.log(f"Processing processes, with data: {self.selected_t_option_selected}") - # get selected process from the tech_type data - self.log(f"Technology Type Process: {self.tech_type_processes}") - if isinstance(self.tech_type_processes, list): - for process in self.tech_type_processes: - self.log(f"Process: {process.get("displayName")}, Selected: {self.selected_t_option_selected}") - if self.selected_t_option_selected == process.get("displayName"): - self.full_process = process - self.selected_t_process = process - self.log(f"Selected Process: {self.selected_t_process}") - break - else: - continue - self.log(f"Selected Process: {self.selected_t_process}, type: {type(self.selected_t_process)}") - - if self.selected_t_process is None: - self.log("No process selected, skipping placeholder display") - return - else: - specification = self.selected_t_process.get("specification") - if specification is None: - self.log("Selected process has no specification, skipping placeholder display") - return - else: - placeholderProperties = specification.get("supportedRequestParameter") - if placeholderProperties is None: - self.log("Selected process has no req uest parameters, skipping display") - return - else: - for parameter in placeholderProperties: - if parameter.get("class") != "SupportedRequestParameter": - continue - else: - name = parameter.get("name") or None - Description = parameter.get("description") or None - Type = parameter.get("dataType") - Example = parameter.get("example") - Required = parameter.get("required") - # governance_action_steps = self.full_process.get("Governance Action Steps") or None - - # Sanitize the name for use as a CSS ID - safe_name = name.replace(" ", "_") if name else f"placeholder_{id(self.selected_t_process)}" - safe_name = safe_name.replace("::", "_") - safe_name = safe_name.replace(":", "_") - self.log(f"Safe name = {safe_name}") - placeholder_text: TextArea = TextArea( - f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", - id=f"{safe_name}_placeholder_text_area", - read_only=True - ) - # Ensure TextArea is visible - placeholder_text.styles.height = 8 - - placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") - - # Mount the TextArea and the associated Input field into the ScrollableContainer - try: - load_point = self.query_one("#technology_type_processes_input") - await load_point.mount(placeholder_text, before="#process_submit_button") - await load_point.mount(placeholder_input, before="#process_submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") - continue - except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") - self.app.dismiss(416) - - - def compose(self) -> ComposeResult: - """ Compose the UI components for the Technology_Type_Processes screen.""" - yield Header(show_clock=True) - yield Static("Please complete the required fields and any optional fields you prefer:") - yield ScrollableContainer( - Static("Technology Type Process Input"), - Button("Submit", variant="primary", id="process_submit_button"), - id="technology_type_processes_input" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200", ) - - @on(Button.Pressed, "#process_submit_button") - def handle_submit_button_pressed(self, event: Button.Pressed) -> None: - """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") - save_input_data:dict = {} - for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") - save_input_data.update({input_widget.id: input_widget.value}) - self.log(f"Save input data: {save_input_data}") - self.dismiss(["input", save_input_data, self.full_process]) - - @on(Input.Changed, "#technology_type_processes_input") - def handle_input_changed(self, event: Input.Changed) -> None: - """The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeTemplatesScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeTemplatesScreen.py deleted file mode 100644 index 5b7a5118..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypeTemplatesScreen.py +++ /dev/null @@ -1,148 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from typing import Any - -from pyegeria import load_app_config -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import TextArea, Input, Header, Static, Button, Footer - - -class TechnologyTypeTemplatesScreen(ModalScreen[Any]): - """Modal screen to display technology type templates in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "back", "Go back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__ (self, - user_name: str, - user_kpts: int, - tech_type_name: str, - tech_type_description: str, - selected_t_option, - tech_type_option_selected, - tech_type_templates - ) -> None: - """Initialize the Technology_Type_Templatess screen.""" - super().__init__() - self.user_name = user_name - self.karma_points = user_kpts - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.selected_t_option = selected_t_option - self.selected_t_option_selected = tech_type_option_selected - self.tech_type_templates = tech_type_templates - self.full_template = None - self.selected_t_template = None - load_app_config() - - async def on_mount(self) -> None: - """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - - if self.selected_t_option == "template": - self.log(f"Processing templates, with data: {self.selected_t_option_selected}") - # get selected template from the tech_type data - self.log(f"Technology Type Templates: {self.tech_type_templates}") - if isinstance(self.tech_type_templates, list): - for template in self.tech_type_templates: - self.log(f"Template: {template.get('displayName')}, Selected: {self.selected_t_option_selected}") - if self.selected_t_option_selected == template.get("displayName"): - self.full_template = template - self.selected_t_template = template - self.log(f"Selected Template: {self.selected_t_template}") - break - else: - continue - self.log(f"Selected Template: {self.selected_t_template}") - - if self.selected_t_template is None: - self.log("No template selected, skipping placeholder display") - return - else: - specification = self.selected_t_template.get("specification") - if specification is None: - self.log("Selected template has no specification, skipping placeholder display") - return - else: - placeholderProperties = specification.get("placeholderProperty") - if placeholderProperties is None: - self.log("Selected template has no placeholder properties, skipping display") - return - else: - for placeholder in placeholderProperties: - if placeholder.get("class") != "PlaceholderProperty": - continue - else: - name = placeholder.get("name") or None - Description = placeholder.get("description") or None - Type = placeholder.get("dataType") or None - Example = placeholder.get("example") or None - Required = placeholder.get("required") or False - - # Sanitize the name for use as a CSS ID - safe_name = name.replace(" ", "_") if name else f"placeholder_{id(placeholder)}" - placeholder_text: TextArea = TextArea( - f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", - id=f"{safe_name}_placeholder_text_area", - read_only=True - ) - # Ensure TextArea is visible - placeholder_text.styles.height = 8 - - placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") - - # Mount the TextArea and the associated Input field into the ScrollableContainer - try: - load_point = self.query_one("#technology_type_templates_input") - await load_point.mount(placeholder_text, before="#template_submit_button") - await load_point.mount(placeholder_input, before="#template_submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") - continue - except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") - self.app.dismiss(416) - - def compose(self) -> ComposeResult: - """ Compose the UI components for the Technology_Type_Templatess screen.""" - yield Header(show_clock=True) - yield Static("Please complete the required fields and any optional fields you prefer:") - yield ScrollableContainer( - Static("Technology Type Template Input"), - Button("Submit", id="template_submit_button"), - id="technology_type_templates_input" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200", ) - - @on(Button.Pressed, "#template_submit_button") - def handle_submit_button_pressed(self, event: Button.Pressed) -> None: - """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") - save_input_data: dict = {} - for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") - save_input_data.update({input_widget.id: input_widget.value}) - self.log(f"Save input data: {save_input_data}") - self.dismiss(["input", save_input_data, self.full_template]) - - @on(Input.Changed, "#technology_type_templates_input") - def handle_input_changed(self, event: Input.Changed) -> None: - """ The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypesScreen.py b/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypesScreen.py deleted file mode 100644 index 1a87b153..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/TechnologyTypesScreen.py +++ /dev/null @@ -1,147 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -from pyegeria import load_app_config, settings -from textual import on -from textual.app import ComposeResult -from textual.containers import ScrollableContainer -from textual.screen import ModalScreen -from textual.widgets import Tree, Header, Static, Button, Footer -from textual.widgets._tree import TreeNode - - -class TechnologyTypesScreen(ModalScreen): - """Modal screen to display technology types in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("g", "go_back", "Back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, ttlist, user_name, user_pwd, user_kpts): - """Initialize the TechnologyTypes screen with a list of technology types.""" - super().__init__() - self.tech_type_list = ttlist - self.user_name = user_name - self.user_password = user_pwd - self.karma_points = user_kpts - self.tech_type_tree: Tree[str] = Tree(label="Technology Types", id="technology_types_tree") - self.tech_type_tree.root.expand() - self.tech_type_tree.auto_expand = True - self.selected_t_node = None - self.selected_t_node_label = None - self.node_id = None - self.node_status = "expanded" - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}" - self.sub_title = "Select a technology type" - - def compose(self) -> ComposeResult: - """ Compose and display the technology type screen""" - self.tech_type_tree.refresh() - if self.tech_type_list: - self.log(f"Technology types: {self.tech_type_list}, type: {type(self.tech_type_list)}") - self.render_tech_type_hierarchy_to_tree(self.tech_type_list, self.tech_type_tree) - else: - self.tech_type_tree.root.add("No technology types found", expand=True) - self.tech_type_tree.refresh() - self.log(f"Technology types tree: {self.tech_type_tree}") - - yield Header(show_clock=True) - yield ScrollableContainer( - Static("Display technology types in Egeria"), - self.tech_type_tree, - # Button("Select", id="select_tech_type_btn"), - id="technology_types_table" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200") - - def action_go_back(self) -> None: - """ The back option in the footer has been selected. Dismiss the screen.""" - self.dismiss("201") - - @on(Tree.NodeSelected) - def handle_tree_node_selected(self, event: Tree.NodeSelected) -> None: - """ The user has selected a node in the tree. """ - self.log(f"Tree node selected, node: {event.node}") - self.selected_t_node = event.node - self.selected_t_node_label = event.node.label - self.log(f"Selected node: {self.selected_t_node}, label: {self.selected_t_node.label}") - self.dismiss(str(self.selected_t_node.label)) - - @on(Tree.NodeCollapsed) - def handle_tree_node_collapsed(self, event: Tree.NodeCollapsed) -> None: - # handle the twisty to close a node in the tree - # logger.debug(f"TreeNodeCollapsed: {event.node.id}") - self.node_id = str(event.node.id) - self.node_status = "collapsed" - - @on(Tree.NodeExpanded) - def handle_tree_node_expanded(self, event: Tree.NodeExpanded) -> None: - # handle the twisty to open a node in the tree - # logger.debug(f"TreeNodeExpanded: {event.node.id}") - self.node_id = str(event.node.id) - self.node_status = "expanded" - - def action_expand(self): - # handle the hot key to expand/collapse all nodes in the tree (see bindings) - self.tech_type_tree = self.query_one("#technology_types_tree", Tree) - if self.node_status == "collapsed": - self.tech_type_tree.root.expand_all() - else: - self.tech_type_tree.root.collapse_all() - return - - def render_tech_type_hierarchy_to_tree(self, - data: dict | list[dict], - tree_or_node: Tree[str] | TreeNode[str], - label_attr: str = "displayName", - guid_attr: str = "guid", - children_attr: str = "subTypes" - ) -> None: - """Recursively render a DICT) hierarchy into a Textual Tree. - - Args: - data: The data structure from pyegeria.get_tech_type_hierarchy. - tree_or_node: The Tree object or a TreeNode to add children to. - label_attr: The attribute in the data for the node label. - guid_attr: The attribute in the data for the node data (GUID). - children_attr: The attribute in the data containing the list of children. - """ - if isinstance(data, list): - for item in data: - self.render_tech_type_hierarchy_to_tree(item, tree_or_node, label_attr, guid_attr, children_attr) - return - - if not isinstance(data, dict): - return - - label = str(data.get(label_attr) or data.get("Display Name") or "Unknown") - guid = str(data.get(guid_attr) or data.get("GUID") or "") - - # If it's a Tree, we add to root. If it's a TreeNode, we add to it. - if isinstance(tree_or_node, Tree): - node = tree_or_node.root.add(label, data=guid, expand=True) - else: - node = tree_or_node.add(label, data=guid, expand=True) - - children = data.get(children_attr) - if children and isinstance(children, list): - for child in children: - self.render_tech_type_hierarchy_to_tree(child, node, label_attr, guid_attr, children_attr) diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/__init__.py b/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/__init__.py deleted file mode 100644 index d7c7a021..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors.py b/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors.py deleted file mode 100644 index 88a65ef6..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors.py +++ /dev/null @@ -1,368 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import asyncio -import os -import sys -from pathlib import Path - -# Add the project root to sys.path to allow running this script from any directory -root_path = Path(__file__).resolve().parents[4] -if str(root_path) not in sys.path: - sys.path.append(str(root_path)) - -from pydantic import ValidationError -from textual import on -from textual.app import ComposeResult, App -from textual.containers import Container, Horizontal, Grid, Vertical -from textual.css.query import NoMatches -from textual.screen import Screen -from textual.widgets import Static, Button, Footer, Header, DataTable, Tree - -from pyegeria.view.base_report_formats import * -from pyegeria.view.format_set_executor import exec_report_spec - -# from my_connectors_splash_screen import SplashScreen - -CSS_PATH = ["my_connectors.tcss"] - -class ReportSpec(App): - - BINDINGS = [ - ("q", "quit", "Quit"), - ("escape", "back", "Back"), - ] - - class MainScreen(Screen): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def compose(self) -> ComposeResult: - self.heading = "Report Specs" - self.subheading = "Select a report spec to execute:" - self.description = "Select a report spec to execute." - # self.tech_type_list = "Tech-Type-Details" - # self.log(f"tech_type_datatable: {self.tech_type_datatable}, id: {self.tech_type_datatable.id}") - - self.tech_type_list = self.get_tech_type_list() - self.log(f"tech_type_list: {self.tech_type_list}, type: {type(self.tech_type_list)}") - try: - self.tech_type_datatable = self.query_one("#tech_type_datatable", DataTable) - self.tech_type_datatable.clear() - - except NoMatches: - self.tech_type_datatable: DataTable = DataTable(id="tech_type_datatable") - - for entry in self.tech_type_list: - dname = entry.get("Display Name", "No Display Name") - qname = entry.get("Qualified Name", "No Qualified Name") - desc = entry.get("Description", "No Description") - self.tech_type_datatable.add_row(dname, desc, qname) - continue - - self.tech_type_datatable.refresh() - - yield Header(show_clock=True) - yield Vertical( - Static(f"{self.subheading}, {self.description}", id="tech_type_list_header"), - Static(f"Server: {self.view_server} | Platform: {self.platform_url} | User: {self.user}"), - id="connection_info") - yield Container( - Static(f"Start of Technology Type List:", id="report_start"), - self.tech_type_datatable, - Static(f"End of Technology Type List:", id = "report_end"), - id = "tech_type_data_container", - ) - yield Horizontal( - Button("Quit", id="quit"), - Button("Back", id="back"), - id="action_row" - ) - yield Footer() - - def get_tech_type_list(self): - """ Get the list of tech types """ - self.tech_type_list: list = [{}] - self.list_unpack: dict = {} - self.tech_type_dict: dict = {} - try: - self.tech_type_response = exec_report_spec(format_set_name="Tech-Types", - output_format="DICT", - params={"search_string": "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password) - self.tech_type_list = self.tech_type_response.get("data") - self.tech_type_separated = ReportSpec.unpack_egeria_data(self.tech_type_response) - self.log(f"tech_type_separated: {self.tech_type_separated}") - except Exception as e: - self.log(f"Exception in get_tech_type_list: {e}") - self.tech_type_separated = {"Error": f"get_tech_type_list - {e}"} - except ValidationError as e: - self.log(f"ValidationError in get_tech_type_list: {e}") - self.tech_type_separated = {"ValidationError": f"get_tech_type_list - {e}"} - return self.tech_type_separated - - - class GridChildrenApp(App): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def compose(self): - self.query_one("#spec_grid", Grid).mount() - return - - SCREENS = { - "tech_type_details": "tech_type_details", - "_default": MainScreen - } - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.tech_type_datatable: DataTable = DataTable() - self.tech_type_datatable.id="tech_type_datatable" - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - self.selected_tech_type: str = "" - self.items: list = [] - self.processes: str = "" - - async def on_mount(self) -> None: - # push the main screen onto the stack so the queries will work correctly - await self.push_screen("_default") - - @on(DataTable.RowSelected, "#tech_type_datatable") - def on_datatable_row_selected(self, event: DataTable.RowSelected) -> None: - # def handle_datatable_row_selected(self, message: DataTable.RowSelected) -> None: - """ Handle ListView selection event """ - selected_row = event.row_key - selected_item = self.tech_type_datatable.get_row(selected_row) - self.log(f"Selected item: {selected_item} type: {type(selected_item)}") - selected_item_name = str(selected_item[0] or None) - selected_item_desc = str(selected_item[1] or None) - selected_item_qname = str(selected_item[2] or None) - if selected_item_qname: - self.selected_tech_type = selected_item_qname - else: - self.selected_tech_type = selected_item_name - - self.log(f"Selected tech type: {self.selected_tech_type}") - self.query_selected_tech_type(self.selected_tech_type) - - @on(Button.Pressed, "#quit") - def handle_button_pressed(self, event: Button.Pressed) -> None: - """ Quit the application gracefully with a "good" return code (200) """ - self.log(f"Quit button clicked") - self.exit(200) - return - - @on(Button.Pressed, "#back") - def handle_back_button_pressed(self, event: Button.Pressed) -> None: - """ Return to the main menu screen """ - self.log(f"Back button clicked") - self.tech_type_list = "" - self.selected_tech_type = "" - self.items.clear() - - def get_tech_type_list(self): - """ Get the list of tech types """ - self.tech_type_list: list = [{}] - self.list_unpack: dict = {} - self.tech_type_dict: dict = {} - try: - self.tech_type_response = exec_report_spec(format_set_name="Tech-Types", - output_format="DICT", - params = {"search_string": "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password) - self.tech_type_list = self.tech_type_response.get("data") - self.tech_type_separated = self.unpack_egeria_data(self.tech_type_response) - self.log(f"tech_type_separated: {self.tech_type_separated}") - - except Exception as e: - self.log(f"Exception in get_tech_type_list: {e}") - self.tech_type_dict = {"Error": f"get_tech_type_list - {e}"} - except ValidationError as e: - self.log(f"ValidationError in get_tech_type_list: {e}") - self.tech_type_dict = {"ValidationError": f"get_tech_type_list - {e}"} - return self.tech_type_separated - - def query_selected_tech_type(self, tech_type: str): - self.selected_tech_type = tech_type - # query the selected tech type - self.log(f"Executing tech type query: {self.selected_tech_type}") - self.parms = {"filter": self.selected_tech_type} - try: - response = exec_report_spec(format_set_name="Tech-Type-Processes", - params=self.parms, - output_format="DICT", - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password) - self.log(f"Return from tech type query:") - self.log(f"reponse: {response}") - except (ValidationError) as e: - self.log(f"ValidationError: {e}") - response = {"error": f"ValidationError: {e}"} - except Exception as e: - self.log(f"Exception: {e}") - response = {"error": f"Exception: {e}"} - if response is None: - response:dict = {"NoData": "No data returned from Egeria for tech type processes for {self.selected_tech_type}"} - if isinstance(response, dict) and "kind" in response: - if response.get("kind") == "Empty": - response:dict = {"NoData": "No data returned from Egeria for tech type processes for {self.selected_tech_type}"} - # remove superfluous data from response - response = self.unpack_egeria_data(response) - new_response: list[dict] = [{}] - counter = 0 - for item in response: - for key, value in item.items(): - if key != "Mermaid" or "Mermaid Specification": - new_response[counter].update({key: value}) - continue - else: - # omit the Mermaid fields from the response - continue - counter += 1 - continue - - # build and display tree of processes for selected tech type - self.build_tech_processes_tree(new_response) - - def unpack_egeria_data(self, data) -> dict: - """ Unpack the data returned from Egeria """ - output_data: list[dict] = [{}] - output_data.clear() - if isinstance(data, dict): - if "data" in data: - output_data = data.get("data") - elif isinstance(data, list): - for entry in data: - if isinstance(entry, dict): - output_data = entry.get("data") - elif isinstance(entry, list): - for subentry in entry: - if isinstance(subentry, dict): - output_data = subentry.get("data") - else: - output_data = {"error": "unknown data structure"} - else: - output_data = {"error": "unknown outer data structure"} - else: - output_data = {"error": "not dict or list", "shape": "data to unpack is not a recognized shape"} - self.log(f"output_data: {output_data}, {type(output_data)}") - # return the extracted data (dict) - return(output_data) - - def build_tech_processes_tree(self, response_data): - """ Create tree of processes for the selected tech_type""" - self.response = response_data - self.items.clear() - try: - self.proc_tree = self.query_one("#proc_tree", Tree) - self.proc_tree.clear() - except NoMatches: - self.proc_tree: Tree = Tree("Processes", id = "proc_tree") - except Exception as e: - self.log(f"Exception building tree: {e}") - self.exit(400) - root_data = "A list of Processes associated with the selected technology type" - self.proc_tree.auto_expand=True - # self.proc_tree.root.expand() - self.proc_tree.root.label = "Associated Processes" - self.proc_tree.root.data = root_data - tree_root = self.proc_tree.root - self.processes = "" - - if not self.response: - response_message = f"No data returned from Egeria for tech type processes for {self.selected_tech_type}" - self.response = [{"Display Name": response_message}] - if type(self.response) is dict: - self.response = [self.response] - if not isinstance(self.response, list): - self.response = [self.response] - self.log(f"response: {self.response}", type(self.response), len(self.response)) - for item in self.response: - process_node = tree_root.add(str(item.get("Display Name")), expand=True) - if len(self.response) > 0 and len(self.response[0]) >= 2: - for key, value in self.response: - if isinstance(value, dict): - self.log(f"key: {key}, {type(key)}, {value}") - sub_node = process_node.add(label=(str(key)), expand=True) - for subkey, subvalue in value.items(): - if isinstance(subvalue, dict): - self.log(f"subkey: {subkey}, {type(subkey)}, {subvalue}") - subprocess_node = sub_node.add(label=(str(subkey)), expand=True) - for subsubkey, subsubvalue in subvalue.items(): - self.log(f"subsubkey: {subsubkey}, {type(subsubkey)}, {subsubvalue}") - subprocess_node.add_leaf(label=(str(subsubkey)), expand=True) - continue - else: - self.log(f"subkey: {subkey}, {type(subkey)}, {subvalue}") - subprocess_node = sub_node.add_leaf(label=(str(subkey)), expand=True) - continue - else: - self.log(f"key: {key}, {type(key)}, {value}") - sub_node = tree_root.add_leaf(label=(str(key)), expand=True) - continue - else: - process_node.add_leaf(label=(str(key)), expand=True) - continue - else: - #if there is no input, skip - continue - - # remove the technology type list and mount the processes tree - self.query_one("#tech_type_data_container", Container).remove() - self.query_one("#tech_type_data_container", Container).mount(self.proc_tree, before="#report_end") - self.query_one("#tech_type_data_container", Container).refresh() - - self.heading = "Available Processes" - self.subheading = "Select a Process to fill in required variables:" - self.description = "A list of available processes, click on one" - # self.tech_processes_tree = TechTypeDetails(self.selected_tech_type, self.response_data) - - -def start_myc(): - try: - import pydevd_pycharm - pydevd_pycharm.settrace( - "127.0.0.1", - port=5679, - stdout_to_server=True, - stderr_to_server=True, - suspend=False # Set to False to let the application continue running after starting - ) - except ImportError: - print("pydevd-pycharm not installed or setup failed. Debugger won't be active.") - pass - except Exception as e: - print(f"Exception setting up debugger: {e}, execution will attempt to continue.") - pass - - app = ReportSpec() - os.environ.setdefault("EGERIA_PLATFORM_URL", "https://127.0.0.1:9443") - os.environ.setdefault("EGERIA_VIEW_SERVER", "qs-view-server") - os.environ.setdefault("EGERIA_USER", "erinoverview") - os.environ.setdefault("EGERIA_USER_PASSWORD", "secret") - app.run() - -if __name__ == "__main__": - app = ReportSpec() - os.environ.setdefault("EGERIA_PLATFORM_URL", "https://127.0.0.1:9443") - os.environ.setdefault("EGERIA_VIEW_SERVER", "qs-view-server") - os.environ.setdefault("EGERIA_USER", "erinoverview") - os.environ.setdefault("EGERIA_USER_PASSWORD", "secret") - app.run() diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors.tcss b/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors.tcss deleted file mode 100644 index 2cc0c237..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors.tcss +++ /dev/null @@ -1,149 +0,0 @@ -/* *********************************************************************************************** */ -/* PDX-License-Identifier: Apache-2.0 */ -/* Copyright Contributors to the ODPi Egeria project. */ -/* */ -/* This file provides a set of report specification related functions for my_egeria. */ -/* */ -/* Unless you are running Microsoft's Studio Product with the Textual Extension installed */ -/* the file will show multiple errors, for example I use the PyCharm IDE for whoich there are */ -/* no extensions for Textual, regarless of the errors the file will run correctly with Textual. */ -/* *********************************************************************************************** */ - -Screen { - layout: grid; - grid-size: 6 3; - grid-columns: 1fr 1fr; -} -.SplashScreen { - background: $surface; - border: solid cyan; -} -Button { - width: auto; - height: auto; - border: solid white; -} -Static { - width: 50%; - height: auto; - /*margin: 1 1;*/ - /*padding-bottom: 1;*/ -} -Label { - width: 50%; - height: auto; - padding-bottom: 1; -} -Input { - width: 50%; - height: auto; - /* margin: 1 1;*/ - padding-bottom: 1; -} -.spaced { - margin-bottom: 1; /* Adds 1 cell of space below the button */ - } -Text {} -.box { - height: 100%; - width: 100%; - border: solid green; -} -#container1 { - column-span: 2; - width: 100%; - height: 1fr; - padding: 1; -} -#container2 { - column-span: 2; - width: 100%; - height: 1fr; - padding: 1; -} -#container3 { - column-span: 4; - row-span: 2; - width: 100%; - height: 1fr; - padding: 1; -} -#container4 { - column-span:2; - row-span: 3; - width: 100%; - height: 1fr; - padding: 1; -} -#one_start { - width: 100%; - height: auto; -} -#two_start { - width: 100%; - height: auto; -} -#three_start { - width: 100%; - height: auto; -} -#four_start { - width: 100%; - height: auto; -} -#one_end { - width: 100%; - height: auto; -} -#two_end { - width: 100%; - height: auto; -} -#three_end { - width: 100%; - height: auto; -} -#four_end { - width: 100%; - height: auto; -} -#spec_datatable { - width: 100%; - height: auto; -} -#spec_extracted_datatable { - width: 100%; - height: auto; -} -#specoutput_datatable { - width: 100%; - height: auto; -} -#tech_table { - width: 100%; - height: auto; -} -#spec_datagrid { - width: 100% -} -#input_fields { - width: 100%; - height: auto; - padding-bottom: 1; -} -#comment1 { - width: 100%; - height: auto; -} -#connection_info { - width: 100%; - height: 10% -} -#tech_type_data_container { - width: 100%; - height: 80%; -} -#action_row { - width: 100%; - height: 10%; -} \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors_splash_screen.py b/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors_splash_screen.py deleted file mode 100644 index dea29816..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/Technology_Type_Demo/my_connectors_splash_screen.py +++ /dev/null @@ -1,119 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import Container -from textual.message import Message -from textual.screen import ModalScreen -from textual.widgets import Static, Label, Button, Footer, Header, TextArea - -CSS_PATH = ["tech_type_splash.tcss"] - -class SplashScreen(ModalScreen): - """ Egeria Exemplar UI Splash screen """ - - class SplashContinue(Message): - """ Message to indicate user is ready to continue to application""" - def __init__(self): - super().__init__() - - - def __init__(self) -> None: - super().__init__() - self.app_title = "My Egeria" - self.app_version = "6.0.0" - self.app_build_date = "2025-09-08" - self.app_build_time = "00:00" - self.app_build_author = "Peter Coldicott" - self.app_build_commit = "00000000000000000" - self.app_build_branch = "main" - self.app_build_platform = "MacOS" - self.welcome_text = ( - "\n\n" - "This is example UI code package that leverages the Textual/Rich open source UI Frameworks,\n" - "and the pyegeria package which is part of the Egeria Project.\n\n" - "The UI is written in Python and is certainly not meant to demonstrate best coding practices!\n\n" - "Textual/Rich frameworks originally authored by Will McGugan (Textualize).\n\n" - "My_Egeria SPDX-License-Identifier: Apache-2.0, " - "Copyright Contributors to the ODPi Egeria project.\n" - ) - - def compose(self) -> ComposeResult: - top = Container( - Label( - f"Welcome to {self.app_title} v{self.app_version} " - f"({self.app_build_date} {self.app_build_time})", - id="splash_title", - ), - TextArea(self.welcome_text, id="splash_text"), - Label( - f"Build Author: {self.app_build_author} | " - f"Commit: {self.app_build_commit} | " - f"Branch: {self.app_build_branch} | " - f"Platform: {self.app_build_platform}", - id="splash_meta", - ), - id="splash_top" - ) - yield Header(show_clock=True) - yield Static("Welcome to the Egeria Exemplar UI!") - yield top - yield Button("Continue", variant="primary", id="continue_splash") - yield Footer() - - async def on_mount(self): - - # Place content in top half, center horizontally - top = self.query_one("#splash_top", Container) - - top.styles.dock = "top" - top.styles.height = "50%" - top.styles.width = "100%" - top.styles.padding = (1, 2) - top.styles.align_horizontal = "center" - top.styles.align_vertical = "top" - top.styles.gap = 1 - - title = self.query_one("#splash_title", Label) - title.styles.text_align = "center" - title.styles.text_style = "bold" - - # Fixed visible rows for vertical centering math - VISIBLE_ROWS = 15 - - ta = self.query_one("#splash_text", TextArea) - ta.styles.width = "90%" - ta.styles.height = VISIBLE_ROWS # fixed rows to make vertical centering predictable - ta.styles.border = ("solid", "white") # solid white border - ta.styles.text_style = "bold" - ta.styles.padding = 1 - ta.styles.text_align = "center" # horizontal centering of text - - # Vertically center the content by adding top padding lines - raw_text = self.welcome_text.strip("\n") - content_lines = raw_text.splitlines() or [raw_text] - content_rows = len(content_lines) - - # Compute usable rows considering numeric padding - pad_top = int(getattr(ta.styles.padding, "top", 0) or 0) - pad_bottom = int(getattr(ta.styles.padding, "bottom", 0) or 0) - usable_rows = max(VISIBLE_ROWS - (pad_top + pad_bottom), 1) - top_pad = max((usable_rows - content_rows) // 2, 0) - - ta.value = ("\n" * top_pad) + raw_text - - meta = self.query_one("#splash_meta", Label) - meta.styles.text_align = "center" - - btn = self.query_one("#continue_splash", Button) - btn.styles.margin = (1, 0, 0, 0) - - @on(Button.Pressed, "#continue_splash") - def handle_continue_splash(self, event: Button.Pressed) -> None: - self.post_message(self.SplashContinue()) - return diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/Temp Data Display b/my_egeria/my_egeria/DemoCode/Deprecated/Temp Data Display deleted file mode 100644 index ab146a8f..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/Temp Data Display +++ /dev/null @@ -1,733 +0,0 @@ -{'kind': 'json', 'data': [{'Display Name': 'Asset Metadata Catalog', 'Qualified Name': 'Egeria:ValidMetadataValue:InventoryCatalog:deployedImplementationType-(Asset Metadata Catalog)', 'GUID': -'fbe0db34-3996-4be3-8294-422bcd8732d8', 'Description': 'A catalog of metadata that describes assets such as deployed systems, data sources, and processes.', 'Reference URL': None}, {'Display Name': 'Valid Metadata OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Valid Metadata OMVS)', 'GUID': 'eb9e44c2-41a4-4ded-aeaf-9d9cc46648f0', 'Description': 'Maintain and query valid values for metadata.', 'Reference URL': None}, {'Display -Name': 'Event Broker', 'Qualified Name': 'Egeria:ValidMetadataValue:EventBroker:deployedImplementationType-(Event Broker)', 'GUID': '6b5f1764-c6ee-4cb6-b279-13502c4ad22b', 'Description': 'A system that manages the asynchronous exchange of -messages (events) from once to potentially many recipients. Typically, this exchange of events is organized into groups called topics.', 'Reference URL': None}, {'Display Name': 'Script File', 'Qualified Name': -'Egeria:ValidMetadataValue:ScriptFile:deployedImplementationType-(Script File)', 'GUID': 'e348b1a3-3bc7-4921-be4f-572fecde32e4', 'Description': 'A file containing code that is interpreted when it is run.', 'Reference URL': None}, {'Display -Name': 'Open Metadata Repository', 'Qualified Name': 'Egeria:ValidMetadataValue:MetadataRepository:deployedImplementationType-(Open Metadata Repository)', 'GUID': '106a41b6-20a5-4c12-b62f-26c328d385f2', 'Description': 'A metadata repository -supporting open metadata types and interfaces.', 'Reference URL': None}, {'Display Name': 'Video Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:VideoFile:deployedImplementationType-(Video Data File)', 'GUID': -'4eca6f52-6d0d-4219-8428-b70a841276d0', 'Description': 'A file containing a video recording.', 'Reference URL': None}, {'Display Name': 'Project Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Project Manager OMVS)', 'GUID': '0c5661d1-777e-4a0e-a8b3-70bccf80d152', 'Description': 'Set up and manage projects.', 'Reference URL': None}, {'Display Name': 'Platform -Metadata Security Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Platform Metadata Security Connector)', 'GUID': '3e0bced9-b539-4b90-a7d7-2d2034e92af1', 'Description': 'A connector that -manages authorization requests to the OMAG Server Platform.', 'Reference URL': None}, {'Display Name': 'View Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(View Server)', 'GUID': -'aa29cd35-c14a-4998-ab53-acc09a363d4b', 'Description': 'A server that provides access to end user open metadata and governance services.', 'Reference URL': None}, {'Display Name': 'External Links OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(External Links OMVS)', 'GUID': '6213b592-2758-4e28-9fc3-d3a601fa48de', 'Description': 'Manages the creation and maintenance of external reference and external -identifiers.', 'Reference URL': None}, {'Display Name': 'OMAG Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(OMAG Server)', 'GUID': '91abf546-f8f3-4d51-aa37-8839582a3ddc', 'Description': 'A -server providing Open Metadata and Governance (OMAG) services.', 'Reference URL': None}, {'Display Name': 'Software Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Software Server)', 'GUID': -'a1a96c4d-5013-4458-9c6b-6520925af6a4', 'Description': 'A callable software server.', 'Reference URL': None}, {'Display Name': 'Collection Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Collection Manager OMVS)', 'GUID': '9345334f-8baf-4cd3-a14f-08b3b09cb101', 'Description': 'Build collections of asset and other metadata.', 'Reference URL': None}, -{'Display Name': 'Webpage File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Webpage File)', 'GUID': 'ac3c99d9-4e5a-430b-aac2-091443c95349', 'Description': 'Text file encoded using HTML tags plus optional -script tags, such as JavaScript and CSS.', 'Reference URL': None}, {'Display Name': 'YAML File', 'Qualified Name': 'Egeria:ValidMetadataValue:YAMLFile:deployedImplementationType-(YAML File)', 'GUID': 'd650b328-4cd0-4f0f-b0b5-41bc8951c902', -'Description': 'A file containing properties in YAML format. This it typically used for configuration.', 'Reference URL': None}, {'Display Name': 'Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(Data File)', 'GUID': '4985b866-8a79-4298-9b87-fcb5b0c42692', 'Description': 'A file containing data. This may be structured data, text or some form or media file.', 'Reference -URL': None}, {'Display Name': 'pyegeria', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareLibrary:deployedImplementationType-(pyegeria)', 'GUID': '3a3cba55-4a6d-45a4-becf-32e93041e6b0', 'Description': "Python language library supporting -calls to Egeria''s REST APIs.", 'Reference URL': None}, {'Display Name': 'File System Directory', 'Qualified Name': 'Egeria:ValidMetadataValue:FileFolder:deployedImplementationType-(File System Directory)', 'GUID': -'d1986b2f-cc82-4a5a-9e9a-559ec9e98edc', 'Description': 'A directory (folder) that holds files that are potential data sources in a file system.', 'Reference URL': None}, {'Display Name': 'Repository Governance OMES', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Repository Governance OMES)', 'GUID': '01f887c9-e73d-49bd-a279-1fee27cd21d0', 'Description': 'Dynamically govern open metadata repositories in the connected cohorts.', -'Reference URL': None}, {'Display Name': 'Executable File', 'Qualified Name': 'Egeria:ValidMetadataValue:ExecutableFile:deployedImplementationType-(Executable File)', 'GUID': '9b12b93a-60f7-495a-8e6a-4f2a75d1bed5', 'Description': 'A file -containing compiled code that can be executed.', 'Reference URL': None}, {'Display Name': 'Governance Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceEngine:deployedImplementationType-(Governance Engine)', 'GUID': -'4612ab00-dc47-48c6-94eb-552b13a70991', 'Description': 'Manages the execution of automated governance activity requested via engine actions.', 'Reference URL': None}, {'Display Name': 'People Organizer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(People Organizer OMVS)', 'GUID': '9a6c7c16-a1f6-4dc8-963d-c81d04d6e901', 'Description': 'Describe teams, roles and organizational structure.', 'Reference URL': None}, -{'Display Name': 'Governance Action', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceAction:deployedImplementationType-(Governance Action)', 'GUID': 'f86de45a-6482-4487-ab69-0e1005de8b1a', 'Description': 'A description of an action -that supports the implementation of a governance program.', 'Reference URL': None}, {'Display Name': 'Engine Host', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Engine Host)', 'GUID': -'5cd74a31-9a1f-40c6-8ce2-f0d803b76db8', 'Description': 'A server that runs governance engines.', 'Reference URL': None}, {'Display Name': 'Integration Connector', 'Qualified Name': -'Egeria:ValidMetadataValue:IntegrationConnector:deployedImplementationType-(Integration Connector)', 'GUID': '3870ee0a-d4a3-419f-a6d0-1440c60e5c00', 'Description': 'Connector that manages metadata exchange with a third party technology.', -'Reference URL': None}, {'Display Name': 'Software Capability', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareCapability:deployedImplementationType-(Software Capability)', 'GUID': 'a51e49cd-baa1-4195-9766-e97dae5bc7a4', 'Description': -'A callable software capability supporting specific types of assets.', 'Reference URL': None}, {'Display Name': 'CSV Tabular Data Set Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSetCollection:deployedImplementationType-(CSV Tabular Data Set Collection)', 'GUID': 'caec0804-102b-4364-be07-2eab863efdef', 'Description': 'A collection of logically related CSV files each representing a -tabular data set.', 'Reference URL': None}, {'Display Name': 'Apache Superset', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Apache Superset)', 'GUID': '1ab15ae2-b937-496a-94a5-dea035ccf5c4', -'Description': 'Manages the definition and display of reports.', 'Reference URL': None}, {'Display Name': 'Document File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Document File)', 'GUID': -'54b9def5-f543-4259-afa5-10c89c38313d', 'Description': 'A data file containing formatted text.', 'Reference URL': None}, {'Display Name': 'Governance Action Engine', 'Qualified Name': -'Egeria:ValidMetadataValue:GovernanceActionEngine:deployedImplementationType-(Governance Action Engine)', 'GUID': '6195bb4b-8fce-43f8-a92a-8ec12bf4a5b9', 'Description': 'A governance engine that runs governance action services.', 'Reference -URL': None}, {'Display Name': 'Metadata Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Server)', 'GUID': 'a672aa09-fa2d-4dc8-86c1-a8ac9a993982', 'Description': 'A software server that -supports the storage of metadata.', 'Reference URL': None}, {'Display Name': 'Android Phone', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Android Phone)', 'GUID': -'357bd44d-0325-4d99-a0be-203ba77ce713', 'Description': 'A smartphone that runs the Android operating system.', 'Reference URL': None}, {'Display Name': 'Apple iPhone', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple iPhone)', 'GUID': '6330b52b-1e35-497e-8721-b94b94cb45ab', 'Description': 'A smartphone from Apple Inc. that runs the iOS operating system.', 'Reference URL': -None}, {'Display Name': 'Governance Action Process', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionProcess:deployedImplementationType-(Governance Action Process)', 'GUID': '0d970e05-6310-49dd-863f-c73401211140', 'Description': -'A modelled workflow process to perform a governance action.', 'Reference URL': None}, {'Display Name': 'Marquez Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Marquez Server)', 'GUID': -'6f559c4c-2c94-43a5-a3e6-089f62dfe39e', 'Description': 'A data catalog for data observability.', 'Reference URL': None}, {'Display Name': 'CSV Tabular Data Set', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(CSV Tabular Data Set)', 'GUID': 'ad10c3c7-e1f2-45d0-8318-2cae98129610', 'Description': 'A CSV file accessed as a tabular data set.', 'Reference URL': None}, {'Display -Name': 'Product Catalog OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Product Catalog OMVS)', 'GUID': 'c386df0a-a24e-4f64-afa8-aa09f3fdbe8d', 'Description': 'Supports researching the available -digital products and managing the subscriptions.', 'Reference URL': None}, {'Display Name': 'Jupyter Notebook File', 'Qualified Name': 'Egeria:ValidMetadataValue:JSONFile:deployedImplementationType-(Jupyter Notebook File)', 'GUID': -'15dfcf08-d73c-48ae-94e7-dfc3bc916265', 'Description': 'Text file encoded in JSON that describes a mixture of python code and descriptive text.', 'Reference URL': None}, {'Display Name': 'REST API Endpoint', 'Qualified Name': -'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(REST API Endpoint)', 'GUID': '770c5be2-a5fd-44a7-96d0-96468b9d6473', 'Description': 'A callable network address supporting the REST protocol.', 'Reference URL': None}, {'Display -Name': 'Open Connector Framework (OCF) Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Open Connector Framework (OCF) Connector)', 'GUID': 'df24f7a6-57e6-4991-ae7a-1dfbdf48fd55', -'Description': 'A pluggable software component that conforms to the Open Connector Framework (OCF).', 'Reference URL': None}, {'Display Name': 'Asset', 'Qualified Name': 'Egeria:ValidMetadataValue:Asset:deployedImplementationType-(Asset)', -'GUID': '0e544ca8-f5f7-49dc-ab78-6d4caedfb397', 'Description': 'A description of a digital resource.', 'Reference URL': None}, {'Display Name': 'UNIX File System', 'Qualified Name': -'Egeria:ValidMetadataValue:FileSystem:deployedImplementationType-(UNIX File System)', 'GUID': '93e1b6df-f597-4d20-b846-7ece880478dc', 'Description': 'A Unix capability that manages hierarchically organized files on persistent storage.', -'Reference URL': None}, {'Display Name': 'Community Matters OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Community Matters OMVS)', 'GUID': '977629ef-a3c3-4deb-bd1f-4b96345c42db', -'Description': 'Manages the definitions of communities, their leaders and membership.', 'Reference URL': None}, {'Display Name': 'Template Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Template Manager OMVS)', 'GUID': 'e2d3906d-c6f4-45cf-ab5e-36e9d1fb4722', 'Description': 'Retrieve and refine the templates for use during cataloguing.', 'Reference URL': -None}, {'Display Name': 'Connection Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Connection Maker OMVS)', 'GUID': '47fe7a5d-6ba2-446f-823f-fe48ad3d8613', 'Description': 'Manages the -creation and maintenance of connections, connector types and endpoints.', 'Reference URL': None}, {'Display Name': 'Business Application', 'Qualified Name': 'Egeria:ValidMetadataValue:Application:deployedImplementationType-(Business -Application)', 'GUID': '6ac87de8-d01f-46bf-8881-0ea3204e716b', 'Description': 'Software supporting a business function.', 'Reference URL': None}, {'Display Name': 'Data Asset', 'Qualified Name': -'Egeria:ValidMetadataValue:DataAsset:deployedImplementationType-(Data Asset)', 'GUID': 'df79a55a-8cd8-4cf5-a35c-c8bea53db0c4', 'Description': 'A collection of data, either at rest or in motion.', 'Reference URL': None}, {'Display Name': -'Apple iPad', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple iPad)', 'GUID': '92d0e4c8-d7f5-4e8b-ad30-f6f20aaead98', 'Description': 'A tablet from Apple Inc. that runs the iOS operating -system.', 'Reference URL': None}, {'Display Name': 'JSON Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:JSONFile:deployedImplementationType-(JSON Data File)', 'GUID': 'f8cd55d2-394e-4589-b16b-b100eeb6f158', 'Description': 'A -description of a file that follows the JavaScript Object Notation specification.', 'Reference URL': None}, {'Display Name': 'Lineage Linker OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Lineage Linker OMVS)', 'GUID': 'ee76a68f-884e-4198-81e2-3c3da03cc8d1', 'Description': 'Manages the creation and maintenance of lineage relationships.', 'Reference URL': -None}, {'Display Name': 'Open Metadata Archive Store Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Open Metadata Archive Store Connector)', 'GUID': -'b0dcf516-4440-4cd3-980b-7cc5eccb9d43', 'Description': 'Reads and writes open metadata types and instances to an open metadata archive.', 'Reference URL': None}, {'Display Name': 'Audio Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:AudioFile:deployedImplementationType-(Audio Data File)', 'GUID': 'e08ff316-5e85-4088-9cd5-d737ed81ccc2', 'Description': 'A file containing an audio recording.', 'Reference URL': None}, {'Display Name': 'Metadata -Access Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Access Server)', 'GUID': '940dda2a-e86d-4ba2-b889-500431d47f70', 'Description': 'A server that provides access to one or more -open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Administration Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Administration Services)', 'GUID': -'d8e2d72a-cea9-404f-867d-775a2fec88a5', 'Description': 'Platform service providing configuration of services for an Open Metadata and Governance server (OMAG Server).', 'Reference URL': None}, {'Display Name': 'Apache Kafka Topic', 'Qualified -Name': 'Egeria:ValidMetadataValue:Topic:deployedImplementationType-(Apache Kafka Topic)', 'GUID': '20f81045-aa24-4374-92cb-d045ac3e24f6', 'Description': 'An event topic supporting high-speed, reliable event exchange.', 'Reference URL': None}, -{'Display Name': 'PostgreSQL database manager (RDBMS)', 'Qualified Name': 'Egeria:ValidMetadataValue:DatabaseManager:deployedImplementationType-(PostgreSQL database manager (RDBMS))', 'GUID': 'cdfedb9c-8020-422d-b89e-40c57df22bd0', -'Description': 'The PostgreSQL capability that manages collections of data called relational databases which in turn are organized into a tabular format and accessed via the Structured Query Language (SQL).', 'Reference URL': None}, {'Display -Name': 'Watchdog Action OMES', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Watchdog Action OMES)', 'GUID': '0be60ce5-f7d8-4b41-8f93-9c30851bcd28', 'Description': 'Monitors for particular -situations/events and notifies registered subscribers.', 'Reference URL': None}, {'Display Name': 'Smart Phone', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Smart Phone)', 'GUID': -'6f617a49-2f25-44d0-b507-d2e4ac3db603', 'Description': 'A two-handed portable device with network, touchscreen, and battery power.', 'Reference URL': None}, {'Display Name': 'Open Governance Framework Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Governance Framework Services)', 'GUID': '23e704d7-287c-4672-9666-04519563edbf', 'Description': 'Support open governance services.', 'Reference URL': None}, {'Display -Name': 'Survey Action Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:SurveyActionEngine:deployedImplementationType-(Survey Action Engine)', 'GUID': 'fe4bcc14-985b-421d-b98e-3fddbc884811', 'Description': 'A governance engine that runs -survey action services.', 'Reference URL': None}, {'Display Name': 'Data Feed', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFeed:deployedImplementationType-(Data Feed)', 'GUID': '8ff94698-32b6-4765-bcc6-b1357bc1d55b', 'Description': 'An -event-based data source', 'Reference URL': None}, {'Display Name': 'Integration Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Integration Server)', 'GUID': -'f7ddc726-7f68-466b-a1e5-cf365d31714b', 'Description': 'A software server that supports the exchange of data.', 'Reference URL': None}, {'Display Name': 'Open Metadata Archive', 'Qualified Name': -'Egeria:ValidMetadataValue:ArchiveFile:deployedImplementationType-(Open Metadata Archive)', 'GUID': '93d47fe4-8b62-4c1e-af89-26743abc6bc8', 'Description': 'An archive file containing pre-defined metadata types and instances.', 'Reference -URL': None}, {'Display Name': 'Repository Governance Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:RepositoryGovernanceEngine:deployedImplementationType-(Repository Governance Engine)', 'GUID': '9a754469-05a1-4d4c-b3b5-6b3ee4207ac7', -'Description': 'A governance engine that runs repository governance services.', 'Reference URL': None}, {'Display Name': 'Subject Area OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Subject -Area OMVS)', 'GUID': 'c872a3bb-9945-445f-afc8-9d73eaf3c398', 'Description': 'Manages the definition of subject areas and reporting on their contents.', 'Reference URL': None}, {'Display Name': 'Platform Operational Services', 'Qualified -Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Platform Operational Services)', 'GUID': '5883c7fb-b339-4dce-98dc-887dfaf8371a', 'Description': 'A platform service that provides information about the registered -services and connectors available in an OMAG Server Platform along with services to control and query information about the OMAG Servers running on the platform.', 'Reference URL': None}, {'Display Name': 'Apple MacBook Pro', 'Qualified -Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple MacBook Pro)', 'GUID': 'b7327658-41c3-4a53-877b-dd1b7a1dbac4', 'Description': 'A professional laptop supplied by Apple Inc that runs the macOS operating -system.', 'Reference URL': None}, {'Display Name': 'Server Metadata Security Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Server Metadata Security Connector)', 'GUID': -'5aa6a746-9dd4-4cbb-b1aa-e05707a156ce', 'Description': 'A connector that manages authorization requests to the OMAG Server.', 'Reference URL': None}, {'Display Name': 'Data Feed', 'Qualified Name': -'Egeria:ValidMetadataValue:Topic:deployedImplementationType-(Data Feed)', 'GUID': 'e3e25371-65da-47a8-bb73-feb7a00b2afe', 'Description': 'An organized event-based data source that supports subscription to a named data feed.', 'Reference URL': -None}, {'Display Name': 'PostgreSQL Relational Database Schema', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedDatabaseSchema:deployedImplementationType-(PostgreSQL Relational Database Schema)', 'GUID': -'e46811ac-8b86-42c3-aaea-2fe55f474044', 'Description': 'A database schema hosted on a PostgreSQL relational database server capable of being called through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Integration Daemon -Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Integration Daemon Services)', 'GUID': 'fbd8b372-d355-49be-af75-c38ee1f0726d', 'Description': 'Hosts integration connectors that are -exchanging metadata with third party technologies.', 'Reference URL': None}, {'Display Name': 'Governance Service', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceService:deployedImplementationType-(Governance Service)', 'GUID': -'f5e38a2f-911d-4718-9039-0eaa25f5b9ee', 'Description': 'Provides the description of a component that implements an automated governance activity.', 'Reference URL': None}, {'Display Name': 'Watchdog Action Service', 'Qualified Name': -'Egeria:ValidMetadataValue:WatchdogActionService:deployedImplementationType-(Watchdog Action Service)', 'GUID': 'd4af2e0f-78db-4b93-bf5d-53632b33c67a', 'Description': 'A connector that coordinates notifications based on situations/events.', -'Reference URL': None}, {'Display Name': 'Tabular Data Set', 'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(Tabular Data Set)', 'GUID': 'f3d81f48-1824-4608-962e-90f3f239e861', 'Description': 'A logical -collection of data, either at rest or in motion, organized into a tabular (columnar) format.', 'Reference URL': None}, {'Display Name': 'Engine Host Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Engine Host Services)', 'GUID': '5195a98e-9e4a-41f1-b00d-1a26cee3e20d', 'Description': 'Host one or more engine services that are actively managing governance of open -metadata and the digital landscape.', 'Reference URL': None}, {'Display Name': 'Archive File', 'Qualified Name': 'Egeria:ValidMetadataValue:ArchiveFile:deployedImplementationType-(Archive File)', 'GUID': -'6148ab2c-26e3-4cb5-870d-d95d070c910f', 'Description': 'A file containing compressed files. These files may be organized into a directory (folder) structure.', 'Reference URL': None}, {'Display Name': 'Data Officer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Officer OMVS)', 'GUID': 'f81202aa-0572-47ae-a6db-5e71459addf2', 'Description': 'Manage the governance of data.', 'Reference URL': None}, {'Display Name': 'Properties -File', 'Qualified Name': 'Egeria:ValidMetadataValue:PropertiesFile:deployedImplementationType-(Properties File)', 'GUID': 'f83e006c-a2bf-40e5-8c04-45e93dee4967', 'Description': 'A file containing a list of properties, typically used for -configuration of some software.', 'Reference URL': None}, {'Display Name': 'Data Discovery OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Discovery OMVS)', 'GUID': -'86efdf03-9cce-4572-bf8e-4b6cca299161', 'Description': 'Define and search for new data resources.', 'Reference URL': None}, {'Display Name': 'OMRS Repository Connector', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(OMRS Repository Connector)', 'GUID': 'a6e46bec-a80a-417e-931f-f6d044ff0f33', 'Description': 'Maps open metadata repository calls defined by the Open Metadata -Repository Services (OMRS) to a metadata repository API and event notifications.', 'Reference URL': None}, {'Display Name': 'Integration Daemon', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Integration Daemon)', 'GUID': 'b2d8d57f-df68-4280-9a41-3df830c3444f', 'Description': 'A server that runs integration connectors that synchronize metadata between different -types of technologies.', 'Reference URL': None}, {'Display Name': 'Digital Business OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Digital Business OMVS)', 'GUID': -'4a74aa92-3fc7-4160-904a-cfd1bb384e43', 'Description': 'Manages the definition and maintenance of the business context description used to identify where business value is being derived from.', 'Reference URL': None}, {'Display Name': -'Dynamic Integration Group', 'Qualified Name': 'Egeria:ValidMetadataValue:IntegrationGroup:deployedImplementationType-(Dynamic Integration Group)', 'GUID': '7a49ed4e-6810-4520-836d-aebc8ee341d7', 'Description': 'Provides the list of -integration connectors that should run in an Integration Daemon. The Integration Daemon is configured with the qualified names of the integration group(s) that provide its connector list.', 'Reference URL': None}, {'Display Name': -'Governance Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Governance Officer OMVS)', 'GUID': '3823da46-43d3-4859-add0-59204bf904dd', 'Description': 'Maintain governance definitions -used to define any governance domain.', 'Reference URL': None}, {'Display Name': 'File', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(File)', 'GUID': 'b5bb473d-947e-4ffb-8605-b8364c5ce85c', 'Description': -'A description of a file stored in a file system.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Tabular Data Set Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSetCollection:deployedImplementationType-(PostgreSQL Tabular Data Set Collection)', 'GUID': 'c113b127-ae8d-417f-90f2-92108c141270', 'Description': 'A database schema hosted on a PostgreSQL relational -database server that has the tabular data set collection interface.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(PostgreSQL Server)', -'GUID': 'd3ea92d0-c53b-4cc8-84d8-764648323d26', 'Description': 'A database server running the PostgreSQL software.', 'Reference URL': None}, {'Display Name': 'Smartphone', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Smartphone)', 'GUID': '94a91316-9f23-4ab5-a907-3c8c6cfea611', 'Description': 'A handheld portable device with network, touchscreen, and battery power.', 'Reference URL': -None}, {'Display Name': 'Metadata Expert OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Metadata Expert OMVS)', 'GUID': '3b1481ad-8b18-48b9-97c1-0a627d97dd65', 'Description': 'Provides advanced -metadata management features for experts in open metadata.', 'Reference URL': None}, {'Display Name': 'Keystore File', 'Qualified Name': 'Egeria:ValidMetadataValue:KeyStoreFile:deployedImplementationType-(Keystore File)', 'GUID': -'409f12a7-b4a3-4e0f-a9bd-8094813346d6', 'Description': 'An encrypted data store containing authentication and related security information.', 'Reference URL': None}, {'Display Name': 'Data Folder', 'Qualified Name': -'Egeria:ValidMetadataValue:DataFolder:deployedImplementationType-(Data Folder)', 'GUID': '0b18ef33-84ee-476d-92fc-82c109824261', 'Description': 'A directory (folder) that holds files representing a single data source.', 'Reference URL': -None}, {'Display Name': 'Endpoint', 'Qualified Name': 'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(Endpoint)', 'GUID': '69a0e224-70d7-43e4-a4af-ecd0d4fc54eb', 'Description': 'A callable network address.', 'Reference URL': -None}, {'Display Name': 'DevOps Pipeline OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(DevOps Pipeline OMVS)', 'GUID': 'c8c83907-20be-4f21-9ee6-64ac21aafd83', 'Description': 'Maintain the -metadata about the assets managed by a devops pipeline.', 'Reference URL': None}, {'Display Name': 'Parquet Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:ParquetFile:deployedImplementationType-(Parquet Data File)', 'GUID': -'734d12c6-0459-42ef-bd41-2ce6e45559ba', 'Description': 'A data file which is formatted using the Apache Parquet format.', 'Reference URL': None}, {'Display Name': 'Action Author OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Action Author OMVS)', 'GUID': '559775a7-ce1e-4f6f-973c-05b86ed2d57f', 'Description': 'Maintain definitions of governance actions such as governance action processes and -governance action types.', 'Reference URL': None}, {'Display Name': 'Governance Action Type', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionType:deployedImplementationType-(Governance Action Type)', 'GUID': -'eda3449b-d928-49aa-a67c-bc934e2a2102', 'Description': 'A modelled governance action with a single step.', 'Reference URL': None}, {'Display Name': 'Avro Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:AvroFile:deployedImplementationType-(Avro Data File)', 'GUID': 'f0fee71a-ed03-44f7-aee3-633732473250', 'Description': 'A description of a file that follows the Apache Avro specification.', 'Reference URL': None}, -{'Display Name': 'Repository Governance Service Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:RepositoryGovernanceService:deployedImplementationType-(Repository Governance Service Connector)', 'GUID': -'958fe597-6db9-4c0f-bf3e-54331934b792', 'Description': 'A connector that dynamically governs the activity of the open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Watchdog Action Engine', 'Qualified Name': -'Egeria:ValidMetadataValue:WatchdogActionEngine:deployedImplementationType-(Watchdog Action Engine)', 'GUID': 'ac264c61-8b57-4afd-a505-d0a2c1fd54cd', 'Description': 'A governance engine that runs watchdog action services.', 'Reference URL': -None}, {'Display Name': 'Log File', 'Qualified Name': 'Egeria:ValidMetadataValue:LogFile:deployedImplementationType-(Log File)', 'GUID': 'b924be69-f87f-4807-8261-5a899a3508dd', 'Description': 'Identifies a data file as one containing log -records.', 'Reference URL': None}, {'Display Name': 'Relational database manager (RDBMS)', 'Qualified Name': 'Egeria:ValidMetadataValue:DatabaseManager:deployedImplementationType-(Relational database manager (RDBMS))', 'GUID': -'dd43d7d7-7c32-42e8-8112-67fa5d5ed15f', 'Description': 'A capability that manages collections of data called relational databases which in turn are organized into a tabular format and accessed via the Structured Query Language (SQL).', -'Reference URL': None}, {'Display Name': 'Program File', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(Program File)', 'GUID': '822af572-069f-4f89-8a52-9a7d5a70092f', 'Description': 'A file containing -program logic.', 'Reference URL': None}, {'Display Name': 'Automated Curation OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Automated Curation OMVS)', 'GUID': -'babf18f0-3f11-439b-8fe0-494aab24f58d', 'Description': "Manage Egeria''s automation services.", 'Reference URL': None}, {'Display Name': '3D Image Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:ThreeDImageFile:deployedImplementationType-(3D Image Data File)', 'GUID': '8c7cc3be-365a-48db-bfba-a64da2a441e4', 'Description': 'A file containing a three-dimensional image.', 'Reference URL': None}, {'Display -Name': 'Apache Airflow Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Apache Airflow Server)', 'GUID': 'f55a0afd-656c-4550-994b-52c8acfc86f7', 'Description': 'Runs data movement and -transformation pipelines.', 'Reference URL': None}, {'Display Name': 'Multi Language OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Multi Language OMVS)', 'GUID': -'8905a42b-bffc-4e4d-b3af-55c90411914f', 'Description': 'Manages the creation and maintenance of translations for open metadata elements.', 'Reference URL': None}, {'Display Name': 'JDBC Endpoint', 'Qualified Name': -'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(JDBC Endpoint)', 'GUID': 'f23c929f-c69a-445e-a0a5-6ae20206e52d', 'Description': 'A callable network address supporting the JDBC protocol.', 'Reference URL': None}, {'Display -Name': 'Docker Container', 'Qualified Name': 'Egeria:ValidMetadataValue:VirtualContainer:deployedImplementationType-(Docker Container)', 'GUID': '8020b056-79be-4378-9e5a-44bc6f3453b5', 'Description': 'A virtual container using the docker -platform.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Tabular Data Set', 'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(PostgreSQL Tabular Data Set)', 'GUID': -'aa50ce4c-917d-4880-a987-d6c771b822b2', 'Description': 'A database table hosted on a PostgreSQL relational database server that has the tabular data set interface.', 'Reference URL': None}, {'Display Name': 'Database Server', 'Qualified -Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Database Server)', 'GUID': 'ff82019c-8faa-4ac3-8f99-c3a8477743e9', 'Description': 'A software server that supports databases.', 'Reference URL': None}, {'Display -Name': 'JDBC Relational Database', 'Qualified Name': 'Egeria:ValidMetadataValue:RelationalDatabase:deployedImplementationType-(JDBC Relational Database)', 'GUID': '878b6fb6-a203-42b5-a04c-4f40de319de9', 'Description': 'A database hosted on a -relational database server callable through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Survey Action Service', 'Qualified Name': 'Egeria:ValidMetadataValue:SurveyActionService:deployedImplementationType-(Survey Action -Service)', 'GUID': 'f3bb3f15-6d09-451d-804c-23ae00529d06', 'Description': 'A connector that coordinates asset surveys.', 'Reference URL': None}, {'Display Name': 'Server Operations', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Server Operations)', 'GUID': '2cf2d01c-30a4-4d37-8347-987f5ecad623', 'Description': 'Supports the start up and shutdown of OMAG Servers.', 'Reference URL': None}, -{'Display Name': 'Open Metadata Repository Services (OMRS)', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata Repository Services (OMRS))', 'GUID': '4f0083f2-32b2-4c22-b78e-e4ff126864a3', -'Description': 'Manages the synchronization, retrieval and maintenance of metadata stored in open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Data Set', 'Qualified Name': -'Egeria:ValidMetadataValue:DataAsset:deployedImplementationType-(Data Set)', 'GUID': '44ad5f7e-804d-4d24-a837-0e7021b12996', 'Description': 'A logical collection of data, either at rest or in motion.', 'Reference URL': None}, {'Display Name': -'Metadata Repository', 'Qualified Name': 'Egeria:ValidMetadataValue:MetadataRepository:deployedImplementationType-(Metadata Repository)', 'GUID': '3e88a901-804c-4a4a-a89c-983f76e7966b', 'Description': 'A data store containing metadata.', -'Reference URL': None}, {'Display Name': 'Runtime Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Runtime Manager OMVS)', 'GUID': '63396929-d549-4c87-b2bb-2f3f82e31796', 'Description': -'Retrieve configuration and status from platforms and servers.', 'Reference URL': None}, {'Display Name': 'API Manager', 'Qualified Name': 'Egeria:ValidMetadataValue:APIManager:deployedImplementationType-(API Manager)', 'GUID': -'18dbff2f-0909-4e85-bbe2-5b5332ddf1f8', 'Description': 'A software capability that provides callable APIs.', 'Reference URL': None}, {'Display Name': 'Data Designer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Designer OMVS)', 'GUID': 'ee1e73dd-7161-4df1-9732-0c8f91e46684', 'Description': 'Create data specifications to describe data requirements.', 'Reference URL': None}, -{'Display Name': 'Data File Collection', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFileCollection:deployedImplementationType-(Data File Collection)', 'GUID': '6cfedd7c-4aaa-49f2-8e67-b70949928ce6', 'Description': 'A collection of -logically related files representing a single data source.', 'Reference URL': None}, {'Display Name': 'Reference Data OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Reference Data OMVS)', -'GUID': '4b83dd72-f74f-4291-b43a-995f6c31aa80', 'Description': 'Work with code tables and associated reference data.', 'Reference URL': None}, {'Display Name': 'Small Form Factor (SFF) Computer', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Small Form Factor (SFF) Computer)', 'GUID': '4636a5c7-b1e9-4a65-8143-97e362efcc07', 'Description': 'A small hardware server enclosed in a protective case that can sit -under a desk.', 'Reference URL': None}, {'Display Name': 'Software Component', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(Software Component)', 'GUID': -'29b4afae-7000-4837-a225-d37bc036d503', 'Description': 'A deployable software component.', 'Reference URL': None}, {'Display Name': 'Conformance Suite Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Conformance Suite Services)', 'GUID': '9984876e-2593-453e-b319-1fe7b096fea9', 'Description': 'Run automated open metadata conformance suite services.', 'Reference URL': -None}, {'Display Name': 'Governance Action Service', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionService:deployedImplementationType-(Governance Action Service)', 'GUID': 'd77773c9-cc7c-4571-8472-f2764aa1471a', 'Description': -'A connector that coordinates governance of digital resources and metadata.', 'Reference URL': None}, {'Display Name': 'REST API Manager', 'Qualified Name': 'Egeria:ValidMetadataValue:APIManager:deployedImplementationType-(REST API Manager)', -'GUID': '0a9947b4-6686-4710-a64f-645f9ce430bd', 'Description': 'A software capability that provides callable APIs supporting the REST protocol.', 'Reference URL': None}, {'Display Name': 'Product Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Product Manager OMVS)', 'GUID': '1eb4a943-c0d4-46d6-acdf-c35330cc3c5e', 'Description': 'Supports the development and maintenance of digital products.', 'Reference URL': -None}, {'Display Name': 'Actor Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Actor Manager OMVS)', 'GUID': 'beb3afb3-027c-479c-bec0-aa239fbcb658', 'Description': 'Manages the -definitions of user identities, actor profiles, contact details and actor roles.', 'Reference URL': None}, {'Display Name': 'Source Code File', 'Qualified Name': 'Egeria:ValidMetadataValue:SourceCodeFile:deployedImplementationType-(Source -Code File)', 'GUID': 'd615f94a-fc78-4f23-8b35-f4af454bc01e', 'Description': 'A text file containing a program written in a language that needs to be complied into an executable form before it can run.', 'Reference URL': None}, {'Display -Name': 'Laptop Computer', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Laptop Computer)', 'GUID': '38ecbb12-c172-461c-aeac-8d7da0502cf5', 'Description': 'A portable computer with a screen, -keyboard, and battery power.', 'Reference URL': None}, {'Display Name': 'Data Engineer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Engineer OMVS)', 'GUID': -'98d78723-7b6c-4ca9-9403-0c4d6a944552', 'Description': 'Manages the creation and maintenance of data pipelines, open metadata templates and reference data.', 'Reference URL': None}, {'Display Name': 'Location Arena OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Location Arena OMVS)', 'GUID': '8b74f416-28a6-4a8c-ab08-6e32eb3ec8e1', 'Description': 'Manages the creation and maintenance of locations.', 'Reference URL': None}, -{'Display Name': 'Bare Metal Computer', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Bare Metal Computer)', 'GUID': 'f593a0c5-4a71-4fd9-9963-dbe6ba464865', 'Description': 'A computer (hardware) -with an operating system for running software.', 'Reference URL': None}, {'Display Name': 'Schema Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Schema Maker OMVS)', 'GUID': -'5fef72f9-7580-4dd1-989a-6946555c0814', 'Description': 'Manages the creation and maintenance of schema elements.', 'Reference URL': None}, {'Display Name': 'JDBC Relational Database Schema', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedDatabaseSchema:deployedImplementationType-(JDBC Relational Database Schema)', 'GUID': '10eb9965-80d1-447a-a207-68e39c2ee755', 'Description': 'A database schema hosted on a relational database server callable -through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Build Instruction File', 'Qualified Name': 'Egeria:ValidMetadataValue:BuildInstructionFile:deployedImplementationType-(Build Instruction File)', 'GUID': -'d5fa0c48-e24c-44b4-910c-a4f29ec0f889', 'Description': 'A file containing instructions to run a build of a software artifact or system.', 'Reference URL': None}, {'Display Name': 'Raster Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:RasterFile:deployedImplementationType-(Raster Data File)', 'GUID': 'c9800490-8706-4d91-bd8f-28813239add3', 'Description': 'A file containing an image as a matrix of pixels.', 'Reference URL': None}, {'Display Name': -'Security Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Security Officer OMVS)', 'GUID': '9bd9e4c0-c994-447b-b1c5-5f3499d3a458', 'Description': 'Set up and review rules and security -tags to protect data and systems.', 'Reference URL': None}, {'Display Name': 'Metadata Access Store', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Access Store)', 'GUID': -'1ae11a7d-cdef-48d2-bbad-1177366c6fb6', 'Description': 'A server that provides access to one or more open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Audit Log Destination', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Audit Log Destination)', 'GUID': '8a1ab260-6acd-4549-b8c3-57243eba9f8e', 'Description': 'Reads and writes records to the Open Metadata Repository Services (OMRS) audit -log.', 'Reference URL': None}, {'Display Name': 'Cohort Registry Store', 'Qualified Name': 'Egeria:ValidMetadataValue:CohortRegistryStore:deployedImplementationType-(Cohort Registry Store)', 'GUID': '6602791d-eb14-4bb6-ae91-7430018b312d', -'Description': 'A data store containing cohort membership registration details.', 'Reference URL': None}, {'Display Name': 'Kubernetes Cluster', 'Qualified Name': 'Egeria:ValidMetadataValue:HostCluster:deployedImplementationType-(Kubernetes -Cluster)', 'GUID': '96248666-e040-460b-96dd-2ce10d68664f', 'Description': 'A host cluster managing containerized applications.', 'Reference URL': None}, {'Display Name': 'Notification Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Notification Manager OMVS)', 'GUID': '69460e45-4572-430f-8629-46fd1cb02e00', 'Description': 'Manages the definitions of notifications. This includes the definition of the -trigger for the notification, the style of notification and the recipient.', 'Reference URL': None}, {'Display Name': 'OMAG Server Platform', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServerPlatform:deployedImplementationType-(OMAG -Server Platform)', 'GUID': 'e76e450d-9533-4e98-b83d-c3ea7f8b0a08', 'Description': 'An Open Metadata and Governance (OMAG) runtime for running one to many OMAG Servers.', 'Reference URL': None}, {'Display Name': 'User Authentication Manager', -'Qualified Name': 'Egeria:ValidMetadataValue:UserAuthenticationManager:deployedImplementationType-(User Authentication Manager)', 'GUID': 'a8a29046-6a5b-4a6c-a655-56533757cd97', 'Description': 'Function that validates the identity of a user -via password or other form of identification', 'Reference URL': None}, {'Display Name': 'Survey Action OMES', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Survey Action OMES)', 'GUID': -'f0b0c0c4-e931-438d-bed1-8ed94f19ae63', 'Description': "Analyses the content of an asset''s real world counterpart (resource), generates annotations in a survey report that is attached to the asset in the open metadata repositories.", -'Reference URL': None}, {'Display Name': 'Feedback Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Feedback Manager OMVS)', 'GUID': '7c7e02c1-d595-4a3a-bc9f-31d32e875f0e', 'Description': -'Work with note logs, comments, informal tags, ratings/reviews and likes.', 'Reference URL': None}, {'Display Name': 'Asset Catalog OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Asset Catalog -OMVS)', 'GUID': 'f9b5e92b-0ed8-4649-8e29-f3dd00ddbcfa', 'Description': 'Search and understand your assets.', 'Reference URL': None}, {'Display Name': 'Process', 'Qualified Name': -'Egeria:ValidMetadataValue:Process:deployedImplementationType-(Process)', 'GUID': 'eca3e5eb-ad51-4637-82f3-3071c6bf0ffb', 'Description': "A well-defined set of processing steps and decisions that drive a particular aspect of the -organization''s business.", 'Reference URL': None}, {'Display Name': 'Open Metadata Security Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata Security Services)', 'GUID': -'d5633baf-51da-434b-96d7-6a9f53e2c7bb', 'Description': 'Authorization services for Open Metadata and Governance', 'Reference URL': None}, {'Display Name': 'Apache Airflow DAG', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(Apache Airflow DAG)', 'GUID': '85b1f2da-a7ec-487a-8af9-2b108c0dad89', 'Description': 'A python component that implements a micro-workflow that runs in Apache -Airflow. DAG stands for Directed Acyclic Graph, which describes the structure of its implementation.', 'Reference URL': None}, {'Display Name': 'Glossary Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Glossary Manager OMVS)', 'GUID': '7be807f1-4e82-4bc9-a418-706b0e5af5b9', 'Description': 'Create glossary terms and organize them into folders. It supports the editing -glossary and multiple states.', 'Reference URL': None}, {'Display Name': 'Resource Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Resource Connector)', 'GUID': -'2dd4b390-94d0-49d6-80d9-13dc06a7cf7b', 'Description': 'A pluggable software component that conforms to the Open Connector Framework (OCF) and provides access to a digital resource.', 'Reference URL': None}, {'Display Name': 'File System', -'Qualified Name': 'Egeria:ValidMetadataValue:FileSystem:deployedImplementationType-(File System)', 'GUID': 'e36cdfe0-91e5-4c30-b9b6-4157ee1b5810', 'Description': 'A system that manages hierarchically organized files on persistent storage.', -'Reference URL': None}, {'Display Name': 'Privacy Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Privacy Officer OMVS)', 'GUID': '7b0b4187-ed5d-4e39-abe7-e1ba6f7e8d11', 'Description': -'Manage the governance of privacy.', 'Reference URL': None}, {'Display Name': 'Classification Explorer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Classification Explorer OMVS)', 'GUID': -'feb4edad-7ad6-4368-895d-bb129b12cf0c', 'Description': 'Retrieve elements based on type or the classifications/relationships attached to these metadata elements.', 'Reference URL': None}, {'Display Name': 'Tabular Data Set Collection', -'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(Tabular Data Set Collection)', 'GUID': '341e6c53-0fb2-4529-a4cc-8d2fadeec67a', 'Description': 'A collection of tabular data sets.', 'Reference URL': -None}, {'Display Name': 'Time Keeper OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Time Keeper OMVS)', 'GUID': '68a60c52-29ba-42b9-9e93-ffdcb1f8da60', 'Description': 'Manages context events -and other time related-behaviour.', 'Reference URL': None}, {'Display Name': 'Connected Asset Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Connected Asset Services)', 'GUID': -'e3f1bb36-e436-4a11-a9d0-22322d296204', 'Description': 'Common metadata services for the Open Connector Framework (OCF).', 'Reference URL': None}, {'Display Name': 'Hadoop Cluster', 'Qualified Name': -'Egeria:ValidMetadataValue:HostCluster:deployedImplementationType-(Hadoop Cluster)', 'GUID': '9df11118-9cac-416f-82ba-46e89ff9c693', 'Description': 'A cluster of nodes for big data workloads.', 'Reference URL': None}, {'Display Name': -'Markdown Document File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Markdown Document File)', 'GUID': 'fdfc2d1a-9844-4965-a0cc-ccfbb2f623e3', 'Description': 'Text file encoded using markdown tags and -layout.', 'Reference URL': None}, {'Display Name': 'Vector Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:VectorFile:deployedImplementationType-(Vector Data File)', 'GUID': 'acefe346-0ab9-4a64-af9b-36f5bfc85508', 'Description': 'A -file containing an image described using mathematical formulas.', 'Reference URL': None}, {'Display Name': 'Root Technology Type', 'Qualified Name': 'Egeria:ValidMetadataValue:Referenceable:deployedImplementationType-(Root Technology Type)', -'GUID': '2b406762-fa26-4ac7-8de8-98d5dd05eaa6', 'Description': 'Root technology type.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Relational Database', 'Qualified Name': -'Egeria:ValidMetadataValue:RelationalDatabase:deployedImplementationType-(PostgreSQL Relational Database)', 'GUID': 'ff56fc56-c4a1-469e-8040-472e8fe54694', 'Description': 'A database hosted on a PostgreSQL server.', 'Reference URL': None}, -{'Display Name': 'CSV Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:CSVFile:deployedImplementationType-(CSV Data File)', 'GUID': 'a312f9d3-0fbb-4d98-8dc9-b298ec972b9e', 'Description': 'A file containing comma-separated (or similar -delimited) data.', 'Reference URL': None}, {'Display Name': 'Solution Architect OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Solution Architect OMVS)', 'GUID': -'4a3c42aa-cdb7-46ae-8ff2-7cccd9179bcb', 'Description': 'Manages the definitions of information supply chains and solution components.', 'Reference URL': None}, {'Display Name': 'Governance Action OMES', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Governance Action OMES)', 'GUID': '82bc3f4f-678d-435e-bb34-dfcbac098663', 'Description': 'Executes requested governance action services to monitor, assess and maintain -metadata and its real-world counterparts.', 'Reference URL': None}, {'Display Name': 'My Profile OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(My Profile OMVS)', 'GUID': -'d4d3ee7c-b269-4bdb-aeca-2c7fca408aef', 'Description': 'Manage information about the logged on user as well as their preferences.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Server', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(PostgreSQL Server)', 'GUID': 'ff7c8d09-9813-453e-9895-7a2b0a6b0be3', 'Description': 'PostgreSQL is an advanced open source relational database.', 'Reference URL': None}, -{'Display Name': 'Spreadsheet Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:SpreadsheetFile:deployedImplementationType-(Spreadsheet Data File)', 'GUID': '742ae07c-ec29-482b-9260-4ab6bd8dac62', 'Description': 'A file containing -tabular data with formula.', 'Reference URL': None}, {'Display Name': 'XML Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:XMLFile:deployedImplementationType-(XML Data File)', 'GUID': '5f5d53fd-028d-4870-8882-1792ce8d353e', -'Description': 'A file containing an XML structure.', 'Reference URL': None}, {'Display Name': 'Asset Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Asset Maker OMVS)', 'GUID': -'2279b215-690f-469d-a1fc-dcaa8d99e36b', 'Description': 'Create new assets, software capabilities and link them to other elements.', 'Reference URL': None}, {'Display Name': 'Metadata Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:MetadataCollection:deployedImplementationType-(Metadata Collection)', 'GUID': 'aafc544d-9b9d-4d9c-86a7-3d6ecaf26fb9', 'Description': 'A data set containing metadata.', 'Reference URL': None}, {'Display Name': -'Cohort Registry Store', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Cohort Registry Store)', 'GUID': '4fd08eac-b864-4baa-a518-b31eb6d6ca4a', 'Description': 'Stores information about the -repositories registered in the open metadata repository cohort.', 'Reference URL': None}, {'Display Name': 'Open Metadata Store Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata -Store Services)', 'GUID': '7d9b5fa2-71b1-46bd-8bac-42a8089cd6ab', 'Description': 'Provides generic open metadata retrieval and management services for the Open Frameworks and Open Metadata Access Services (OMASs).', 'Reference URL': None}]} -[08:03:29] INFO my_profile_tui.py:548 -Tech Type Response: {'kind': 'json', 'data': [{'Display Name': 'Asset Metadata Catalog', 'Qualified Name': 'Egeria:ValidMetadataValue:InventoryCatalog:deployedImplementationType-(Asset Metadata Catalog)', 'GUID': -'fbe0db34-3996-4be3-8294-422bcd8732d8', 'Description': 'A catalog of metadata that describes assets such as deployed systems, data sources, and processes.', 'Reference URL': None}, {'Display Name': 'Valid Metadata OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Valid Metadata OMVS)', 'GUID': 'eb9e44c2-41a4-4ded-aeaf-9d9cc46648f0', 'Description': 'Maintain and query valid values for metadata.', 'Reference URL': None}, {'Display -Name': 'Event Broker', 'Qualified Name': 'Egeria:ValidMetadataValue:EventBroker:deployedImplementationType-(Event Broker)', 'GUID': '6b5f1764-c6ee-4cb6-b279-13502c4ad22b', 'Description': 'A system that manages the asynchronous exchange of -messages (events) from once to potentially many recipients. Typically, this exchange of events is organized into groups called topics.', 'Reference URL': None}, {'Display Name': 'Script File', 'Qualified Name': -'Egeria:ValidMetadataValue:ScriptFile:deployedImplementationType-(Script File)', 'GUID': 'e348b1a3-3bc7-4921-be4f-572fecde32e4', 'Description': 'A file containing code that is interpreted when it is run.', 'Reference URL': None}, {'Display -Name': 'Open Metadata Repository', 'Qualified Name': 'Egeria:ValidMetadataValue:MetadataRepository:deployedImplementationType-(Open Metadata Repository)', 'GUID': '106a41b6-20a5-4c12-b62f-26c328d385f2', 'Description': 'A metadata repository -supporting open metadata types and interfaces.', 'Reference URL': None}, {'Display Name': 'Video Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:VideoFile:deployedImplementationType-(Video Data File)', 'GUID': -'4eca6f52-6d0d-4219-8428-b70a841276d0', 'Description': 'A file containing a video recording.', 'Reference URL': None}, {'Display Name': 'Project Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Project Manager OMVS)', 'GUID': '0c5661d1-777e-4a0e-a8b3-70bccf80d152', 'Description': 'Set up and manage projects.', 'Reference URL': None}, {'Display Name': 'Platform -Metadata Security Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Platform Metadata Security Connector)', 'GUID': '3e0bced9-b539-4b90-a7d7-2d2034e92af1', 'Description': 'A connector that -manages authorization requests to the OMAG Server Platform.', 'Reference URL': None}, {'Display Name': 'View Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(View Server)', 'GUID': -'aa29cd35-c14a-4998-ab53-acc09a363d4b', 'Description': 'A server that provides access to end user open metadata and governance services.', 'Reference URL': None}, {'Display Name': 'External Links OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(External Links OMVS)', 'GUID': '6213b592-2758-4e28-9fc3-d3a601fa48de', 'Description': 'Manages the creation and maintenance of external reference and external -identifiers.', 'Reference URL': None}, {'Display Name': 'OMAG Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(OMAG Server)', 'GUID': '91abf546-f8f3-4d51-aa37-8839582a3ddc', 'Description': 'A -server providing Open Metadata and Governance (OMAG) services.', 'Reference URL': None}, {'Display Name': 'Software Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Software Server)', 'GUID': -'a1a96c4d-5013-4458-9c6b-6520925af6a4', 'Description': 'A callable software server.', 'Reference URL': None}, {'Display Name': 'Collection Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Collection Manager OMVS)', 'GUID': '9345334f-8baf-4cd3-a14f-08b3b09cb101', 'Description': 'Build collections of asset and other metadata.', 'Reference URL': None}, -{'Display Name': 'Webpage File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Webpage File)', 'GUID': 'ac3c99d9-4e5a-430b-aac2-091443c95349', 'Description': 'Text file encoded using HTML tags plus optional -script tags, such as JavaScript and CSS.', 'Reference URL': None}, {'Display Name': 'YAML File', 'Qualified Name': 'Egeria:ValidMetadataValue:YAMLFile:deployedImplementationType-(YAML File)', 'GUID': 'd650b328-4cd0-4f0f-b0b5-41bc8951c902', -'Description': 'A file containing properties in YAML format. This it typically used for configuration.', 'Reference URL': None}, {'Display Name': 'Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(Data File)', 'GUID': '4985b866-8a79-4298-9b87-fcb5b0c42692', 'Description': 'A file containing data. This may be structured data, text or some form or media file.', 'Reference -URL': None}, {'Display Name': 'pyegeria', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareLibrary:deployedImplementationType-(pyegeria)', 'GUID': '3a3cba55-4a6d-45a4-becf-32e93041e6b0', 'Description': "Python language library supporting -calls to Egeria''s REST APIs.", 'Reference URL': None}, {'Display Name': 'File System Directory', 'Qualified Name': 'Egeria:ValidMetadataValue:FileFolder:deployedImplementationType-(File System Directory)', 'GUID': -'d1986b2f-cc82-4a5a-9e9a-559ec9e98edc', 'Description': 'A directory (folder) that holds files that are potential data sources in a file system.', 'Reference URL': None}, {'Display Name': 'Repository Governance OMES', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Repository Governance OMES)', 'GUID': '01f887c9-e73d-49bd-a279-1fee27cd21d0', 'Description': 'Dynamically govern open metadata repositories in the connected cohorts.', -'Reference URL': None}, {'Display Name': 'Executable File', 'Qualified Name': 'Egeria:ValidMetadataValue:ExecutableFile:deployedImplementationType-(Executable File)', 'GUID': '9b12b93a-60f7-495a-8e6a-4f2a75d1bed5', 'Description': 'A file -containing compiled code that can be executed.', 'Reference URL': None}, {'Display Name': 'Governance Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceEngine:deployedImplementationType-(Governance Engine)', 'GUID': -'4612ab00-dc47-48c6-94eb-552b13a70991', 'Description': 'Manages the execution of automated governance activity requested via engine actions.', 'Reference URL': None}, {'Display Name': 'People Organizer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(People Organizer OMVS)', 'GUID': '9a6c7c16-a1f6-4dc8-963d-c81d04d6e901', 'Description': 'Describe teams, roles and organizational structure.', 'Reference URL': None}, -{'Display Name': 'Governance Action', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceAction:deployedImplementationType-(Governance Action)', 'GUID': 'f86de45a-6482-4487-ab69-0e1005de8b1a', 'Description': 'A description of an action -that supports the implementation of a governance program.', 'Reference URL': None}, {'Display Name': 'Engine Host', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Engine Host)', 'GUID': -'5cd74a31-9a1f-40c6-8ce2-f0d803b76db8', 'Description': 'A server that runs governance engines.', 'Reference URL': None}, {'Display Name': 'Integration Connector', 'Qualified Name': -'Egeria:ValidMetadataValue:IntegrationConnector:deployedImplementationType-(Integration Connector)', 'GUID': '3870ee0a-d4a3-419f-a6d0-1440c60e5c00', 'Description': 'Connector that manages metadata exchange with a third party technology.', -'Reference URL': None}, {'Display Name': 'Software Capability', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareCapability:deployedImplementationType-(Software Capability)', 'GUID': 'a51e49cd-baa1-4195-9766-e97dae5bc7a4', 'Description': -'A callable software capability supporting specific types of assets.', 'Reference URL': None}, {'Display Name': 'CSV Tabular Data Set Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSetCollection:deployedImplementationType-(CSV Tabular Data Set Collection)', 'GUID': 'caec0804-102b-4364-be07-2eab863efdef', 'Description': 'A collection of logically related CSV files each representing a -tabular data set.', 'Reference URL': None}, {'Display Name': 'Apache Superset', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Apache Superset)', 'GUID': '1ab15ae2-b937-496a-94a5-dea035ccf5c4', -'Description': 'Manages the definition and display of reports.', 'Reference URL': None}, {'Display Name': 'Document File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Document File)', 'GUID': -'54b9def5-f543-4259-afa5-10c89c38313d', 'Description': 'A data file containing formatted text.', 'Reference URL': None}, {'Display Name': 'Governance Action Engine', 'Qualified Name': -'Egeria:ValidMetadataValue:GovernanceActionEngine:deployedImplementationType-(Governance Action Engine)', 'GUID': '6195bb4b-8fce-43f8-a92a-8ec12bf4a5b9', 'Description': 'A governance engine that runs governance action services.', 'Reference -URL': None}, {'Display Name': 'Metadata Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Server)', 'GUID': 'a672aa09-fa2d-4dc8-86c1-a8ac9a993982', 'Description': 'A software server that -supports the storage of metadata.', 'Reference URL': None}, {'Display Name': 'Android Phone', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Android Phone)', 'GUID': -'357bd44d-0325-4d99-a0be-203ba77ce713', 'Description': 'A smartphone that runs the Android operating system.', 'Reference URL': None}, {'Display Name': 'Apple iPhone', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple iPhone)', 'GUID': '6330b52b-1e35-497e-8721-b94b94cb45ab', 'Description': 'A smartphone from Apple Inc. that runs the iOS operating system.', 'Reference URL': -None}, {'Display Name': 'Governance Action Process', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionProcess:deployedImplementationType-(Governance Action Process)', 'GUID': '0d970e05-6310-49dd-863f-c73401211140', 'Description': -'A modelled workflow process to perform a governance action.', 'Reference URL': None}, {'Display Name': 'Marquez Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Marquez Server)', 'GUID': -'6f559c4c-2c94-43a5-a3e6-089f62dfe39e', 'Description': 'A data catalog for data observability.', 'Reference URL': None}, {'Display Name': 'CSV Tabular Data Set', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(CSV Tabular Data Set)', 'GUID': 'ad10c3c7-e1f2-45d0-8318-2cae98129610', 'Description': 'A CSV file accessed as a tabular data set.', 'Reference URL': None}, {'Display -Name': 'Product Catalog OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Product Catalog OMVS)', 'GUID': 'c386df0a-a24e-4f64-afa8-aa09f3fdbe8d', 'Description': 'Supports researching the available -digital products and managing the subscriptions.', 'Reference URL': None}, {'Display Name': 'Jupyter Notebook File', 'Qualified Name': 'Egeria:ValidMetadataValue:JSONFile:deployedImplementationType-(Jupyter Notebook File)', 'GUID': -'15dfcf08-d73c-48ae-94e7-dfc3bc916265', 'Description': 'Text file encoded in JSON that describes a mixture of python code and descriptive text.', 'Reference URL': None}, {'Display Name': 'REST API Endpoint', 'Qualified Name': -'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(REST API Endpoint)', 'GUID': '770c5be2-a5fd-44a7-96d0-96468b9d6473', 'Description': 'A callable network address supporting the REST protocol.', 'Reference URL': None}, {'Display -Name': 'Open Connector Framework (OCF) Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Open Connector Framework (OCF) Connector)', 'GUID': 'df24f7a6-57e6-4991-ae7a-1dfbdf48fd55', -'Description': 'A pluggable software component that conforms to the Open Connector Framework (OCF).', 'Reference URL': None}, {'Display Name': 'Asset', 'Qualified Name': 'Egeria:ValidMetadataValue:Asset:deployedImplementationType-(Asset)', -'GUID': '0e544ca8-f5f7-49dc-ab78-6d4caedfb397', 'Description': 'A description of a digital resource.', 'Reference URL': None}, {'Display Name': 'UNIX File System', 'Qualified Name': -'Egeria:ValidMetadataValue:FileSystem:deployedImplementationType-(UNIX File System)', 'GUID': '93e1b6df-f597-4d20-b846-7ece880478dc', 'Description': 'A Unix capability that manages hierarchically organized files on persistent storage.', -'Reference URL': None}, {'Display Name': 'Community Matters OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Community Matters OMVS)', 'GUID': '977629ef-a3c3-4deb-bd1f-4b96345c42db', -'Description': 'Manages the definitions of communities, their leaders and membership.', 'Reference URL': None}, {'Display Name': 'Template Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Template Manager OMVS)', 'GUID': 'e2d3906d-c6f4-45cf-ab5e-36e9d1fb4722', 'Description': 'Retrieve and refine the templates for use during cataloguing.', 'Reference URL': -None}, {'Display Name': 'Connection Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Connection Maker OMVS)', 'GUID': '47fe7a5d-6ba2-446f-823f-fe48ad3d8613', 'Description': 'Manages the -creation and maintenance of connections, connector types and endpoints.', 'Reference URL': None}, {'Display Name': 'Business Application', 'Qualified Name': 'Egeria:ValidMetadataValue:Application:deployedImplementationType-(Business -Application)', 'GUID': '6ac87de8-d01f-46bf-8881-0ea3204e716b', 'Description': 'Software supporting a business function.', 'Reference URL': None}, {'Display Name': 'Data Asset', 'Qualified Name': -'Egeria:ValidMetadataValue:DataAsset:deployedImplementationType-(Data Asset)', 'GUID': 'df79a55a-8cd8-4cf5-a35c-c8bea53db0c4', 'Description': 'A collection of data, either at rest or in motion.', 'Reference URL': None}, {'Display Name': -'Apple iPad', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple iPad)', 'GUID': '92d0e4c8-d7f5-4e8b-ad30-f6f20aaead98', 'Description': 'A tablet from Apple Inc. that runs the iOS operating -system.', 'Reference URL': None}, {'Display Name': 'JSON Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:JSONFile:deployedImplementationType-(JSON Data File)', 'GUID': 'f8cd55d2-394e-4589-b16b-b100eeb6f158', 'Description': 'A -description of a file that follows the JavaScript Object Notation specification.', 'Reference URL': None}, {'Display Name': 'Lineage Linker OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Lineage Linker OMVS)', 'GUID': 'ee76a68f-884e-4198-81e2-3c3da03cc8d1', 'Description': 'Manages the creation and maintenance of lineage relationships.', 'Reference URL': -None}, {'Display Name': 'Open Metadata Archive Store Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Open Metadata Archive Store Connector)', 'GUID': -'b0dcf516-4440-4cd3-980b-7cc5eccb9d43', 'Description': 'Reads and writes open metadata types and instances to an open metadata archive.', 'Reference URL': None}, {'Display Name': 'Audio Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:AudioFile:deployedImplementationType-(Audio Data File)', 'GUID': 'e08ff316-5e85-4088-9cd5-d737ed81ccc2', 'Description': 'A file containing an audio recording.', 'Reference URL': None}, {'Display Name': 'Metadata -Access Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Access Server)', 'GUID': '940dda2a-e86d-4ba2-b889-500431d47f70', 'Description': 'A server that provides access to one or more -open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Administration Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Administration Services)', 'GUID': -'d8e2d72a-cea9-404f-867d-775a2fec88a5', 'Description': 'Platform service providing configuration of services for an Open Metadata and Governance server (OMAG Server).', 'Reference URL': None}, {'Display Name': 'Apache Kafka Topic', 'Qualified -Name': 'Egeria:ValidMetadataValue:Topic:deployedImplementationType-(Apache Kafka Topic)', 'GUID': '20f81045-aa24-4374-92cb-d045ac3e24f6', 'Description': 'An event topic supporting high-speed, reliable event exchange.', 'Reference URL': None}, -{'Display Name': 'PostgreSQL database manager (RDBMS)', 'Qualified Name': 'Egeria:ValidMetadataValue:DatabaseManager:deployedImplementationType-(PostgreSQL database manager (RDBMS))', 'GUID': 'cdfedb9c-8020-422d-b89e-40c57df22bd0', -'Description': 'The PostgreSQL capability that manages collections of data called relational databases which in turn are organized into a tabular format and accessed via the Structured Query Language (SQL).', 'Reference URL': None}, {'Display -Name': 'Watchdog Action OMES', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Watchdog Action OMES)', 'GUID': '0be60ce5-f7d8-4b41-8f93-9c30851bcd28', 'Description': 'Monitors for particular -situations/events and notifies registered subscribers.', 'Reference URL': None}, {'Display Name': 'Smart Phone', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Smart Phone)', 'GUID': -'6f617a49-2f25-44d0-b507-d2e4ac3db603', 'Description': 'A two-handed portable device with network, touchscreen, and battery power.', 'Reference URL': None}, {'Display Name': 'Open Governance Framework Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Governance Framework Services)', 'GUID': '23e704d7-287c-4672-9666-04519563edbf', 'Description': 'Support open governance services.', 'Reference URL': None}, {'Display -Name': 'Survey Action Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:SurveyActionEngine:deployedImplementationType-(Survey Action Engine)', 'GUID': 'fe4bcc14-985b-421d-b98e-3fddbc884811', 'Description': 'A governance engine that runs -survey action services.', 'Reference URL': None}, {'Display Name': 'Data Feed', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFeed:deployedImplementationType-(Data Feed)', 'GUID': '8ff94698-32b6-4765-bcc6-b1357bc1d55b', 'Description': 'An -event-based data source', 'Reference URL': None}, {'Display Name': 'Integration Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Integration Server)', 'GUID': -'f7ddc726-7f68-466b-a1e5-cf365d31714b', 'Description': 'A software server that supports the exchange of data.', 'Reference URL': None}, {'Display Name': 'Open Metadata Archive', 'Qualified Name': -'Egeria:ValidMetadataValue:ArchiveFile:deployedImplementationType-(Open Metadata Archive)', 'GUID': '93d47fe4-8b62-4c1e-af89-26743abc6bc8', 'Description': 'An archive file containing pre-defined metadata types and instances.', 'Reference -URL': None}, {'Display Name': 'Repository Governance Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:RepositoryGovernanceEngine:deployedImplementationType-(Repository Governance Engine)', 'GUID': '9a754469-05a1-4d4c-b3b5-6b3ee4207ac7', -'Description': 'A governance engine that runs repository governance services.', 'Reference URL': None}, {'Display Name': 'Subject Area OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Subject -Area OMVS)', 'GUID': 'c872a3bb-9945-445f-afc8-9d73eaf3c398', 'Description': 'Manages the definition of subject areas and reporting on their contents.', 'Reference URL': None}, {'Display Name': 'Platform Operational Services', 'Qualified -Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Platform Operational Services)', 'GUID': '5883c7fb-b339-4dce-98dc-887dfaf8371a', 'Description': 'A platform service that provides information about the registered -services and connectors available in an OMAG Server Platform along with services to control and query information about the OMAG Servers running on the platform.', 'Reference URL': None}, {'Display Name': 'Apple MacBook Pro', 'Qualified -Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple MacBook Pro)', 'GUID': 'b7327658-41c3-4a53-877b-dd1b7a1dbac4', 'Description': 'A professional laptop supplied by Apple Inc that runs the macOS operating -system.', 'Reference URL': None}, {'Display Name': 'Server Metadata Security Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Server Metadata Security Connector)', 'GUID': -'5aa6a746-9dd4-4cbb-b1aa-e05707a156ce', 'Description': 'A connector that manages authorization requests to the OMAG Server.', 'Reference URL': None}, {'Display Name': 'Data Feed', 'Qualified Name': -'Egeria:ValidMetadataValue:Topic:deployedImplementationType-(Data Feed)', 'GUID': 'e3e25371-65da-47a8-bb73-feb7a00b2afe', 'Description': 'An organized event-based data source that supports subscription to a named data feed.', 'Reference URL': -None}, {'Display Name': 'PostgreSQL Relational Database Schema', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedDatabaseSchema:deployedImplementationType-(PostgreSQL Relational Database Schema)', 'GUID': -'e46811ac-8b86-42c3-aaea-2fe55f474044', 'Description': 'A database schema hosted on a PostgreSQL relational database server capable of being called through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Integration Daemon -Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Integration Daemon Services)', 'GUID': 'fbd8b372-d355-49be-af75-c38ee1f0726d', 'Description': 'Hosts integration connectors that are -exchanging metadata with third party technologies.', 'Reference URL': None}, {'Display Name': 'Governance Service', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceService:deployedImplementationType-(Governance Service)', 'GUID': -'f5e38a2f-911d-4718-9039-0eaa25f5b9ee', 'Description': 'Provides the description of a component that implements an automated governance activity.', 'Reference URL': None}, {'Display Name': 'Watchdog Action Service', 'Qualified Name': -'Egeria:ValidMetadataValue:WatchdogActionService:deployedImplementationType-(Watchdog Action Service)', 'GUID': 'd4af2e0f-78db-4b93-bf5d-53632b33c67a', 'Description': 'A connector that coordinates notifications based on situations/events.', -'Reference URL': None}, {'Display Name': 'Tabular Data Set', 'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(Tabular Data Set)', 'GUID': 'f3d81f48-1824-4608-962e-90f3f239e861', 'Description': 'A logical -collection of data, either at rest or in motion, organized into a tabular (columnar) format.', 'Reference URL': None}, {'Display Name': 'Engine Host Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Engine Host Services)', 'GUID': '5195a98e-9e4a-41f1-b00d-1a26cee3e20d', 'Description': 'Host one or more engine services that are actively managing governance of open -metadata and the digital landscape.', 'Reference URL': None}, {'Display Name': 'Archive File', 'Qualified Name': 'Egeria:ValidMetadataValue:ArchiveFile:deployedImplementationType-(Archive File)', 'GUID': -'6148ab2c-26e3-4cb5-870d-d95d070c910f', 'Description': 'A file containing compressed files. These files may be organized into a directory (folder) structure.', 'Reference URL': None}, {'Display Name': 'Data Officer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Officer OMVS)', 'GUID': 'f81202aa-0572-47ae-a6db-5e71459addf2', 'Description': 'Manage the governance of data.', 'Reference URL': None}, {'Display Name': 'Properties -File', 'Qualified Name': 'Egeria:ValidMetadataValue:PropertiesFile:deployedImplementationType-(Properties File)', 'GUID': 'f83e006c-a2bf-40e5-8c04-45e93dee4967', 'Description': 'A file containing a list of properties, typically used for -configuration of some software.', 'Reference URL': None}, {'Display Name': 'Data Discovery OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Discovery OMVS)', 'GUID': -'86efdf03-9cce-4572-bf8e-4b6cca299161', 'Description': 'Define and search for new data resources.', 'Reference URL': None}, {'Display Name': 'OMRS Repository Connector', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(OMRS Repository Connector)', 'GUID': 'a6e46bec-a80a-417e-931f-f6d044ff0f33', 'Description': 'Maps open metadata repository calls defined by the Open Metadata -Repository Services (OMRS) to a metadata repository API and event notifications.', 'Reference URL': None}, {'Display Name': 'Integration Daemon', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Integration Daemon)', 'GUID': 'b2d8d57f-df68-4280-9a41-3df830c3444f', 'Description': 'A server that runs integration connectors that synchronize metadata between different -types of technologies.', 'Reference URL': None}, {'Display Name': 'Digital Business OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Digital Business OMVS)', 'GUID': -'4a74aa92-3fc7-4160-904a-cfd1bb384e43', 'Description': 'Manages the definition and maintenance of the business context description used to identify where business value is being derived from.', 'Reference URL': None}, {'Display Name': -'Dynamic Integration Group', 'Qualified Name': 'Egeria:ValidMetadataValue:IntegrationGroup:deployedImplementationType-(Dynamic Integration Group)', 'GUID': '7a49ed4e-6810-4520-836d-aebc8ee341d7', 'Description': 'Provides the list of -integration connectors that should run in an Integration Daemon. The Integration Daemon is configured with the qualified names of the integration group(s) that provide its connector list.', 'Reference URL': None}, {'Display Name': -'Governance Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Governance Officer OMVS)', 'GUID': '3823da46-43d3-4859-add0-59204bf904dd', 'Description': 'Maintain governance definitions -used to define any governance domain.', 'Reference URL': None}, {'Display Name': 'File', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(File)', 'GUID': 'b5bb473d-947e-4ffb-8605-b8364c5ce85c', 'Description': -'A description of a file stored in a file system.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Tabular Data Set Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSetCollection:deployedImplementationType-(PostgreSQL Tabular Data Set Collection)', 'GUID': 'c113b127-ae8d-417f-90f2-92108c141270', 'Description': 'A database schema hosted on a PostgreSQL relational -database server that has the tabular data set collection interface.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(PostgreSQL Server)', -'GUID': 'd3ea92d0-c53b-4cc8-84d8-764648323d26', 'Description': 'A database server running the PostgreSQL software.', 'Reference URL': None}, {'Display Name': 'Smartphone', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Smartphone)', 'GUID': '94a91316-9f23-4ab5-a907-3c8c6cfea611', 'Description': 'A handheld portable device with network, touchscreen, and battery power.', 'Reference URL': -None}, {'Display Name': 'Metadata Expert OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Metadata Expert OMVS)', 'GUID': '3b1481ad-8b18-48b9-97c1-0a627d97dd65', 'Description': 'Provides advanced -metadata management features for experts in open metadata.', 'Reference URL': None}, {'Display Name': 'Keystore File', 'Qualified Name': 'Egeria:ValidMetadataValue:KeyStoreFile:deployedImplementationType-(Keystore File)', 'GUID': -'409f12a7-b4a3-4e0f-a9bd-8094813346d6', 'Description': 'An encrypted data store containing authentication and related security information.', 'Reference URL': None}, {'Display Name': 'Data Folder', 'Qualified Name': -'Egeria:ValidMetadataValue:DataFolder:deployedImplementationType-(Data Folder)', 'GUID': '0b18ef33-84ee-476d-92fc-82c109824261', 'Description': 'A directory (folder) that holds files representing a single data source.', 'Reference URL': -None}, {'Display Name': 'Endpoint', 'Qualified Name': 'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(Endpoint)', 'GUID': '69a0e224-70d7-43e4-a4af-ecd0d4fc54eb', 'Description': 'A callable network address.', 'Reference URL': -None}, {'Display Name': 'DevOps Pipeline OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(DevOps Pipeline OMVS)', 'GUID': 'c8c83907-20be-4f21-9ee6-64ac21aafd83', 'Description': 'Maintain the -metadata about the assets managed by a devops pipeline.', 'Reference URL': None}, {'Display Name': 'Parquet Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:ParquetFile:deployedImplementationType-(Parquet Data File)', 'GUID': -'734d12c6-0459-42ef-bd41-2ce6e45559ba', 'Description': 'A data file which is formatted using the Apache Parquet format.', 'Reference URL': None}, {'Display Name': 'Action Author OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Action Author OMVS)', 'GUID': '559775a7-ce1e-4f6f-973c-05b86ed2d57f', 'Description': 'Maintain definitions of governance actions such as governance action processes and -governance action types.', 'Reference URL': None}, {'Display Name': 'Governance Action Type', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionType:deployedImplementationType-(Governance Action Type)', 'GUID': -'eda3449b-d928-49aa-a67c-bc934e2a2102', 'Description': 'A modelled governance action with a single step.', 'Reference URL': None}, {'Display Name': 'Avro Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:AvroFile:deployedImplementationType-(Avro Data File)', 'GUID': 'f0fee71a-ed03-44f7-aee3-633732473250', 'Description': 'A description of a file that follows the Apache Avro specification.', 'Reference URL': None}, -{'Display Name': 'Repository Governance Service Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:RepositoryGovernanceService:deployedImplementationType-(Repository Governance Service Connector)', 'GUID': -'958fe597-6db9-4c0f-bf3e-54331934b792', 'Description': 'A connector that dynamically governs the activity of the open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Watchdog Action Engine', 'Qualified Name': -'Egeria:ValidMetadataValue:WatchdogActionEngine:deployedImplementationType-(Watchdog Action Engine)', 'GUID': 'ac264c61-8b57-4afd-a505-d0a2c1fd54cd', 'Description': 'A governance engine that runs watchdog action services.', 'Reference URL': -None}, {'Display Name': 'Log File', 'Qualified Name': 'Egeria:ValidMetadataValue:LogFile:deployedImplementationType-(Log File)', 'GUID': 'b924be69-f87f-4807-8261-5a899a3508dd', 'Description': 'Identifies a data file as one containing log -records.', 'Reference URL': None}, {'Display Name': 'Relational database manager (RDBMS)', 'Qualified Name': 'Egeria:ValidMetadataValue:DatabaseManager:deployedImplementationType-(Relational database manager (RDBMS))', 'GUID': -'dd43d7d7-7c32-42e8-8112-67fa5d5ed15f', 'Description': 'A capability that manages collections of data called relational databases which in turn are organized into a tabular format and accessed via the Structured Query Language (SQL).', -'Reference URL': None}, {'Display Name': 'Program File', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(Program File)', 'GUID': '822af572-069f-4f89-8a52-9a7d5a70092f', 'Description': 'A file containing -program logic.', 'Reference URL': None}, {'Display Name': 'Automated Curation OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Automated Curation OMVS)', 'GUID': -'babf18f0-3f11-439b-8fe0-494aab24f58d', 'Description': "Manage Egeria''s automation services.", 'Reference URL': None}, {'Display Name': '3D Image Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:ThreeDImageFile:deployedImplementationType-(3D Image Data File)', 'GUID': '8c7cc3be-365a-48db-bfba-a64da2a441e4', 'Description': 'A file containing a three-dimensional image.', 'Reference URL': None}, {'Display -Name': 'Apache Airflow Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Apache Airflow Server)', 'GUID': 'f55a0afd-656c-4550-994b-52c8acfc86f7', 'Description': 'Runs data movement and -transformation pipelines.', 'Reference URL': None}, {'Display Name': 'Multi Language OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Multi Language OMVS)', 'GUID': -'8905a42b-bffc-4e4d-b3af-55c90411914f', 'Description': 'Manages the creation and maintenance of translations for open metadata elements.', 'Reference URL': None}, {'Display Name': 'JDBC Endpoint', 'Qualified Name': -'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(JDBC Endpoint)', 'GUID': 'f23c929f-c69a-445e-a0a5-6ae20206e52d', 'Description': 'A callable network address supporting the JDBC protocol.', 'Reference URL': None}, {'Display -Name': 'Docker Container', 'Qualified Name': 'Egeria:ValidMetadataValue:VirtualContainer:deployedImplementationType-(Docker Container)', 'GUID': '8020b056-79be-4378-9e5a-44bc6f3453b5', 'Description': 'A virtual container using the docker -platform.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Tabular Data Set', 'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(PostgreSQL Tabular Data Set)', 'GUID': -'aa50ce4c-917d-4880-a987-d6c771b822b2', 'Description': 'A database table hosted on a PostgreSQL relational database server that has the tabular data set interface.', 'Reference URL': None}, {'Display Name': 'Database Server', 'Qualified -Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Database Server)', 'GUID': 'ff82019c-8faa-4ac3-8f99-c3a8477743e9', 'Description': 'A software server that supports databases.', 'Reference URL': None}, {'Display -Name': 'JDBC Relational Database', 'Qualified Name': 'Egeria:ValidMetadataValue:RelationalDatabase:deployedImplementationType-(JDBC Relational Database)', 'GUID': '878b6fb6-a203-42b5-a04c-4f40de319de9', 'Description': 'A database hosted on a -relational database server callable through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Survey Action Service', 'Qualified Name': 'Egeria:ValidMetadataValue:SurveyActionService:deployedImplementationType-(Survey Action -Service)', 'GUID': 'f3bb3f15-6d09-451d-804c-23ae00529d06', 'Description': 'A connector that coordinates asset surveys.', 'Reference URL': None}, {'Display Name': 'Server Operations', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Server Operations)', 'GUID': '2cf2d01c-30a4-4d37-8347-987f5ecad623', 'Description': 'Supports the start up and shutdown of OMAG Servers.', 'Reference URL': None}, -{'Display Name': 'Open Metadata Repository Services (OMRS)', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata Repository Services (OMRS))', 'GUID': '4f0083f2-32b2-4c22-b78e-e4ff126864a3', -'Description': 'Manages the synchronization, retrieval and maintenance of metadata stored in open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Data Set', 'Qualified Name': -'Egeria:ValidMetadataValue:DataAsset:deployedImplementationType-(Data Set)', 'GUID': '44ad5f7e-804d-4d24-a837-0e7021b12996', 'Description': 'A logical collection of data, either at rest or in motion.', 'Reference URL': None}, {'Display Name': -'Metadata Repository', 'Qualified Name': 'Egeria:ValidMetadataValue:MetadataRepository:deployedImplementationType-(Metadata Repository)', 'GUID': '3e88a901-804c-4a4a-a89c-983f76e7966b', 'Description': 'A data store containing metadata.', -'Reference URL': None}, {'Display Name': 'Runtime Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Runtime Manager OMVS)', 'GUID': '63396929-d549-4c87-b2bb-2f3f82e31796', 'Description': -'Retrieve configuration and status from platforms and servers.', 'Reference URL': None}, {'Display Name': 'API Manager', 'Qualified Name': 'Egeria:ValidMetadataValue:APIManager:deployedImplementationType-(API Manager)', 'GUID': -'18dbff2f-0909-4e85-bbe2-5b5332ddf1f8', 'Description': 'A software capability that provides callable APIs.', 'Reference URL': None}, {'Display Name': 'Data Designer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Designer OMVS)', 'GUID': 'ee1e73dd-7161-4df1-9732-0c8f91e46684', 'Description': 'Create data specifications to describe data requirements.', 'Reference URL': None}, -{'Display Name': 'Data File Collection', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFileCollection:deployedImplementationType-(Data File Collection)', 'GUID': '6cfedd7c-4aaa-49f2-8e67-b70949928ce6', 'Description': 'A collection of -logically related files representing a single data source.', 'Reference URL': None}, {'Display Name': 'Reference Data OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Reference Data OMVS)', -'GUID': '4b83dd72-f74f-4291-b43a-995f6c31aa80', 'Description': 'Work with code tables and associated reference data.', 'Reference URL': None}, {'Display Name': 'Small Form Factor (SFF) Computer', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Small Form Factor (SFF) Computer)', 'GUID': '4636a5c7-b1e9-4a65-8143-97e362efcc07', 'Description': 'A small hardware server enclosed in a protective case that can sit -under a desk.', 'Reference URL': None}, {'Display Name': 'Software Component', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(Software Component)', 'GUID': -'29b4afae-7000-4837-a225-d37bc036d503', 'Description': 'A deployable software component.', 'Reference URL': None}, {'Display Name': 'Conformance Suite Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Conformance Suite Services)', 'GUID': '9984876e-2593-453e-b319-1fe7b096fea9', 'Description': 'Run automated open metadata conformance suite services.', 'Reference URL': -None}, {'Display Name': 'Governance Action Service', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionService:deployedImplementationType-(Governance Action Service)', 'GUID': 'd77773c9-cc7c-4571-8472-f2764aa1471a', 'Description': -'A connector that coordinates governance of digital resources and metadata.', 'Reference URL': None}, {'Display Name': 'REST API Manager', 'Qualified Name': 'Egeria:ValidMetadataValue:APIManager:deployedImplementationType-(REST API Manager)', -'GUID': '0a9947b4-6686-4710-a64f-645f9ce430bd', 'Description': 'A software capability that provides callable APIs supporting the REST protocol.', 'Reference URL': None}, {'Display Name': 'Product Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Product Manager OMVS)', 'GUID': '1eb4a943-c0d4-46d6-acdf-c35330cc3c5e', 'Description': 'Supports the development and maintenance of digital products.', 'Reference URL': -None}, {'Display Name': 'Actor Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Actor Manager OMVS)', 'GUID': 'beb3afb3-027c-479c-bec0-aa239fbcb658', 'Description': 'Manages the -definitions of user identities, actor profiles, contact details and actor roles.', 'Reference URL': None}, {'Display Name': 'Source Code File', 'Qualified Name': 'Egeria:ValidMetadataValue:SourceCodeFile:deployedImplementationType-(Source -Code File)', 'GUID': 'd615f94a-fc78-4f23-8b35-f4af454bc01e', 'Description': 'A text file containing a program written in a language that needs to be complied into an executable form before it can run.', 'Reference URL': None}, {'Display -Name': 'Laptop Computer', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Laptop Computer)', 'GUID': '38ecbb12-c172-461c-aeac-8d7da0502cf5', 'Description': 'A portable computer with a screen, -keyboard, and battery power.', 'Reference URL': None}, {'Display Name': 'Data Engineer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Engineer OMVS)', 'GUID': -'98d78723-7b6c-4ca9-9403-0c4d6a944552', 'Description': 'Manages the creation and maintenance of data pipelines, open metadata templates and reference data.', 'Reference URL': None}, {'Display Name': 'Location Arena OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Location Arena OMVS)', 'GUID': '8b74f416-28a6-4a8c-ab08-6e32eb3ec8e1', 'Description': 'Manages the creation and maintenance of locations.', 'Reference URL': None}, -{'Display Name': 'Bare Metal Computer', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Bare Metal Computer)', 'GUID': 'f593a0c5-4a71-4fd9-9963-dbe6ba464865', 'Description': 'A computer (hardware) -with an operating system for running software.', 'Reference URL': None}, {'Display Name': 'Schema Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Schema Maker OMVS)', 'GUID': -'5fef72f9-7580-4dd1-989a-6946555c0814', 'Description': 'Manages the creation and maintenance of schema elements.', 'Reference URL': None}, {'Display Name': 'JDBC Relational Database Schema', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedDatabaseSchema:deployedImplementationType-(JDBC Relational Database Schema)', 'GUID': '10eb9965-80d1-447a-a207-68e39c2ee755', 'Description': 'A database schema hosted on a relational database server callable -through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Build Instruction File', 'Qualified Name': 'Egeria:ValidMetadataValue:BuildInstructionFile:deployedImplementationType-(Build Instruction File)', 'GUID': -'d5fa0c48-e24c-44b4-910c-a4f29ec0f889', 'Description': 'A file containing instructions to run a build of a software artifact or system.', 'Reference URL': None}, {'Display Name': 'Raster Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:RasterFile:deployedImplementationType-(Raster Data File)', 'GUID': 'c9800490-8706-4d91-bd8f-28813239add3', 'Description': 'A file containing an image as a matrix of pixels.', 'Reference URL': None}, {'Display Name': -'Security Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Security Officer OMVS)', 'GUID': '9bd9e4c0-c994-447b-b1c5-5f3499d3a458', 'Description': 'Set up and review rules and security -tags to protect data and systems.', 'Reference URL': None}, {'Display Name': 'Metadata Access Store', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Access Store)', 'GUID': -'1ae11a7d-cdef-48d2-bbad-1177366c6fb6', 'Description': 'A server that provides access to one or more open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Audit Log Destination', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Audit Log Destination)', 'GUID': '8a1ab260-6acd-4549-b8c3-57243eba9f8e', 'Description': 'Reads and writes records to the Open Metadata Repository Services (OMRS) audit -log.', 'Reference URL': None}, {'Display Name': 'Cohort Registry Store', 'Qualified Name': 'Egeria:ValidMetadataValue:CohortRegistryStore:deployedImplementationType-(Cohort Registry Store)', 'GUID': '6602791d-eb14-4bb6-ae91-7430018b312d', -'Description': 'A data store containing cohort membership registration details.', 'Reference URL': None}, {'Display Name': 'Kubernetes Cluster', 'Qualified Name': 'Egeria:ValidMetadataValue:HostCluster:deployedImplementationType-(Kubernetes -Cluster)', 'GUID': '96248666-e040-460b-96dd-2ce10d68664f', 'Description': 'A host cluster managing containerized applications.', 'Reference URL': None}, {'Display Name': 'Notification Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Notification Manager OMVS)', 'GUID': '69460e45-4572-430f-8629-46fd1cb02e00', 'Description': 'Manages the definitions of notifications. This includes the definition of the -trigger for the notification, the style of notification and the recipient.', 'Reference URL': None}, {'Display Name': 'OMAG Server Platform', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServerPlatform:deployedImplementationType-(OMAG -Server Platform)', 'GUID': 'e76e450d-9533-4e98-b83d-c3ea7f8b0a08', 'Description': 'An Open Metadata and Governance (OMAG) runtime for running one to many OMAG Servers.', 'Reference URL': None}, {'Display Name': 'User Authentication Manager', -'Qualified Name': 'Egeria:ValidMetadataValue:UserAuthenticationManager:deployedImplementationType-(User Authentication Manager)', 'GUID': 'a8a29046-6a5b-4a6c-a655-56533757cd97', 'Description': 'Function that validates the identity of a user -via password or other form of identification', 'Reference URL': None}, {'Display Name': 'Survey Action OMES', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Survey Action OMES)', 'GUID': -'f0b0c0c4-e931-438d-bed1-8ed94f19ae63', 'Description': "Analyses the content of an asset''s real world counterpart (resource), generates annotations in a survey report that is attached to the asset in the open metadata repositories.", -'Reference URL': None}, {'Display Name': 'Feedback Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Feedback Manager OMVS)', 'GUID': '7c7e02c1-d595-4a3a-bc9f-31d32e875f0e', 'Description': -'Work with note logs, comments, informal tags, ratings/reviews and likes.', 'Reference URL': None}, {'Display Name': 'Asset Catalog OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Asset Catalog -OMVS)', 'GUID': 'f9b5e92b-0ed8-4649-8e29-f3dd00ddbcfa', 'Description': 'Search and understand your assets.', 'Reference URL': None}, {'Display Name': 'Process', 'Qualified Name': -'Egeria:ValidMetadataValue:Process:deployedImplementationType-(Process)', 'GUID': 'eca3e5eb-ad51-4637-82f3-3071c6bf0ffb', 'Description': "A well-defined set of processing steps and decisions that drive a particular aspect of the -organization''s business.", 'Reference URL': None}, {'Display Name': 'Open Metadata Security Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata Security Services)', 'GUID': -'d5633baf-51da-434b-96d7-6a9f53e2c7bb', 'Description': 'Authorization services for Open Metadata and Governance', 'Reference URL': None}, {'Display Name': 'Apache Airflow DAG', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(Apache Airflow DAG)', 'GUID': '85b1f2da-a7ec-487a-8af9-2b108c0dad89', 'Description': 'A python component that implements a micro-workflow that runs in Apache -Airflow. DAG stands for Directed Acyclic Graph, which describes the structure of its implementation.', 'Reference URL': None}, {'Display Name': 'Glossary Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Glossary Manager OMVS)', 'GUID': '7be807f1-4e82-4bc9-a418-706b0e5af5b9', 'Description': 'Create glossary terms and organize them into folders. It supports the editing -glossary and multiple states.', 'Reference URL': None}, {'Display Name': 'Resource Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Resource Connector)', 'GUID': -'2dd4b390-94d0-49d6-80d9-13dc06a7cf7b', 'Description': 'A pluggable software component that conforms to the Open Connector Framework (OCF) and provides access to a digital resource.', 'Reference URL': None}, {'Display Name': 'File System', -'Qualified Name': 'Egeria:ValidMetadataValue:FileSystem:deployedImplementationType-(File System)', 'GUID': 'e36cdfe0-91e5-4c30-b9b6-4157ee1b5810', 'Description': 'A system that manages hierarchically organized files on persistent storage.', -'Reference URL': None}, {'Display Name': 'Privacy Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Privacy Officer OMVS)', 'GUID': '7b0b4187-ed5d-4e39-abe7-e1ba6f7e8d11', 'Description': -'Manage the governance of privacy.', 'Reference URL': None}, {'Display Name': 'Classification Explorer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Classification Explorer OMVS)', 'GUID': -'feb4edad-7ad6-4368-895d-bb129b12cf0c', 'Description': 'Retrieve elements based on type or the classifications/relationships attached to these metadata elements.', 'Reference URL': None}, {'Display Name': 'Tabular Data Set Collection', -'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(Tabular Data Set Collection)', 'GUID': '341e6c53-0fb2-4529-a4cc-8d2fadeec67a', 'Description': 'A collection of tabular data sets.', 'Reference URL': -None}, {'Display Name': 'Time Keeper OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Time Keeper OMVS)', 'GUID': '68a60c52-29ba-42b9-9e93-ffdcb1f8da60', 'Description': 'Manages context events -and other time related-behaviour.', 'Reference URL': None}, {'Display Name': 'Connected Asset Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Connected Asset Services)', 'GUID': -'e3f1bb36-e436-4a11-a9d0-22322d296204', 'Description': 'Common metadata services for the Open Connector Framework (OCF).', 'Reference URL': None}, {'Display Name': 'Hadoop Cluster', 'Qualified Name': -'Egeria:ValidMetadataValue:HostCluster:deployedImplementationType-(Hadoop Cluster)', 'GUID': '9df11118-9cac-416f-82ba-46e89ff9c693', 'Description': 'A cluster of nodes for big data workloads.', 'Reference URL': None}, {'Display Name': -'Markdown Document File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Markdown Document File)', 'GUID': 'fdfc2d1a-9844-4965-a0cc-ccfbb2f623e3', 'Description': 'Text file encoded using markdown tags and -layout.', 'Reference URL': None}, {'Display Name': 'Vector Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:VectorFile:deployedImplementationType-(Vector Data File)', 'GUID': 'acefe346-0ab9-4a64-af9b-36f5bfc85508', 'Description': 'A -file containing an image described using mathematical formulas.', 'Reference URL': None}, {'Display Name': 'Root Technology Type', 'Qualified Name': 'Egeria:ValidMetadataValue:Referenceable:deployedImplementationType-(Root Technology Type)', -'GUID': '2b406762-fa26-4ac7-8de8-98d5dd05eaa6', 'Description': 'Root technology type.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Relational Database', 'Qualified Name': -'Egeria:ValidMetadataValue:RelationalDatabase:deployedImplementationType-(PostgreSQL Relational Database)', 'GUID': 'ff56fc56-c4a1-469e-8040-472e8fe54694', 'Description': 'A database hosted on a PostgreSQL server.', 'Reference URL': None}, -{'Display Name': 'CSV Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:CSVFile:deployedImplementationType-(CSV Data File)', 'GUID': 'a312f9d3-0fbb-4d98-8dc9-b298ec972b9e', 'Description': 'A file containing comma-separated (or similar -delimited) data.', 'Reference URL': None}, {'Display Name': 'Solution Architect OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Solution Architect OMVS)', 'GUID': -'4a3c42aa-cdb7-46ae-8ff2-7cccd9179bcb', 'Description': 'Manages the definitions of information supply chains and solution components.', 'Reference URL': None}, {'Display Name': 'Governance Action OMES', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Governance Action OMES)', 'GUID': '82bc3f4f-678d-435e-bb34-dfcbac098663', 'Description': 'Executes requested governance action services to monitor, assess and maintain -metadata and its real-world counterparts.', 'Reference URL': None}, {'Display Name': 'My Profile OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(My Profile OMVS)', 'GUID': -'d4d3ee7c-b269-4bdb-aeca-2c7fca408aef', 'Description': 'Manage information about the logged on user as well as their preferences.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Server', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(PostgreSQL Server)', 'GUID': 'ff7c8d09-9813-453e-9895-7a2b0a6b0be3', 'Description': 'PostgreSQL is an advanced open source relational database.', 'Reference URL': None}, -{'Display Name': 'Spreadsheet Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:SpreadsheetFile:deployedImplementationType-(Spreadsheet Data File)', 'GUID': '742ae07c-ec29-482b-9260-4ab6bd8dac62', 'Description': 'A file containing -tabular data with formula.', 'Reference URL': None}, {'Display Name': 'XML Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:XMLFile:deployedImplementationType-(XML Data File)', 'GUID': '5f5d53fd-028d-4870-8882-1792ce8d353e', -'Description': 'A file containing an XML structure.', 'Reference URL': None}, {'Display Name': 'Asset Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Asset Maker OMVS)', 'GUID': -'2279b215-690f-469d-a1fc-dcaa8d99e36b', 'Description': 'Create new assets, software capabilities and link them to other elements.', 'Reference URL': None}, {'Display Name': 'Metadata Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:MetadataCollection:deployedImplementationType-(Metadata Collection)', 'GUID': 'aafc544d-9b9d-4d9c-86a7-3d6ecaf26fb9', 'Description': 'A data set containing metadata.', 'Reference URL': None}, {'Display Name': -'Cohort Registry Store', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Cohort Registry Store)', 'GUID': '4fd08eac-b864-4baa-a518-b31eb6d6ca4a', 'Description': 'Stores information about the -repositories registered in the open metadata repository cohort.', 'Reference URL': None}, {'Display Name': 'Open Metadata Store Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata -Store Services)', 'GUID': '7d9b5fa2-71b1-46bd-8bac-42a8089cd6ab', 'Description': 'Provides generic open metadata retrieval and management services for the Open Frameworks and Open Metadata Access Services (OMASs).', 'Reference URL': None}]} | -[{'Display Name': 'Asset Metadata Catalog', 'Qualified Name': 'Egeria:ValidMetadataValue:InventoryCatalog:deployedImplementationType-(Asset Metadata Catalog)', 'GUID': 'fbe0db34-3996-4be3-8294-422bcd8732d8', 'Description': 'A catalog of -metadata that describes assets such as deployed systems, data sources, and processes.', 'Reference URL': None}, {'Display Name': 'Valid Metadata OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Valid Metadata OMVS)', 'GUID': 'eb9e44c2-41a4-4ded-aeaf-9d9cc46648f0', 'Description': 'Maintain and query valid values for metadata.', 'Reference URL': None}, {'Display -Name': 'Event Broker', 'Qualified Name': 'Egeria:ValidMetadataValue:EventBroker:deployedImplementationType-(Event Broker)', 'GUID': '6b5f1764-c6ee-4cb6-b279-13502c4ad22b', 'Description': 'A system that manages the asynchronous exchange of -messages (events) from once to potentially many recipients. Typically, this exchange of events is organized into groups called topics.', 'Reference URL': None}, {'Display Name': 'Script File', 'Qualified Name': -'Egeria:ValidMetadataValue:ScriptFile:deployedImplementationType-(Script File)', 'GUID': 'e348b1a3-3bc7-4921-be4f-572fecde32e4', 'Description': 'A file containing code that is interpreted when it is run.', 'Reference URL': None}, {'Display -Name': 'Open Metadata Repository', 'Qualified Name': 'Egeria:ValidMetadataValue:MetadataRepository:deployedImplementationType-(Open Metadata Repository)', 'GUID': '106a41b6-20a5-4c12-b62f-26c328d385f2', 'Description': 'A metadata repository -supporting open metadata types and interfaces.', 'Reference URL': None}, {'Display Name': 'Video Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:VideoFile:deployedImplementationType-(Video Data File)', 'GUID': -'4eca6f52-6d0d-4219-8428-b70a841276d0', 'Description': 'A file containing a video recording.', 'Reference URL': None}, {'Display Name': 'Project Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Project Manager OMVS)', 'GUID': '0c5661d1-777e-4a0e-a8b3-70bccf80d152', 'Description': 'Set up and manage projects.', 'Reference URL': None}, {'Display Name': 'Platform -Metadata Security Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Platform Metadata Security Connector)', 'GUID': '3e0bced9-b539-4b90-a7d7-2d2034e92af1', 'Description': 'A connector that -manages authorization requests to the OMAG Server Platform.', 'Reference URL': None}, {'Display Name': 'View Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(View Server)', 'GUID': -'aa29cd35-c14a-4998-ab53-acc09a363d4b', 'Description': 'A server that provides access to end user open metadata and governance services.', 'Reference URL': None}, {'Display Name': 'External Links OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(External Links OMVS)', 'GUID': '6213b592-2758-4e28-9fc3-d3a601fa48de', 'Description': 'Manages the creation and maintenance of external reference and external -identifiers.', 'Reference URL': None}, {'Display Name': 'OMAG Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(OMAG Server)', 'GUID': '91abf546-f8f3-4d51-aa37-8839582a3ddc', 'Description': 'A -server providing Open Metadata and Governance (OMAG) services.', 'Reference URL': None}, {'Display Name': 'Software Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Software Server)', 'GUID': -'a1a96c4d-5013-4458-9c6b-6520925af6a4', 'Description': 'A callable software server.', 'Reference URL': None}, {'Display Name': 'Collection Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Collection Manager OMVS)', 'GUID': '9345334f-8baf-4cd3-a14f-08b3b09cb101', 'Description': 'Build collections of asset and other metadata.', 'Reference URL': None}, -{'Display Name': 'Webpage File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Webpage File)', 'GUID': 'ac3c99d9-4e5a-430b-aac2-091443c95349', 'Description': 'Text file encoded using HTML tags plus optional -script tags, such as JavaScript and CSS.', 'Reference URL': None}, {'Display Name': 'YAML File', 'Qualified Name': 'Egeria:ValidMetadataValue:YAMLFile:deployedImplementationType-(YAML File)', 'GUID': 'd650b328-4cd0-4f0f-b0b5-41bc8951c902', -'Description': 'A file containing properties in YAML format. This it typically used for configuration.', 'Reference URL': None}, {'Display Name': 'Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(Data File)', 'GUID': '4985b866-8a79-4298-9b87-fcb5b0c42692', 'Description': 'A file containing data. This may be structured data, text or some form or media file.', 'Reference -URL': None}, {'Display Name': 'pyegeria', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareLibrary:deployedImplementationType-(pyegeria)', 'GUID': '3a3cba55-4a6d-45a4-becf-32e93041e6b0', 'Description': "Python language library supporting -calls to Egeria''s REST APIs.", 'Reference URL': None}, {'Display Name': 'File System Directory', 'Qualified Name': 'Egeria:ValidMetadataValue:FileFolder:deployedImplementationType-(File System Directory)', 'GUID': -'d1986b2f-cc82-4a5a-9e9a-559ec9e98edc', 'Description': 'A directory (folder) that holds files that are potential data sources in a file system.', 'Reference URL': None}, {'Display Name': 'Repository Governance OMES', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Repository Governance OMES)', 'GUID': '01f887c9-e73d-49bd-a279-1fee27cd21d0', 'Description': 'Dynamically govern open metadata repositories in the connected cohorts.', -'Reference URL': None}, {'Display Name': 'Executable File', 'Qualified Name': 'Egeria:ValidMetadataValue:ExecutableFile:deployedImplementationType-(Executable File)', 'GUID': '9b12b93a-60f7-495a-8e6a-4f2a75d1bed5', 'Description': 'A file -containing compiled code that can be executed.', 'Reference URL': None}, {'Display Name': 'Governance Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceEngine:deployedImplementationType-(Governance Engine)', 'GUID': -'4612ab00-dc47-48c6-94eb-552b13a70991', 'Description': 'Manages the execution of automated governance activity requested via engine actions.', 'Reference URL': None}, {'Display Name': 'People Organizer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(People Organizer OMVS)', 'GUID': '9a6c7c16-a1f6-4dc8-963d-c81d04d6e901', 'Description': 'Describe teams, roles and organizational structure.', 'Reference URL': None}, -{'Display Name': 'Governance Action', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceAction:deployedImplementationType-(Governance Action)', 'GUID': 'f86de45a-6482-4487-ab69-0e1005de8b1a', 'Description': 'A description of an action -that supports the implementation of a governance program.', 'Reference URL': None}, {'Display Name': 'Engine Host', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Engine Host)', 'GUID': -'5cd74a31-9a1f-40c6-8ce2-f0d803b76db8', 'Description': 'A server that runs governance engines.', 'Reference URL': None}, {'Display Name': 'Integration Connector', 'Qualified Name': -'Egeria:ValidMetadataValue:IntegrationConnector:deployedImplementationType-(Integration Connector)', 'GUID': '3870ee0a-d4a3-419f-a6d0-1440c60e5c00', 'Description': 'Connector that manages metadata exchange with a third party technology.', -'Reference URL': None}, {'Display Name': 'Software Capability', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareCapability:deployedImplementationType-(Software Capability)', 'GUID': 'a51e49cd-baa1-4195-9766-e97dae5bc7a4', 'Description': -'A callable software capability supporting specific types of assets.', 'Reference URL': None}, {'Display Name': 'CSV Tabular Data Set Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSetCollection:deployedImplementationType-(CSV Tabular Data Set Collection)', 'GUID': 'caec0804-102b-4364-be07-2eab863efdef', 'Description': 'A collection of logically related CSV files each representing a -tabular data set.', 'Reference URL': None}, {'Display Name': 'Apache Superset', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Apache Superset)', 'GUID': '1ab15ae2-b937-496a-94a5-dea035ccf5c4', -'Description': 'Manages the definition and display of reports.', 'Reference URL': None}, {'Display Name': 'Document File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Document File)', 'GUID': -'54b9def5-f543-4259-afa5-10c89c38313d', 'Description': 'A data file containing formatted text.', 'Reference URL': None}, {'Display Name': 'Governance Action Engine', 'Qualified Name': -'Egeria:ValidMetadataValue:GovernanceActionEngine:deployedImplementationType-(Governance Action Engine)', 'GUID': '6195bb4b-8fce-43f8-a92a-8ec12bf4a5b9', 'Description': 'A governance engine that runs governance action services.', 'Reference -URL': None}, {'Display Name': 'Metadata Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Server)', 'GUID': 'a672aa09-fa2d-4dc8-86c1-a8ac9a993982', 'Description': 'A software server that -supports the storage of metadata.', 'Reference URL': None}, {'Display Name': 'Android Phone', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Android Phone)', 'GUID': -'357bd44d-0325-4d99-a0be-203ba77ce713', 'Description': 'A smartphone that runs the Android operating system.', 'Reference URL': None}, {'Display Name': 'Apple iPhone', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple iPhone)', 'GUID': '6330b52b-1e35-497e-8721-b94b94cb45ab', 'Description': 'A smartphone from Apple Inc. that runs the iOS operating system.', 'Reference URL': -None}, {'Display Name': 'Governance Action Process', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionProcess:deployedImplementationType-(Governance Action Process)', 'GUID': '0d970e05-6310-49dd-863f-c73401211140', 'Description': -'A modelled workflow process to perform a governance action.', 'Reference URL': None}, {'Display Name': 'Marquez Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Marquez Server)', 'GUID': -'6f559c4c-2c94-43a5-a3e6-089f62dfe39e', 'Description': 'A data catalog for data observability.', 'Reference URL': None}, {'Display Name': 'CSV Tabular Data Set', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(CSV Tabular Data Set)', 'GUID': 'ad10c3c7-e1f2-45d0-8318-2cae98129610', 'Description': 'A CSV file accessed as a tabular data set.', 'Reference URL': None}, {'Display -Name': 'Product Catalog OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Product Catalog OMVS)', 'GUID': 'c386df0a-a24e-4f64-afa8-aa09f3fdbe8d', 'Description': 'Supports researching the available -digital products and managing the subscriptions.', 'Reference URL': None}, {'Display Name': 'Jupyter Notebook File', 'Qualified Name': 'Egeria:ValidMetadataValue:JSONFile:deployedImplementationType-(Jupyter Notebook File)', 'GUID': -'15dfcf08-d73c-48ae-94e7-dfc3bc916265', 'Description': 'Text file encoded in JSON that describes a mixture of python code and descriptive text.', 'Reference URL': None}, {'Display Name': 'REST API Endpoint', 'Qualified Name': -'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(REST API Endpoint)', 'GUID': '770c5be2-a5fd-44a7-96d0-96468b9d6473', 'Description': 'A callable network address supporting the REST protocol.', 'Reference URL': None}, {'Display -Name': 'Open Connector Framework (OCF) Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Open Connector Framework (OCF) Connector)', 'GUID': 'df24f7a6-57e6-4991-ae7a-1dfbdf48fd55', -'Description': 'A pluggable software component that conforms to the Open Connector Framework (OCF).', 'Reference URL': None}, {'Display Name': 'Asset', 'Qualified Name': 'Egeria:ValidMetadataValue:Asset:deployedImplementationType-(Asset)', -'GUID': '0e544ca8-f5f7-49dc-ab78-6d4caedfb397', 'Description': 'A description of a digital resource.', 'Reference URL': None}, {'Display Name': 'UNIX File System', 'Qualified Name': -'Egeria:ValidMetadataValue:FileSystem:deployedImplementationType-(UNIX File System)', 'GUID': '93e1b6df-f597-4d20-b846-7ece880478dc', 'Description': 'A Unix capability that manages hierarchically organized files on persistent storage.', -'Reference URL': None}, {'Display Name': 'Community Matters OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Community Matters OMVS)', 'GUID': '977629ef-a3c3-4deb-bd1f-4b96345c42db', -'Description': 'Manages the definitions of communities, their leaders and membership.', 'Reference URL': None}, {'Display Name': 'Template Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Template Manager OMVS)', 'GUID': 'e2d3906d-c6f4-45cf-ab5e-36e9d1fb4722', 'Description': 'Retrieve and refine the templates for use during cataloguing.', 'Reference URL': -None}, {'Display Name': 'Connection Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Connection Maker OMVS)', 'GUID': '47fe7a5d-6ba2-446f-823f-fe48ad3d8613', 'Description': 'Manages the -creation and maintenance of connections, connector types and endpoints.', 'Reference URL': None}, {'Display Name': 'Business Application', 'Qualified Name': 'Egeria:ValidMetadataValue:Application:deployedImplementationType-(Business -Application)', 'GUID': '6ac87de8-d01f-46bf-8881-0ea3204e716b', 'Description': 'Software supporting a business function.', 'Reference URL': None}, {'Display Name': 'Data Asset', 'Qualified Name': -'Egeria:ValidMetadataValue:DataAsset:deployedImplementationType-(Data Asset)', 'GUID': 'df79a55a-8cd8-4cf5-a35c-c8bea53db0c4', 'Description': 'A collection of data, either at rest or in motion.', 'Reference URL': None}, {'Display Name': -'Apple iPad', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple iPad)', 'GUID': '92d0e4c8-d7f5-4e8b-ad30-f6f20aaead98', 'Description': 'A tablet from Apple Inc. that runs the iOS operating -system.', 'Reference URL': None}, {'Display Name': 'JSON Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:JSONFile:deployedImplementationType-(JSON Data File)', 'GUID': 'f8cd55d2-394e-4589-b16b-b100eeb6f158', 'Description': 'A -description of a file that follows the JavaScript Object Notation specification.', 'Reference URL': None}, {'Display Name': 'Lineage Linker OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Lineage Linker OMVS)', 'GUID': 'ee76a68f-884e-4198-81e2-3c3da03cc8d1', 'Description': 'Manages the creation and maintenance of lineage relationships.', 'Reference URL': -None}, {'Display Name': 'Open Metadata Archive Store Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Open Metadata Archive Store Connector)', 'GUID': -'b0dcf516-4440-4cd3-980b-7cc5eccb9d43', 'Description': 'Reads and writes open metadata types and instances to an open metadata archive.', 'Reference URL': None}, {'Display Name': 'Audio Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:AudioFile:deployedImplementationType-(Audio Data File)', 'GUID': 'e08ff316-5e85-4088-9cd5-d737ed81ccc2', 'Description': 'A file containing an audio recording.', 'Reference URL': None}, {'Display Name': 'Metadata -Access Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Access Server)', 'GUID': '940dda2a-e86d-4ba2-b889-500431d47f70', 'Description': 'A server that provides access to one or more -open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Administration Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Administration Services)', 'GUID': -'d8e2d72a-cea9-404f-867d-775a2fec88a5', 'Description': 'Platform service providing configuration of services for an Open Metadata and Governance server (OMAG Server).', 'Reference URL': None}, {'Display Name': 'Apache Kafka Topic', 'Qualified -Name': 'Egeria:ValidMetadataValue:Topic:deployedImplementationType-(Apache Kafka Topic)', 'GUID': '20f81045-aa24-4374-92cb-d045ac3e24f6', 'Description': 'An event topic supporting high-speed, reliable event exchange.', 'Reference URL': None}, -{'Display Name': 'PostgreSQL database manager (RDBMS)', 'Qualified Name': 'Egeria:ValidMetadataValue:DatabaseManager:deployedImplementationType-(PostgreSQL database manager (RDBMS))', 'GUID': 'cdfedb9c-8020-422d-b89e-40c57df22bd0', -'Description': 'The PostgreSQL capability that manages collections of data called relational databases which in turn are organized into a tabular format and accessed via the Structured Query Language (SQL).', 'Reference URL': None}, {'Display -Name': 'Watchdog Action OMES', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Watchdog Action OMES)', 'GUID': '0be60ce5-f7d8-4b41-8f93-9c30851bcd28', 'Description': 'Monitors for particular -situations/events and notifies registered subscribers.', 'Reference URL': None}, {'Display Name': 'Smart Phone', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Smart Phone)', 'GUID': -'6f617a49-2f25-44d0-b507-d2e4ac3db603', 'Description': 'A two-handed portable device with network, touchscreen, and battery power.', 'Reference URL': None}, {'Display Name': 'Open Governance Framework Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Governance Framework Services)', 'GUID': '23e704d7-287c-4672-9666-04519563edbf', 'Description': 'Support open governance services.', 'Reference URL': None}, {'Display -Name': 'Survey Action Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:SurveyActionEngine:deployedImplementationType-(Survey Action Engine)', 'GUID': 'fe4bcc14-985b-421d-b98e-3fddbc884811', 'Description': 'A governance engine that runs -survey action services.', 'Reference URL': None}, {'Display Name': 'Data Feed', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFeed:deployedImplementationType-(Data Feed)', 'GUID': '8ff94698-32b6-4765-bcc6-b1357bc1d55b', 'Description': 'An -event-based data source', 'Reference URL': None}, {'Display Name': 'Integration Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Integration Server)', 'GUID': -'f7ddc726-7f68-466b-a1e5-cf365d31714b', 'Description': 'A software server that supports the exchange of data.', 'Reference URL': None}, {'Display Name': 'Open Metadata Archive', 'Qualified Name': -'Egeria:ValidMetadataValue:ArchiveFile:deployedImplementationType-(Open Metadata Archive)', 'GUID': '93d47fe4-8b62-4c1e-af89-26743abc6bc8', 'Description': 'An archive file containing pre-defined metadata types and instances.', 'Reference -URL': None}, {'Display Name': 'Repository Governance Engine', 'Qualified Name': 'Egeria:ValidMetadataValue:RepositoryGovernanceEngine:deployedImplementationType-(Repository Governance Engine)', 'GUID': '9a754469-05a1-4d4c-b3b5-6b3ee4207ac7', -'Description': 'A governance engine that runs repository governance services.', 'Reference URL': None}, {'Display Name': 'Subject Area OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Subject -Area OMVS)', 'GUID': 'c872a3bb-9945-445f-afc8-9d73eaf3c398', 'Description': 'Manages the definition of subject areas and reporting on their contents.', 'Reference URL': None}, {'Display Name': 'Platform Operational Services', 'Qualified -Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Platform Operational Services)', 'GUID': '5883c7fb-b339-4dce-98dc-887dfaf8371a', 'Description': 'A platform service that provides information about the registered -services and connectors available in an OMAG Server Platform along with services to control and query information about the OMAG Servers running on the platform.', 'Reference URL': None}, {'Display Name': 'Apple MacBook Pro', 'Qualified -Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Apple MacBook Pro)', 'GUID': 'b7327658-41c3-4a53-877b-dd1b7a1dbac4', 'Description': 'A professional laptop supplied by Apple Inc that runs the macOS operating -system.', 'Reference URL': None}, {'Display Name': 'Server Metadata Security Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Server Metadata Security Connector)', 'GUID': -'5aa6a746-9dd4-4cbb-b1aa-e05707a156ce', 'Description': 'A connector that manages authorization requests to the OMAG Server.', 'Reference URL': None}, {'Display Name': 'Data Feed', 'Qualified Name': -'Egeria:ValidMetadataValue:Topic:deployedImplementationType-(Data Feed)', 'GUID': 'e3e25371-65da-47a8-bb73-feb7a00b2afe', 'Description': 'An organized event-based data source that supports subscription to a named data feed.', 'Reference URL': -None}, {'Display Name': 'PostgreSQL Relational Database Schema', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedDatabaseSchema:deployedImplementationType-(PostgreSQL Relational Database Schema)', 'GUID': -'e46811ac-8b86-42c3-aaea-2fe55f474044', 'Description': 'A database schema hosted on a PostgreSQL relational database server capable of being called through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Integration Daemon -Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Integration Daemon Services)', 'GUID': 'fbd8b372-d355-49be-af75-c38ee1f0726d', 'Description': 'Hosts integration connectors that are -exchanging metadata with third party technologies.', 'Reference URL': None}, {'Display Name': 'Governance Service', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceService:deployedImplementationType-(Governance Service)', 'GUID': -'f5e38a2f-911d-4718-9039-0eaa25f5b9ee', 'Description': 'Provides the description of a component that implements an automated governance activity.', 'Reference URL': None}, {'Display Name': 'Watchdog Action Service', 'Qualified Name': -'Egeria:ValidMetadataValue:WatchdogActionService:deployedImplementationType-(Watchdog Action Service)', 'GUID': 'd4af2e0f-78db-4b93-bf5d-53632b33c67a', 'Description': 'A connector that coordinates notifications based on situations/events.', -'Reference URL': None}, {'Display Name': 'Tabular Data Set', 'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(Tabular Data Set)', 'GUID': 'f3d81f48-1824-4608-962e-90f3f239e861', 'Description': 'A logical -collection of data, either at rest or in motion, organized into a tabular (columnar) format.', 'Reference URL': None}, {'Display Name': 'Engine Host Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Engine Host Services)', 'GUID': '5195a98e-9e4a-41f1-b00d-1a26cee3e20d', 'Description': 'Host one or more engine services that are actively managing governance of open -metadata and the digital landscape.', 'Reference URL': None}, {'Display Name': 'Archive File', 'Qualified Name': 'Egeria:ValidMetadataValue:ArchiveFile:deployedImplementationType-(Archive File)', 'GUID': -'6148ab2c-26e3-4cb5-870d-d95d070c910f', 'Description': 'A file containing compressed files. These files may be organized into a directory (folder) structure.', 'Reference URL': None}, {'Display Name': 'Data Officer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Officer OMVS)', 'GUID': 'f81202aa-0572-47ae-a6db-5e71459addf2', 'Description': 'Manage the governance of data.', 'Reference URL': None}, {'Display Name': 'Properties -File', 'Qualified Name': 'Egeria:ValidMetadataValue:PropertiesFile:deployedImplementationType-(Properties File)', 'GUID': 'f83e006c-a2bf-40e5-8c04-45e93dee4967', 'Description': 'A file containing a list of properties, typically used for -configuration of some software.', 'Reference URL': None}, {'Display Name': 'Data Discovery OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Discovery OMVS)', 'GUID': -'86efdf03-9cce-4572-bf8e-4b6cca299161', 'Description': 'Define and search for new data resources.', 'Reference URL': None}, {'Display Name': 'OMRS Repository Connector', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(OMRS Repository Connector)', 'GUID': 'a6e46bec-a80a-417e-931f-f6d044ff0f33', 'Description': 'Maps open metadata repository calls defined by the Open Metadata -Repository Services (OMRS) to a metadata repository API and event notifications.', 'Reference URL': None}, {'Display Name': 'Integration Daemon', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Integration Daemon)', 'GUID': 'b2d8d57f-df68-4280-9a41-3df830c3444f', 'Description': 'A server that runs integration connectors that synchronize metadata between different -types of technologies.', 'Reference URL': None}, {'Display Name': 'Digital Business OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Digital Business OMVS)', 'GUID': -'4a74aa92-3fc7-4160-904a-cfd1bb384e43', 'Description': 'Manages the definition and maintenance of the business context description used to identify where business value is being derived from.', 'Reference URL': None}, {'Display Name': -'Dynamic Integration Group', 'Qualified Name': 'Egeria:ValidMetadataValue:IntegrationGroup:deployedImplementationType-(Dynamic Integration Group)', 'GUID': '7a49ed4e-6810-4520-836d-aebc8ee341d7', 'Description': 'Provides the list of -integration connectors that should run in an Integration Daemon. The Integration Daemon is configured with the qualified names of the integration group(s) that provide its connector list.', 'Reference URL': None}, {'Display Name': -'Governance Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Governance Officer OMVS)', 'GUID': '3823da46-43d3-4859-add0-59204bf904dd', 'Description': 'Maintain governance definitions -used to define any governance domain.', 'Reference URL': None}, {'Display Name': 'File', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(File)', 'GUID': 'b5bb473d-947e-4ffb-8605-b8364c5ce85c', 'Description': -'A description of a file stored in a file system.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Tabular Data Set Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:TabularDataSetCollection:deployedImplementationType-(PostgreSQL Tabular Data Set Collection)', 'GUID': 'c113b127-ae8d-417f-90f2-92108c141270', 'Description': 'A database schema hosted on a PostgreSQL relational -database server that has the tabular data set collection interface.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(PostgreSQL Server)', -'GUID': 'd3ea92d0-c53b-4cc8-84d8-764648323d26', 'Description': 'A database server running the PostgreSQL software.', 'Reference URL': None}, {'Display Name': 'Smartphone', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Smartphone)', 'GUID': '94a91316-9f23-4ab5-a907-3c8c6cfea611', 'Description': 'A handheld portable device with network, touchscreen, and battery power.', 'Reference URL': -None}, {'Display Name': 'Metadata Expert OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Metadata Expert OMVS)', 'GUID': '3b1481ad-8b18-48b9-97c1-0a627d97dd65', 'Description': 'Provides advanced -metadata management features for experts in open metadata.', 'Reference URL': None}, {'Display Name': 'Keystore File', 'Qualified Name': 'Egeria:ValidMetadataValue:KeyStoreFile:deployedImplementationType-(Keystore File)', 'GUID': -'409f12a7-b4a3-4e0f-a9bd-8094813346d6', 'Description': 'An encrypted data store containing authentication and related security information.', 'Reference URL': None}, {'Display Name': 'Data Folder', 'Qualified Name': -'Egeria:ValidMetadataValue:DataFolder:deployedImplementationType-(Data Folder)', 'GUID': '0b18ef33-84ee-476d-92fc-82c109824261', 'Description': 'A directory (folder) that holds files representing a single data source.', 'Reference URL': -None}, {'Display Name': 'Endpoint', 'Qualified Name': 'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(Endpoint)', 'GUID': '69a0e224-70d7-43e4-a4af-ecd0d4fc54eb', 'Description': 'A callable network address.', 'Reference URL': -None}, {'Display Name': 'DevOps Pipeline OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(DevOps Pipeline OMVS)', 'GUID': 'c8c83907-20be-4f21-9ee6-64ac21aafd83', 'Description': 'Maintain the -metadata about the assets managed by a devops pipeline.', 'Reference URL': None}, {'Display Name': 'Parquet Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:ParquetFile:deployedImplementationType-(Parquet Data File)', 'GUID': -'734d12c6-0459-42ef-bd41-2ce6e45559ba', 'Description': 'A data file which is formatted using the Apache Parquet format.', 'Reference URL': None}, {'Display Name': 'Action Author OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Action Author OMVS)', 'GUID': '559775a7-ce1e-4f6f-973c-05b86ed2d57f', 'Description': 'Maintain definitions of governance actions such as governance action processes and -governance action types.', 'Reference URL': None}, {'Display Name': 'Governance Action Type', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionType:deployedImplementationType-(Governance Action Type)', 'GUID': -'eda3449b-d928-49aa-a67c-bc934e2a2102', 'Description': 'A modelled governance action with a single step.', 'Reference URL': None}, {'Display Name': 'Avro Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:AvroFile:deployedImplementationType-(Avro Data File)', 'GUID': 'f0fee71a-ed03-44f7-aee3-633732473250', 'Description': 'A description of a file that follows the Apache Avro specification.', 'Reference URL': None}, -{'Display Name': 'Repository Governance Service Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:RepositoryGovernanceService:deployedImplementationType-(Repository Governance Service Connector)', 'GUID': -'958fe597-6db9-4c0f-bf3e-54331934b792', 'Description': 'A connector that dynamically governs the activity of the open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Watchdog Action Engine', 'Qualified Name': -'Egeria:ValidMetadataValue:WatchdogActionEngine:deployedImplementationType-(Watchdog Action Engine)', 'GUID': 'ac264c61-8b57-4afd-a505-d0a2c1fd54cd', 'Description': 'A governance engine that runs watchdog action services.', 'Reference URL': -None}, {'Display Name': 'Log File', 'Qualified Name': 'Egeria:ValidMetadataValue:LogFile:deployedImplementationType-(Log File)', 'GUID': 'b924be69-f87f-4807-8261-5a899a3508dd', 'Description': 'Identifies a data file as one containing log -records.', 'Reference URL': None}, {'Display Name': 'Relational database manager (RDBMS)', 'Qualified Name': 'Egeria:ValidMetadataValue:DatabaseManager:deployedImplementationType-(Relational database manager (RDBMS))', 'GUID': -'dd43d7d7-7c32-42e8-8112-67fa5d5ed15f', 'Description': 'A capability that manages collections of data called relational databases which in turn are organized into a tabular format and accessed via the Structured Query Language (SQL).', -'Reference URL': None}, {'Display Name': 'Program File', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFile:deployedImplementationType-(Program File)', 'GUID': '822af572-069f-4f89-8a52-9a7d5a70092f', 'Description': 'A file containing -program logic.', 'Reference URL': None}, {'Display Name': 'Automated Curation OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Automated Curation OMVS)', 'GUID': -'babf18f0-3f11-439b-8fe0-494aab24f58d', 'Description': "Manage Egeria''s automation services.", 'Reference URL': None}, {'Display Name': '3D Image Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:ThreeDImageFile:deployedImplementationType-(3D Image Data File)', 'GUID': '8c7cc3be-365a-48db-bfba-a64da2a441e4', 'Description': 'A file containing a three-dimensional image.', 'Reference URL': None}, {'Display -Name': 'Apache Airflow Server', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Apache Airflow Server)', 'GUID': 'f55a0afd-656c-4550-994b-52c8acfc86f7', 'Description': 'Runs data movement and -transformation pipelines.', 'Reference URL': None}, {'Display Name': 'Multi Language OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Multi Language OMVS)', 'GUID': -'8905a42b-bffc-4e4d-b3af-55c90411914f', 'Description': 'Manages the creation and maintenance of translations for open metadata elements.', 'Reference URL': None}, {'Display Name': 'JDBC Endpoint', 'Qualified Name': -'Egeria:ValidMetadataValue:Endpoint:deployedImplementationType-(JDBC Endpoint)', 'GUID': 'f23c929f-c69a-445e-a0a5-6ae20206e52d', 'Description': 'A callable network address supporting the JDBC protocol.', 'Reference URL': None}, {'Display -Name': 'Docker Container', 'Qualified Name': 'Egeria:ValidMetadataValue:VirtualContainer:deployedImplementationType-(Docker Container)', 'GUID': '8020b056-79be-4378-9e5a-44bc6f3453b5', 'Description': 'A virtual container using the docker -platform.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Tabular Data Set', 'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(PostgreSQL Tabular Data Set)', 'GUID': -'aa50ce4c-917d-4880-a987-d6c771b822b2', 'Description': 'A database table hosted on a PostgreSQL relational database server that has the tabular data set interface.', 'Reference URL': None}, {'Display Name': 'Database Server', 'Qualified -Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Database Server)', 'GUID': 'ff82019c-8faa-4ac3-8f99-c3a8477743e9', 'Description': 'A software server that supports databases.', 'Reference URL': None}, {'Display -Name': 'JDBC Relational Database', 'Qualified Name': 'Egeria:ValidMetadataValue:RelationalDatabase:deployedImplementationType-(JDBC Relational Database)', 'GUID': '878b6fb6-a203-42b5-a04c-4f40de319de9', 'Description': 'A database hosted on a -relational database server callable through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Survey Action Service', 'Qualified Name': 'Egeria:ValidMetadataValue:SurveyActionService:deployedImplementationType-(Survey Action -Service)', 'GUID': 'f3bb3f15-6d09-451d-804c-23ae00529d06', 'Description': 'A connector that coordinates asset surveys.', 'Reference URL': None}, {'Display Name': 'Server Operations', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Server Operations)', 'GUID': '2cf2d01c-30a4-4d37-8347-987f5ecad623', 'Description': 'Supports the start up and shutdown of OMAG Servers.', 'Reference URL': None}, -{'Display Name': 'Open Metadata Repository Services (OMRS)', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata Repository Services (OMRS))', 'GUID': '4f0083f2-32b2-4c22-b78e-e4ff126864a3', -'Description': 'Manages the synchronization, retrieval and maintenance of metadata stored in open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Data Set', 'Qualified Name': -'Egeria:ValidMetadataValue:DataAsset:deployedImplementationType-(Data Set)', 'GUID': '44ad5f7e-804d-4d24-a837-0e7021b12996', 'Description': 'A logical collection of data, either at rest or in motion.', 'Reference URL': None}, {'Display Name': -'Metadata Repository', 'Qualified Name': 'Egeria:ValidMetadataValue:MetadataRepository:deployedImplementationType-(Metadata Repository)', 'GUID': '3e88a901-804c-4a4a-a89c-983f76e7966b', 'Description': 'A data store containing metadata.', -'Reference URL': None}, {'Display Name': 'Runtime Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Runtime Manager OMVS)', 'GUID': '63396929-d549-4c87-b2bb-2f3f82e31796', 'Description': -'Retrieve configuration and status from platforms and servers.', 'Reference URL': None}, {'Display Name': 'API Manager', 'Qualified Name': 'Egeria:ValidMetadataValue:APIManager:deployedImplementationType-(API Manager)', 'GUID': -'18dbff2f-0909-4e85-bbe2-5b5332ddf1f8', 'Description': 'A software capability that provides callable APIs.', 'Reference URL': None}, {'Display Name': 'Data Designer OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Designer OMVS)', 'GUID': 'ee1e73dd-7161-4df1-9732-0c8f91e46684', 'Description': 'Create data specifications to describe data requirements.', 'Reference URL': None}, -{'Display Name': 'Data File Collection', 'Qualified Name': 'Egeria:ValidMetadataValue:DataFileCollection:deployedImplementationType-(Data File Collection)', 'GUID': '6cfedd7c-4aaa-49f2-8e67-b70949928ce6', 'Description': 'A collection of -logically related files representing a single data source.', 'Reference URL': None}, {'Display Name': 'Reference Data OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Reference Data OMVS)', -'GUID': '4b83dd72-f74f-4291-b43a-995f6c31aa80', 'Description': 'Work with code tables and associated reference data.', 'Reference URL': None}, {'Display Name': 'Small Form Factor (SFF) Computer', 'Qualified Name': -'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Small Form Factor (SFF) Computer)', 'GUID': '4636a5c7-b1e9-4a65-8143-97e362efcc07', 'Description': 'A small hardware server enclosed in a protective case that can sit -under a desk.', 'Reference URL': None}, {'Display Name': 'Software Component', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(Software Component)', 'GUID': -'29b4afae-7000-4837-a225-d37bc036d503', 'Description': 'A deployable software component.', 'Reference URL': None}, {'Display Name': 'Conformance Suite Services', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Conformance Suite Services)', 'GUID': '9984876e-2593-453e-b319-1fe7b096fea9', 'Description': 'Run automated open metadata conformance suite services.', 'Reference URL': -None}, {'Display Name': 'Governance Action Service', 'Qualified Name': 'Egeria:ValidMetadataValue:GovernanceActionService:deployedImplementationType-(Governance Action Service)', 'GUID': 'd77773c9-cc7c-4571-8472-f2764aa1471a', 'Description': -'A connector that coordinates governance of digital resources and metadata.', 'Reference URL': None}, {'Display Name': 'REST API Manager', 'Qualified Name': 'Egeria:ValidMetadataValue:APIManager:deployedImplementationType-(REST API Manager)', -'GUID': '0a9947b4-6686-4710-a64f-645f9ce430bd', 'Description': 'A software capability that provides callable APIs supporting the REST protocol.', 'Reference URL': None}, {'Display Name': 'Product Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Product Manager OMVS)', 'GUID': '1eb4a943-c0d4-46d6-acdf-c35330cc3c5e', 'Description': 'Supports the development and maintenance of digital products.', 'Reference URL': -None}, {'Display Name': 'Actor Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Actor Manager OMVS)', 'GUID': 'beb3afb3-027c-479c-bec0-aa239fbcb658', 'Description': 'Manages the -definitions of user identities, actor profiles, contact details and actor roles.', 'Reference URL': None}, {'Display Name': 'Source Code File', 'Qualified Name': 'Egeria:ValidMetadataValue:SourceCodeFile:deployedImplementationType-(Source -Code File)', 'GUID': 'd615f94a-fc78-4f23-8b35-f4af454bc01e', 'Description': 'A text file containing a program written in a language that needs to be complied into an executable form before it can run.', 'Reference URL': None}, {'Display -Name': 'Laptop Computer', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Laptop Computer)', 'GUID': '38ecbb12-c172-461c-aeac-8d7da0502cf5', 'Description': 'A portable computer with a screen, -keyboard, and battery power.', 'Reference URL': None}, {'Display Name': 'Data Engineer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Data Engineer OMVS)', 'GUID': -'98d78723-7b6c-4ca9-9403-0c4d6a944552', 'Description': 'Manages the creation and maintenance of data pipelines, open metadata templates and reference data.', 'Reference URL': None}, {'Display Name': 'Location Arena OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Location Arena OMVS)', 'GUID': '8b74f416-28a6-4a8c-ab08-6e32eb3ec8e1', 'Description': 'Manages the creation and maintenance of locations.', 'Reference URL': None}, -{'Display Name': 'Bare Metal Computer', 'Qualified Name': 'Egeria:ValidMetadataValue:BareMetalComputer:deployedImplementationType-(Bare Metal Computer)', 'GUID': 'f593a0c5-4a71-4fd9-9963-dbe6ba464865', 'Description': 'A computer (hardware) -with an operating system for running software.', 'Reference URL': None}, {'Display Name': 'Schema Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Schema Maker OMVS)', 'GUID': -'5fef72f9-7580-4dd1-989a-6946555c0814', 'Description': 'Manages the creation and maintenance of schema elements.', 'Reference URL': None}, {'Display Name': 'JDBC Relational Database Schema', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedDatabaseSchema:deployedImplementationType-(JDBC Relational Database Schema)', 'GUID': '10eb9965-80d1-447a-a207-68e39c2ee755', 'Description': 'A database schema hosted on a relational database server callable -through a JDBC Driver.', 'Reference URL': None}, {'Display Name': 'Build Instruction File', 'Qualified Name': 'Egeria:ValidMetadataValue:BuildInstructionFile:deployedImplementationType-(Build Instruction File)', 'GUID': -'d5fa0c48-e24c-44b4-910c-a4f29ec0f889', 'Description': 'A file containing instructions to run a build of a software artifact or system.', 'Reference URL': None}, {'Display Name': 'Raster Data File', 'Qualified Name': -'Egeria:ValidMetadataValue:RasterFile:deployedImplementationType-(Raster Data File)', 'GUID': 'c9800490-8706-4d91-bd8f-28813239add3', 'Description': 'A file containing an image as a matrix of pixels.', 'Reference URL': None}, {'Display Name': -'Security Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Security Officer OMVS)', 'GUID': '9bd9e4c0-c994-447b-b1c5-5f3499d3a458', 'Description': 'Set up and review rules and security -tags to protect data and systems.', 'Reference URL': None}, {'Display Name': 'Metadata Access Store', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(Metadata Access Store)', 'GUID': -'1ae11a7d-cdef-48d2-bbad-1177366c6fb6', 'Description': 'A server that provides access to one or more open metadata repositories.', 'Reference URL': None}, {'Display Name': 'Audit Log Destination', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Audit Log Destination)', 'GUID': '8a1ab260-6acd-4549-b8c3-57243eba9f8e', 'Description': 'Reads and writes records to the Open Metadata Repository Services (OMRS) audit -log.', 'Reference URL': None}, {'Display Name': 'Cohort Registry Store', 'Qualified Name': 'Egeria:ValidMetadataValue:CohortRegistryStore:deployedImplementationType-(Cohort Registry Store)', 'GUID': '6602791d-eb14-4bb6-ae91-7430018b312d', -'Description': 'A data store containing cohort membership registration details.', 'Reference URL': None}, {'Display Name': 'Kubernetes Cluster', 'Qualified Name': 'Egeria:ValidMetadataValue:HostCluster:deployedImplementationType-(Kubernetes -Cluster)', 'GUID': '96248666-e040-460b-96dd-2ce10d68664f', 'Description': 'A host cluster managing containerized applications.', 'Reference URL': None}, {'Display Name': 'Notification Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Notification Manager OMVS)', 'GUID': '69460e45-4572-430f-8629-46fd1cb02e00', 'Description': 'Manages the definitions of notifications. This includes the definition of the -trigger for the notification, the style of notification and the recipient.', 'Reference URL': None}, {'Display Name': 'OMAG Server Platform', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareServerPlatform:deployedImplementationType-(OMAG -Server Platform)', 'GUID': 'e76e450d-9533-4e98-b83d-c3ea7f8b0a08', 'Description': 'An Open Metadata and Governance (OMAG) runtime for running one to many OMAG Servers.', 'Reference URL': None}, {'Display Name': 'User Authentication Manager', -'Qualified Name': 'Egeria:ValidMetadataValue:UserAuthenticationManager:deployedImplementationType-(User Authentication Manager)', 'GUID': 'a8a29046-6a5b-4a6c-a655-56533757cd97', 'Description': 'Function that validates the identity of a user -via password or other form of identification', 'Reference URL': None}, {'Display Name': 'Survey Action OMES', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Survey Action OMES)', 'GUID': -'f0b0c0c4-e931-438d-bed1-8ed94f19ae63', 'Description': "Analyses the content of an asset''s real world counterpart (resource), generates annotations in a survey report that is attached to the asset in the open metadata repositories.", -'Reference URL': None}, {'Display Name': 'Feedback Manager OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Feedback Manager OMVS)', 'GUID': '7c7e02c1-d595-4a3a-bc9f-31d32e875f0e', 'Description': -'Work with note logs, comments, informal tags, ratings/reviews and likes.', 'Reference URL': None}, {'Display Name': 'Asset Catalog OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Asset Catalog -OMVS)', 'GUID': 'f9b5e92b-0ed8-4649-8e29-f3dd00ddbcfa', 'Description': 'Search and understand your assets.', 'Reference URL': None}, {'Display Name': 'Process', 'Qualified Name': -'Egeria:ValidMetadataValue:Process:deployedImplementationType-(Process)', 'GUID': 'eca3e5eb-ad51-4637-82f3-3071c6bf0ffb', 'Description': "A well-defined set of processing steps and decisions that drive a particular aspect of the -organization''s business.", 'Reference URL': None}, {'Display Name': 'Open Metadata Security Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata Security Services)', 'GUID': -'d5633baf-51da-434b-96d7-6a9f53e2c7bb', 'Description': 'Authorization services for Open Metadata and Governance', 'Reference URL': None}, {'Display Name': 'Apache Airflow DAG', 'Qualified Name': -'Egeria:ValidMetadataValue:DeployedSoftwareComponent:deployedImplementationType-(Apache Airflow DAG)', 'GUID': '85b1f2da-a7ec-487a-8af9-2b108c0dad89', 'Description': 'A python component that implements a micro-workflow that runs in Apache -Airflow. DAG stands for Directed Acyclic Graph, which describes the structure of its implementation.', 'Reference URL': None}, {'Display Name': 'Glossary Manager OMVS', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Glossary Manager OMVS)', 'GUID': '7be807f1-4e82-4bc9-a418-706b0e5af5b9', 'Description': 'Create glossary terms and organize them into folders. It supports the editing -glossary and multiple states.', 'Reference URL': None}, {'Display Name': 'Resource Connector', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Resource Connector)', 'GUID': -'2dd4b390-94d0-49d6-80d9-13dc06a7cf7b', 'Description': 'A pluggable software component that conforms to the Open Connector Framework (OCF) and provides access to a digital resource.', 'Reference URL': None}, {'Display Name': 'File System', -'Qualified Name': 'Egeria:ValidMetadataValue:FileSystem:deployedImplementationType-(File System)', 'GUID': 'e36cdfe0-91e5-4c30-b9b6-4157ee1b5810', 'Description': 'A system that manages hierarchically organized files on persistent storage.', -'Reference URL': None}, {'Display Name': 'Privacy Officer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Privacy Officer OMVS)', 'GUID': '7b0b4187-ed5d-4e39-abe7-e1ba6f7e8d11', 'Description': -'Manage the governance of privacy.', 'Reference URL': None}, {'Display Name': 'Classification Explorer OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Classification Explorer OMVS)', 'GUID': -'feb4edad-7ad6-4368-895d-bb129b12cf0c', 'Description': 'Retrieve elements based on type or the classifications/relationships attached to these metadata elements.', 'Reference URL': None}, {'Display Name': 'Tabular Data Set Collection', -'Qualified Name': 'Egeria:ValidMetadataValue:TabularDataSet:deployedImplementationType-(Tabular Data Set Collection)', 'GUID': '341e6c53-0fb2-4529-a4cc-8d2fadeec67a', 'Description': 'A collection of tabular data sets.', 'Reference URL': -None}, {'Display Name': 'Time Keeper OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Time Keeper OMVS)', 'GUID': '68a60c52-29ba-42b9-9e93-ffdcb1f8da60', 'Description': 'Manages context events -and other time related-behaviour.', 'Reference URL': None}, {'Display Name': 'Connected Asset Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Connected Asset Services)', 'GUID': -'e3f1bb36-e436-4a11-a9d0-22322d296204', 'Description': 'Common metadata services for the Open Connector Framework (OCF).', 'Reference URL': None}, {'Display Name': 'Hadoop Cluster', 'Qualified Name': -'Egeria:ValidMetadataValue:HostCluster:deployedImplementationType-(Hadoop Cluster)', 'GUID': '9df11118-9cac-416f-82ba-46e89ff9c693', 'Description': 'A cluster of nodes for big data workloads.', 'Reference URL': None}, {'Display Name': -'Markdown Document File', 'Qualified Name': 'Egeria:ValidMetadataValue:Document:deployedImplementationType-(Markdown Document File)', 'GUID': 'fdfc2d1a-9844-4965-a0cc-ccfbb2f623e3', 'Description': 'Text file encoded using markdown tags and -layout.', 'Reference URL': None}, {'Display Name': 'Vector Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:VectorFile:deployedImplementationType-(Vector Data File)', 'GUID': 'acefe346-0ab9-4a64-af9b-36f5bfc85508', 'Description': 'A -file containing an image described using mathematical formulas.', 'Reference URL': None}, {'Display Name': 'Root Technology Type', 'Qualified Name': 'Egeria:ValidMetadataValue:Referenceable:deployedImplementationType-(Root Technology Type)', -'GUID': '2b406762-fa26-4ac7-8de8-98d5dd05eaa6', 'Description': 'Root technology type.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Relational Database', 'Qualified Name': -'Egeria:ValidMetadataValue:RelationalDatabase:deployedImplementationType-(PostgreSQL Relational Database)', 'GUID': 'ff56fc56-c4a1-469e-8040-472e8fe54694', 'Description': 'A database hosted on a PostgreSQL server.', 'Reference URL': None}, -{'Display Name': 'CSV Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:CSVFile:deployedImplementationType-(CSV Data File)', 'GUID': 'a312f9d3-0fbb-4d98-8dc9-b298ec972b9e', 'Description': 'A file containing comma-separated (or similar -delimited) data.', 'Reference URL': None}, {'Display Name': 'Solution Architect OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Solution Architect OMVS)', 'GUID': -'4a3c42aa-cdb7-46ae-8ff2-7cccd9179bcb', 'Description': 'Manages the definitions of information supply chains and solution components.', 'Reference URL': None}, {'Display Name': 'Governance Action OMES', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Governance Action OMES)', 'GUID': '82bc3f4f-678d-435e-bb34-dfcbac098663', 'Description': 'Executes requested governance action services to monitor, assess and maintain -metadata and its real-world counterparts.', 'Reference URL': None}, {'Display Name': 'My Profile OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(My Profile OMVS)', 'GUID': -'d4d3ee7c-b269-4bdb-aeca-2c7fca408aef', 'Description': 'Manage information about the logged on user as well as their preferences.', 'Reference URL': None}, {'Display Name': 'PostgreSQL Server', 'Qualified Name': -'Egeria:ValidMetadataValue:SoftwareServer:deployedImplementationType-(PostgreSQL Server)', 'GUID': 'ff7c8d09-9813-453e-9895-7a2b0a6b0be3', 'Description': 'PostgreSQL is an advanced open source relational database.', 'Reference URL': None}, -{'Display Name': 'Spreadsheet Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:SpreadsheetFile:deployedImplementationType-(Spreadsheet Data File)', 'GUID': '742ae07c-ec29-482b-9260-4ab6bd8dac62', 'Description': 'A file containing -tabular data with formula.', 'Reference URL': None}, {'Display Name': 'XML Data File', 'Qualified Name': 'Egeria:ValidMetadataValue:XMLFile:deployedImplementationType-(XML Data File)', 'GUID': '5f5d53fd-028d-4870-8882-1792ce8d353e', -'Description': 'A file containing an XML structure.', 'Reference URL': None}, {'Display Name': 'Asset Maker OMVS', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Asset Maker OMVS)', 'GUID': -'2279b215-690f-469d-a1fc-dcaa8d99e36b', 'Description': 'Create new assets, software capabilities and link them to other elements.', 'Reference URL': None}, {'Display Name': 'Metadata Collection', 'Qualified Name': -'Egeria:ValidMetadataValue:MetadataCollection:deployedImplementationType-(Metadata Collection)', 'GUID': 'aafc544d-9b9d-4d9c-86a7-3d6ecaf26fb9', 'Description': 'A data set containing metadata.', 'Reference URL': None}, {'Display Name': -'Cohort Registry Store', 'Qualified Name': 'Egeria:ValidMetadataValue:DeployedConnector:deployedImplementationType-(Cohort Registry Store)', 'GUID': '4fd08eac-b864-4baa-a518-b31eb6d6ca4a', 'Description': 'Stores information about the -repositories registered in the open metadata repository cohort.', 'Reference URL': None}, {'Display Name': 'Open Metadata Store Services', 'Qualified Name': 'Egeria:ValidMetadataValue:SoftwareService:deployedImplementationType-(Open Metadata -Store Services)', 'GUID': '7d9b5fa2-71b1-46bd-8bac-42a8089cd6ab', 'Description': 'Provides generic open metadata retrieval and management services for the Open Frameworks and Open Metadata Access Services (OMASs).', 'Reference URL': None}] \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/my_connectors.py b/my_egeria/my_egeria/DemoCode/Deprecated/my_connectors.py deleted file mode 100644 index 0f8f2406..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/my_connectors.py +++ /dev/null @@ -1,595 +0,0 @@ -""" python - - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a utility function for my_egeria. - - This file is being used to test some display layout options - This is a test file to explore different ways to display Egeria data - Author: Peter Coldicott, November 2025 - - This version is to test having the user input required parameters to run - customized reports - -""" - -import os -import re -import sys - -from inflect import pl_sb_U_louse_lice_list -from textual.widget import WidgetError - -# Ensure we're using the local pyegeria, not an installed version -project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -sys.path.insert(0, project_root) - -from typing import Any -from loguru import logger - -from commands.cat.run_report import list_generic -from pyegeria import Run_Report -from pydantic import ValidationError -from textual import on -from textual.app import App -from textual.containers import Container, Horizontal, ScrollableContainer, HorizontalScroll -from textual.widgets import Static, Button, DataTable, Header, Footer, Placeholder, Input, Tree, Label - - -class MyApp(App): - CSS_PATH = "./my_reports.tcss" - - BINDINGS = [ - ("q", "quit", "Quit"), - ("ctl+e", "expand", "Expand Twisties"), - ("r", "prepare_report", "Run"), - ] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - self.selected_tech_type: str = "" - self.items: list = [] - self.created_inputs: list[Input] = [] - self.inputs_tracker: dict[str, list] = {} - self.snode_label: str = "" - # Holds rotated data as column -> list of row values - self.rotated_table: dict[str, list[str]] = {} - - def compose(self): - self.title = "My_Egeria" - self.sub_title = "Report Specification Details" - yield Header() - yield Container( - Static("Start of Technology Type list:", id="one_start"), - Tree(label="Tech Types", id="tech_table", classes="tables"), - Static("End of report specification list:", id="one_end"), - id="container1", classes="box") - yield Container( - Static("Report Specification Details", classes="box", id="two_start"), - DataTable(id="spec_extracted_datatable"), - Static("End of Report Specification Details", classes="box", id="two_end"), - id="container2", classes="box") - yield ScrollableContainer( - Static("Report Specs Input Fields", classes="box", id="four_start"), - Container(id="input_fields"), - Static("End of Report Specs Input Fields", classes="box", id="four_end"), - id="container4", classes="box") - yield Container( - Static("Report Specification Output)", classes="box", id="three_start"), - DataTable(id="spec_output_datatable"), - Static("End of Report Specification Output)", classes="box", id="three_end"), - id="container3", classes="box") - yield Footer() - - def on_mount(self): - # Import here after path is set - from pyegeria.format_set_executor import exec_report_spec - from pyegeria.base_report_formats import Tech-Type-Processes - from pyegeria import list_tech_types - - self.tech_tree = self.query_one("#tech_tree", Tree) - self.tech_tree.clear() - root_data = "A categorized list of Egeria supported Technology Types" - self.tech_tree.root.expand() - self.tech_tree.root.label = "Technology Types" - self.tech_tree.root.data = root_data - tree_root = self.tech_tree.root - self.family: str = "" - self.heading = "Report Specs" - self.subheading = "Select a report spec to execute:" - self.description = "A list of report specifications, click on one to see its attributes\ns key to run" - - try: - self.tech_type_list: dict = list_tech_types( - # format_set_name="Tech-Type-Processes", - output_format="DICT", - ) - logger.debug(f"tech_type_list: {self.tech_type_list}, type: {type(self.tech_type_list)}") - except Exception as e: - logger.debug(f"Error getting tech_type_list: {e}") - import traceback - logger.debug(traceback.format_exc()) - self.tech_type_list = {} - - for type in self.tech_type_list: - # if ttype.get("family") != self.family: - # self.family = spec.get("family") - # family_node = tree_root.add(self.family, data="family_name") - # family_node.expand() - # family_node.add(spec.get("name"), data=spec.get("description")) - # continue - # else: - # family_node.add(spec.get("name"), spec.get("description")) - # continue - tree_root.collapse_all() - self.tech_tree.refresh() - - @on(Tree.NodeSelected, "#tech_tree") - async def handle_tech_tree_node_selected(self, message: Tree.NodeSelected): - logger.debug(f"Node Selected, Processing selection") - self.selected_tech_type = message.node - logger.debug(f"selected_tech_type: {self.selected_tech_type}") - snode = message.node - logger.debug(f"snode: {snode}", type(snode)) - if snode.data: - snode_label = str(snode.label) - logger.debug(f"snode_label: {snode_label}", type(snode_label)) - else: - snode_label = str(snode.label) - logger.debug(f"snode_label: {snode_label}", type(snode_label)) - # address input fields container so we can clear it from prior selections - inputs_container = self.query_one("#input_fields", Container) - # Clear previous rows - try: - self.created_inputs: list[Input] = [] - # Remove all existing children synchronously - for child in list(inputs_container.children): - await child.remove() - except Exception as e: - # if this is first time through there is validly no container to clear - logger.debug(f"Exception clearing inputs container: {e} possibly first time through") - except WidgetError: - # assume this is forst time through and container is not yet created - pass - self.snode_label = snode_label - await self.get_named_tech_type_details(snode_label) - - if "DICT" or "FORM" in self.inputs_tracker: - outlabel = Label("Output Format: DICT to display report on screen, FORM to write to file", id="comment1") - await inputs_container.mount(outlabel) - - async def get_named_tech_type_details(self, name): - """Get the details of a named report spec and render as a flat table.""" - from pyegeria.base_report_formats import - - self.spec_name = name - logger.debug(f"get_named_tech_type_details: {self.spec_name}") - selected_tech_type = select_tech_type(self.spec_name, output_type="DICT") - logger.debug(f"selected_tech_type: {selected_tech_type}, type: {type(selected_tech_type)}") - if selected_tech_type == None: - selected_tech_type = {"NoDetails": "No details found for selected report spec"} - # Normalize the shape of the returned spec → always a dict - if isinstance(selected_tech_type, dict) and "data" in selected_tech_type: - self.extracted_tech_type: dict = selected_tech_type.get("data") or {} - elif isinstance(selected_tech_type, dict): - self.extracted_tech_type: dict = selected_tech_type or {} - else: - error_text = f"Unknown shape: {selected_tech_type}" - self.extracted_tech_type = {"Error": error_text} - - logger.debug(f"extracted_tech_type: {self.extracted_tech_type}, type: {type(self.extracted_tech_type)}") - response_data = self.extracted_tech_type - # Access the datatable used to display report specification attributes - self.spec_attribute_datatable = self.query_one("#spec_extracted_datatable", DataTable) - # Ensure columns exist only once - self.spec_attribute_datatable.clear(columns=True) - self.spec_attribute_datatable.add_columns("Attribute", "Value", "Extended Values") - self.spec_attribute_datatable.border = True - self.spec_attribute_datatable.zebra_stripes = True - self.spec_attribute_datatable.cursor_type = "row" - self.spec_attribute_datatable.refresh() - - if not response_data: - response_data = {"NoData": "No data found for selected item"} - if isinstance(response_data, list): - for list_item in response_data: - response_data: dict = list_item - logger.debug(f"list_item: {list_item}") - if isinstance(response_data, dict): - for key, value in response_data.items(): - logger.debug(f"key: {key}, value: {value}") - # check for input capable report spec parms - if key == "required_params" or key == "optional_params" or key == "spec_params" or key == "types": - logger.debug(f"Executing populate_input_fields for key: {key}") - await self.populate_input_fields(key, value) - logger.debug(f"populate_input_fields completed for key: {key}") - logger.debug(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - if vkey == "required_params" or vkey == "optional_params" or vkey == "spec_params" or vkey == "types": - logger.debug(f"Executing populate_input_fields for key: {vkey}") - await self.populate_input_fields(vkey, vvalue) - logger.debug(f"populate_input_fields completed for key: {vkey}") - logger.debug(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - logger.debug(f"v: {v}") - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - logger.debug(f"key_str: {key_str}, value_str: {value_str}") - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - elif isinstance(value, dict): - for vkey, vvalue in value.items(): - logger.debug(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - else: - logger.debug(f"else: key {key} value: {value}") - self.spec_attribute_datatable.add_row(key, value) - continue - elif isinstance(response_data, dict): - for key, value in response_data.items(): - logger.debug(f"key: {key}, value: {value}") - if key == "required_params" or key == "optional_params" or key == "spec_params" or key == "types": - logger.debug(f"Executing populate_input_fields for key: {key}") - await self.populate_input_fields(key, value) - logger.debug(f"populate_input_fields completed for key: {key}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - if vkey == "required_params" or vkey == "optional_params" or vkey == "spec_params" or vkey == "types": - logger.debug(f"Executing populate_input_fields for key: {vkey}") - await self.populate_input_fields(vkey, vvalue) - logger.debug(f"populate_input_fields completed for key: {vkey}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.spec_attribute_datatable.add_row(key, value) - continue - - self.spec_attribute_datatable.refresh() - mount_point = self.query_one("#container2", Container) - await mount_point.mount(self.spec_attribute_datatable, before="#two_end") - return - - async def execute_selected_tech_type(self, selected_name: str = "", additional_parameters: dict = None): - """ execute the selected report spec """ - from pyegeria.format_set_executor import Run_Report - - self.selected_name = selected_name - self.additional_parameters = additional_parameters - output_form = "DICT" - if len(self.additional_parameters) > 0: - logger.debug(f"additional_parameters: {self.additional_parameters}") - for key, value in self.additional_parameters.items(): - if value == "True" or value == "False": - self.additional_parameters[key] = bool(value) - else: - logger.debug(f"No additional parameters provided") - self.additional_parameters = {} - if self.additional_parameters != None: - if "inp_types" in self.additional_parameters: - output_form = self.additional_parameters["inp_types"] - - logger.debug(f"Executing report spec: {self.selected_name}") - try: - reponse = exec_tech_type(format_set_name=selected_name, - output_format=output_form, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password, - params=self.additional_parameters - ) - logger.debug(f"Return from exec_tech_type:") - logger.debug(f"reponse: {reponse}") - except (ValidationError) as e: - logger.debug(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - logger.debug(f"Exception: {e}") - import traceback - logger.debug(traceback.format_exc()) - reponse = {"error": f"Exception: {e}"} - await self.display_response(reponse) - - async def display_response(self, response): - """ Display the response from executing the selected report spec""" - self.response = response - # create and/or clear datatable for displaying data - self.spec_output_datatable = self.query_one("#spec_output_datatable", DataTable) - self.spec_output_datatable.clear(columns=True) - logger.debug(f"self.spec_output_datatable : {self.spec_output_datatable} has been created") - logger.debug(f"self.spec_output_datatable has been assigned an id : {self.spec_output_datatable.id} ") - # extract data payload from response["data"]. Then populate variables for display - if isinstance(self.response, dict) and "data" in self.response: - self.response_data = self.response.get("data") - elif isinstance(self.response, dict): - self.response_data = self.response - elif isinstance(self.response, list): - self.response_data = self.response - else: - self.response_data = [{"NoData": "No data found for selected item", "Data Content": self.response}] - - self.rotated_table.clear() - if isinstance(self.response_data, list): - for list_item in self.response_data: - response_dict = list_item if isinstance(list_item, dict) else {"Value": list_item} - logger.debug(f"list_item: {list_item}") - for key, value in response_dict.items(): - logger.debug(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - logger.debug(f"vkey: {vkey}, vvalue: {vvalue}") - self.rotated_table.setdefault(str(vkey), []).append(vvalue) - elif isinstance(value, list): - for v in value: - logger.debug(f"v: {v}") - self.rotated_table.setdefault(str(v), []).append(" ") - elif key == "kind" and value == "empty": - key_str: str = "NoData" - message: str = "For That Asset Type in this repository" - logger.debug(f"key_str: {key_str}, value_str: {message}") - # Present a single consolidated message row under NoData - self.rotated_table[key_str] = [message] - else: - logger.debug(f"else: key {key} value: {value}") - self.rotated_table.setdefault(str(key), []).append(value) - elif isinstance(self.response_data, dict): - for key, value in self.response_data.items(): - logger.debug(f"key: {key}, value: {value}") - if key == "kind": - continue - elif isinstance(value, dict): - for vkey, vvalue in value.items(): - self.rotated_table.setdefault(str(vkey), []).append(vvalue) - elif isinstance(value, list): - for v in value: - self.rotated_table.setdefault(str(v), []).append(" ") - elif key == "kind" and value == "empty": - key_str = "NoData" - message = "For That Asset Type in this repository" - logger.debug(f"key_str: {key_str}, value_str: {message}") - # Present a single consolidated message row under NoData - self.rotated_table[key_str] = [message] - else: - self.rotated_table.setdefault(str(key), []).append(value) - # now the rotated table is loaded with data, create the datatable - keys = list(self.rotated_table.keys()) - for col_key in keys: - self.spec_output_datatable.add_column(str(col_key)) - max_rows = max((len(self.rotated_table[k]) for k in keys), default=0) - for row_idx in range(max_rows): - row = [str(self.rotated_table[k][row_idx]) if row_idx < len(self.rotated_table[k]) else "" for k in keys] - self.spec_output_datatable.add_row(*row) - - # Always refresh after populating - self.spec_output_datatable.refresh() - mount_point = self.query_one("#container3", Container) - await mount_point.mount(self.spec_output_datatable, before="#three_end") - return - - async def populate_input_fields(self, key, value): - """Create interactive input rows for required/optional params.""" - self.key = key - self.input = value - inp_value: Any = None - inputs_container = self.query_one("#input_fields", Container) - - def safe_id(s: str) -> str: - return re.sub(r"[^0-9a-zA-Z_-]", "_", s) - - self.created_inputs: list[Input] = [] - # reset tracker for this render; we'll repopulate with the current inputs - self.inputs_tracker.clear() - - # Build rows according to incoming shape - if isinstance(self.input, dict): - items = list(self.input.items()) - for ikey, ivalue in items: - row = HorizontalScroll(id=f"input_row_{safe_id(str(ikey))}") - await inputs_container.mount(row) - await row.mount(Label(f"{ikey}:")) - logger.debug(f"Processing input field: {ikey}") - if ikey == "search_string": - inp_value = "*" - elif ikey == "page_size": - inp_value = '0' - elif ikey == "start_from": - inp_value = '0' - elif ikey == "starts_with": - inp_value = "True" - elif ikey == "ends_with": - inp_value = "False" - elif ikey == "ignore_case": - inp_value = "False" - elif ikey == "output_format": - inp_value = "DICT" - else: - inp_value = "" - inp = Input(value=inp_value, placeholder=str(ivalue), id=f"inp_{safe_id(str(ikey))}", classes="spaced") - await row.mount(inp) - self.created_inputs.append(inp) - # Track by input id so events can update values reliably - self.inputs_tracker[inp.id] = [str(ikey), ""] - elif isinstance(self.input, list): - # Special-case for types: just display info text - if key == "types": - row = Horizontal(id=f"input_row_{safe_id(str(key))}") - await inputs_container.mount(row) - await row.mount(Label("Output Format:")) - await row.mount(Input(value="DICT", placeholder="DICT", id=f"inp_{safe_id(str(key))}", classes="spaced")) - # informational only; no input to track - else: - for list_item in self.input: - label = str(list_item) - row = Horizontal(id=f"input_row_{safe_id(label)}") - await inputs_container.mount(row) - await row.mount(Label(f"{label}:")) - logger.debug(f"Processing input field: {label}") - if label == "search_string": - inp_value = "*" - elif label == "page_size": - inp_value = '0' - elif label == "start_from": - inp_value = '0' - elif label == "starts_with": - inp_value = "True" - elif label == "ends_with": - inp_value = "False" - elif label == "ignore_case": - inp_value = "False" - elif label == "output_format": - inp_value = "DICT" - else: - inp_value = "" - if isinstance(inp_value, list): - inp_value = ", ".join(inp_value) - logger.debug(f"inp_value: {inp_value}") - inp = Input(value=inp_value, placeholder="", id=f"inp_{safe_id(label)}") - await row.mount(inp) - self.created_inputs.append(inp) - # Track with a reasonable parameter name for list entries - self.inputs_tracker[inp.id] = [label, ""] - else: - # Unknown shape: show a message - row = Horizontal(id=f"input_row_{safe_id(str(key))}") - await inputs_container.mount(row) - await row.mount(Label(f"Error, Unknown Shape for {key}: {value}")) - - # Give focus to the first input if any - if self.created_inputs: - self.created_inputs[0].focus() - logger.debug(f"inputs_tracker: {self.inputs_tracker}") - - # Refresh the container - inputs_container.refresh() - return - - @on(Input.Changed, "#input_fields Input") - async def handle_input_changed(self, event: Input.Changed) -> None: - """Update the tracker whenever an input changes. - - We key the tracker by the actual Input id and store [param_name, value]. - """ - input_id = event.input.id - value = event.value or "" - # Initialize if missing (defensive) - if input_id not in self.inputs_tracker: - # Default param name falls back to the visible label-less id - self.inputs_tracker[input_id] = [input_id, value] - else: - self.inputs_tracker[input_id][1] = value - logger.debug(f"Changed: {input_id} -> {value}") - return - - @on(Button.Pressed, "#quit") - def handle_quit(self, event: Button.Pressed) -> None: - self.exit(200) - - def action_quit(self): - self.exit(200) - - def action_expand_spec_attribute_table(self): - self.tech_tree.root.expand_all() - return - - async def action_prepare_report(self): - # Build additional parameters from current inputs - self.new_additional_parameters = {} - self.additional_parameters = {} - if self.inputs_tracker: - logger.debug(f"self.inputs_tracker: {self.inputs_tracker}") - for _, (parm_name, parm_value) in self.inputs_tracker.items(): - self.additional_parameters[parm_name] = parm_value - logger.debug(f"additional_parameters: {self.additional_parameters}") - # run report when input fields completed - # iterate and strip the inp_ from the front of the key values if present - for key, value in self.additional_parameters.items(): - new_key:str = key.removeprefix("inp_") - self.new_additional_parameters.update({new_key: value}) - self.additional_parameters.clear() - # change the types key to output_format if present fior inclusion in parameters dict - value = self.new_additional_parameters.get("types") - if value: - self.new_additional_parameters.update({"output_format": value}) - else: - self.new_additional_parameters.update({"output_format": "DICT"}) - self.log (f"new_additional_parameters: {self.new_additional_parameters}") - self.additional_parameters = self.new_additional_parameters - logger.debug(f"additional_parameters trimmed: {self.additional_parameters}") - # Retrieve the output_format value - output_form = self.additional_parameters.get("output_format", "FORM") - logger.debug(f"output_format: {output_form}") - # if output_format is FORM, we write the report output to a file - # if not then we execute the report spec and display the output on the screen - if output_form == "FORM": - await self.report_to_file(self.snode_label, self.additional_parameters) - else: - await self.execute_selected_tech_type(self.snode_label, self.additional_parameters) - return - - async def report_to_file(self, selected_name: str = "", additional_parameters: dict = None): - """ execute the selected report spec """ - # self.selected_name = selected_name - # self.additional_parameters = additional_parameters - my_additional_parameters: dict = additional_parameters - output_form = "FORM" - if len(my_additional_parameters) > 0: - logger.debug(f"additional_parameters: {my_additional_parameters}") - if "output_format" in my_additional_parameters: - output_form = my_additional_parameters.get("output_format") - del my_additional_parameters["output_format"] - else: - output_form = "FORM" - else: - logger.debug(f"No additional parameters provided") - my_additional_parameters = {} - - logger.debug(f"Executing report spec: {selected_name}") - try: - reponse = list_generic(tech_type=selected_name, - output_format=output_form, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password, - params=my_additional_parameters, - write_file=True - ) - logger.debug(f"Return from exec_tech_type:") - logger.debug(f"reponse: {reponse}") - except (ValidationError) as e: - logger.debug(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - logger.debug(f"Exception: {e}") - reponse = {"error": f"Exception: {e}"} - await self.display_response(reponse) - -def start_exp2(): - app = MyApp() - app.run() - -if __name__ == "__main__": - start_exp2() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/my_profile_app-orig.py b/my_egeria/my_egeria/DemoCode/Deprecated/my_profile_app-orig.py deleted file mode 100644 index 5af0d3c6..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/my_profile_app-orig.py +++ /dev/null @@ -1,1924 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" - -import datetime -import re -import json -import sys -from pathlib import Path - -# Add the project root to sys.path to allow running this script from any directory -root_path = Path(__file__).resolve().parents[4] -if str(root_path) not in sys.path: - sys.path.append(str(root_path)) - -from pyegeria import load_app_config, settings, MyProfile, PyegeriaException, print_basic_exception, exec_report_spec, \ - AutomatedCuration, ProductManager, \ - PyegeriaInvalidParameterException, PyegeriaAPIException, DataEngineer -from textual import on -from textual.app import App, ComposeResult -from textual.containers import ScrollableContainer -from textual.widgets import DataTable, OptionList, Header, Static, Footer, Tree - -from CreateProfileScreen import CreateProfileScreen -from my_egeria.DemoCode.Deprecated.EditProfileScreen import EditProfileScreen -from my_egeria.DemoCode.Deprecated.EditCommunitiesScreen import EditCommunitiesScreen -from my_egeria.DemoCode.Deprecated.EditIdentitiesScreen import EditIdentitiesScreen -from my_egeria.DemoCode.Deprecated.EditRolesScreen import EditRolesScreen -from my_egeria.DemoCode.Deprecated.EditTeamsScreen import EditTeamsScreen -from my_egeria.DemoCode.Deprecated.TechnologyTypesScreen import TechnologyTypesScreen -from my_egeria.DemoCode.Deprecated.TechnologyTypeOptionsScreen import TechnologyTypeOptionsScreen -from my_egeria.DemoCode.Deprecated.TechnologyTypeTemplatesScreen import TechnologyTypeTemplatesScreen -from my_egeria.DemoCode.Deprecated.TechnologyTypeProcessesScreen import TechnologyTypeProcessesScreen -from StatusScreen import StatusScreen -from ShopForDataScreen import ShopForDataScreen -from SelectionOverviewScreen import SelectionOverviewScreen -from MyTeamScreen import MyTeam -from MainScreen import MainScreen -from SearchForTermScreen import SearchForTermScreen -from CreateSubscriptionRequestScreen import CreateSubscriptionRequestScreen -from UserIdentitiesScreen import UserIdentitiesScreen - - -class MyProfileApp(App): - """My Profile App. - - Retrieves a user's profile from Egeria and displays current work items. - If no profile is found, offers a UI to create one. - """ - - BINDINGS = [("q", "quit", "Quit"), - ("r", "refresh", "Refresh Data"), - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - SCREENS = { - "main": MainScreen, - "_default": MainScreen, - "create_profile": CreateProfileScreen, - "edit_profile": EditProfileScreen, - "edit_communities": EditCommunitiesScreen, - "edit_identities": EditIdentitiesScreen, - "edit_roles": EditRolesScreen, - "edit_teams": EditTeamsScreen, - "tech_types": TechnologyTypesScreen, - "tech_type_options": TechnologyTypeOptionsScreen, - "tech_type_templates": TechnologyTypeTemplatesScreen, - "tech_type_processes": TechnologyTypeProcessesScreen, - "status": StatusScreen, - "shop_4_data": ShopForDataScreen, - "search_for_term": SearchForTermScreen, - "overview": SelectionOverviewScreen, - "create_subscription": CreateSubscriptionRequestScreen, - "my_team": MyTeam - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.contribution_record = None - self.heading = "My_Profile" - self.subheading = "Egeria Profile for current user" - self.description = "Display the user related items for the current user." - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - print("Platform:", app_config.egeria_platform_url) - print("View Server:", app_config.egeria_view_server) - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - - # Ensure compose() is safe before data loads - self.actor_profile: dict = {} - self.projects = [] - self.communities = [] - self.roles = [] - self.actions = [] - self.teams = [] - self.other_function_list = [] - self.tech_type_json: str - self.tech_type_response = None - self.tech_type_list = [] - self.tech_type_guid = "" - self.tech_type_name = "" - self.tech_type_description = "" - self.selected_t_node = None - self.selected_t_node_label = None - self.karma_points = 0 - self.tech_type_templates = [{}] - self.tech_type_processes = [{}] - self.full_template = None - self.glossary_data_extract = None - self.business_glossary_data_extract = None - self.display_glossary_data_extract = None - self.digital_glossary_data_extract = None - self.team_members: list[list] = [] - self.max_mermaid_node_count = 0 # This is to tell egeria we dont want mermaid graphs in the response packet. - self.graph_query_depth = 0 # This tell egeria not to include relationships in the response packet - self.user_GUID = "" - self.user_data = {} - - def compose(self) -> ComposeResult: - yield Header() - yield Footer() - - async def on_mount(self) -> None: - """Load profile; if missing, prompt to create it; then populate tables.""" - # DOMInfo.attach_to(self) - - await self._load_or_create_profile() - await self._populate_tables() - - - async def _load_or_create_profile(self) -> None: - try: - #instantiate the class - self.my_profile_inst = MyProfile(self.view_server, self.platform_url, self.user_name, self.user_password) - self.my_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) - #retrieve the profile - self.my_profile_data = await self.my_profile_inst._async_get_my_profile( - report_spec="My-User-MD", - output_format="DICT", - ) - self.log(f"retrieve profile result: {self.my_profile_data}") - except (PyegeriaException) as e: - self.log(f"Error retrieving profile: {e!s}") - print_basic_exception(e) - self.exit(402) - return - - if self.my_profile_data == []: - self.log(f"Error retrieving profile. Prompting to create one...") - self.log(f"To create a profile you must have a valid userid in the system, please contact your system administrator to create one if needed") - await self.push_screen(CreateProfileScreen( - self.user_name, - self.user_password, - self.view_server, - self.platform_url, - ), callback = self.new_profile_return) - else: - self.new_profile_return(200) - - def new_profile_return(self, result: int) -> None: - """ This function handles either the return from the create new profile screen or - when the user already has a profile continue processing """ - self.log(f"Profile creation result: {result}") - if not result or result != 200: - self.log(f"Profile creation cancelled/failed; return: {result}, exiting.") - self.exit(403) - return - - self.result = result - - # Retry after creation if necessary - try: - self.user_profile_struct = self.my_profile_inst.get_my_profile( - output_format="DICT", - report_spec="My-User-MD", - ) - self.log(f"Profile retrieved successfully: {self.user_profile_struct}") - self.push_screen("main") - except PyegeriaException as e2: - self.log(f"Error retrieving user profile: {e2!s}") - self.exit(412) - return - if self.user_profile_struct is []: - self.log(f"Error retrieving user profile. Exiting.") - self.exit(413) - return - - # strip out the individual profile elements - self.user_profile = self.user_profile_struct[0] - self.contribution_record = self.user_profile.get("Contribution Record") or {} - for c in self.contribution_record: - self.karma_points = c.get("Karma Points") or 0 - break - self.my_projects_data = self.user_profile.get("Projects") or [] - self.my_teams_data = self.user_profile.get("Teams") or [] - self.my_communities_data = self.user_profile.get("Communities") or [] - self.my_roles_data = self.user_profile.get("Roles") or [] - self.my_actions_data = self.user_profile.get("Actions") or [] - self.log(f"Contribution Record: {self.contribution_record}") - self.log(f"Karma Points: {self.karma_points}") - self.log(f"my_projects_data: {self.my_projects_data}") - self.log(f"my_teams_data: {self.my_teams_data}") - self.log(f"my_communities_data: {self.my_communities_data}") - self.log(f"my_roles_data: {self.my_roles_data}") - self.log(f"my_actions_data: {self.my_actions_data}") - # User Identities - try: - self.user_identities = self.my_profile_inst.get_my_profile( - report_spec="User-Identities", - output_format="DICT", - ) - self.log(f"User-Identities: {self.user_identities}, type: {type(self.user_identities)}") - except PyegeriaException as e: - self.log(f"Error retrieving User-Identities: {e!s}") - self.user_identities = {} - - # User GUID - - try: - self.user_data = exec_report_spec(format_set_name="Actor-Profiles", - output_format="DICT", - params = {"search_string" : self.user_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving actor profile: {e!s}") - self.exit(420) - self.log(f"Actor Profile retrieved: {self.user_data}") - self.user_data = self.user_data.get("data") - self.log(f"Extracted data : {self.user_data}") - self.user_GUID = self.user_data[0].get("GUID") - self.log(f"User GUID retrieved: {self.user_GUID}") - - # Normalize expected keys - self.full_name = self.user_profile.get("Full Name") or "" - self.sub_title = f"{self.full_name} ({self.user_profile.get('User ID')}, Karma Points: {self.karma_points})" - self.projects = self.my_projects_data or [] - self.communities = self.my_communities_data or [] - self.roles = self.my_roles_data or [] - self.actions = self.user_profile.get("actions") or [] - self.teams = self.my_teams_data or [] - if isinstance(self.user_identities, list): - self.user_identity = self.user_identities - else: - self.user_identity = self.user_identities.get("User-Identities") or [] - - async def _populate_tables(self) -> None: - """Populates tables from normalized profile data""" - main_screen = self.get_screen("main") - self.projects_table = main_screen.query_one("#projects_table", DataTable) - self.communities_table = main_screen.query_one("#communities_table", DataTable) - self.roles_table = main_screen.query_one("#roles_table", DataTable) - self.actions_table = main_screen.query_one("#actions_table", DataTable) - self.user_identity_table = main_screen.query_one("#user_identity_table", DataTable) - self.teams_table = main_screen.query_one("#teams_table", DataTable) - - assert self.projects_table is not None - assert self.communities_table is not None - assert self.roles_table is not None - assert self.actions_table is not None - assert self.user_identity_table is not None - assert self.teams_table is not None - - self.projects_table.clear(columns=True) - self.projects_table.add_columns("Project Name", "Description", "Qualified Name") - - self.communities_table.clear(columns=True) - self.communities_table.add_columns("Assignment Type", "Community Name", "Description", "GUID") - - self.roles_table.clear(columns=True) - self.roles_table.add_columns("Role Name", "Role Type","Description", "GUID") - self.roles_table.zebra = True - self.roles_table.cursor_type = "row" - - self.teams_table.clear(columns=True) - self.teams_table.add_columns("Assignment Type", "Team Name", "Description","GUID") - - self.actions_table.clear(columns=True) - self.actions_table.add_columns("Action Name", "Status", "Description") - - self.user_identity_table.clear(columns=True) - self.user_identity_table.add_columns("Display Name", "User ID", "Distinguished Name") - - # Populate rows - for p in self.projects if isinstance(self.projects, list) else []: - self.projects_table.add_row( - str(p.get("Name", "")), - str(p.get("Description", "")), - str(p.get("Qualified Name", "")), - ) - - for c in self.communities if isinstance(self.communities, list) else []: - self.communities_table.add_row( - str(c.get("Assignment Type", "")), - str(c.get("Name", "")), - str(c.get("Description", "")), - str(c.get("GUID", "")) - ) - - for r in self.roles if isinstance(self.roles, list) else []: - self.roles_table.add_row( - str(r.get("Name", "")), - str(r.get("Type", "")), - str(r.get("Description", "")), - str(r.get("GUID", "")), - ) - - for a in self.actions if isinstance(self.actions, list) else []: - self.actions_table.add_row( - str(a.get("Display Name", "")), - str(a.get("Description", "")), - ) - - for t in self.teams if isinstance(self.teams, list) else []: - self.teams_table.add_row( - str(t.get("Assignment Type", "")), - str(t.get("Name", "")), - str(t.get("Description", "")), - str(t.get("GUID", "")), - ) - - def action_quit(self) -> None: - # quit selected by user, so exit app - self.exit(200) - - async def action_refresh(self) -> None: - # refresh requested by User - self.log("Refreshing data...") - await self._load_or_create_profile() - await self._populate_tables() - self.log("Data refresh completed.") - - @on(OptionList.OptionSelected, "#other_function_list") - async def handle_option_selected(self, event: OptionList.OptionSelected): - # option selected by user - selected_option = event.option.prompt - selected_option_id = event.option.id - self.log(f"Selected option: {selected_option} ({selected_option_id})") - if selected_option == "Technology Types": - self.log("Fetching technology types...") - await self.fetch_technology_types() - self.log(f"Tech Type Response: {self.tech_type_response} | {self.tech_type_list}") - if self.tech_type_response == []: - self.log("No technology types found.") - self.exit(200) - elif len(self.tech_type_response) == 3 and int(self.tech_type_response) >= 400: - self.log("Error fetching technology types.") - self.exit(int(self.tech_type_response)) - self.log("Technology types fetched successfully.") - self.log("Displaying technology types...") - await self.push_screen(TechnologyTypesScreen(self.tech_type_list, - self.user_name, - self.user_password, - self.karma_points), - callback=self.tech_type_callback) - self.log("Technology types displayed successfully.") - elif selected_option == "User Identities": - await self.push_screen(UserIdentitiesScreen(self.user_name, - self.user_password, - self.karma_points, - self.user_identities), - callback=self.user_identities_callback) - elif selected_option == "Catalogs/Shop for Data": - """ Push new Screen, Show Glossaries, Digital Product Catalogs, Data Dictionaries and - Business Domains, allow the user to select from one of the 4 categories and use that selection to - display a list of available collections of the chosen type and allow the user to subscribe to them - """ - - # start by gathering the data using Pyegeria to access the Egeria backend servers - - # Glossaries - glossary_table: DataTable = DataTable(id="glossary_table") - glossary_table.add_columns("Glossary Name", "Description", "Qualified Name") - glossary_table.cursor_type = "row" - glossary_table.zebra_stripes = True - try: - self.glossary_data = exec_report_spec(format_set_name="Glossaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving glossary details: {e!s}") - self.exit(420) - return(420) - self.log(f"Glossary data returned: {self.glossary_data}") - self.glossary_data_extract = self.glossary_data.get("data") or [] - self.log(f"Glossary data extracted: {self.glossary_data_extract}") - if self.glossary_data_extract == []: - self.log(f"No glossary data found for search string: {self.selected_t_node}") - glossary_table.add_row("No glossaries found", "No data returned from Egeria", "") - else: - for g in self.glossary_data_extract: - glossary_table.add_row(g.get("Display Name"), g.get("Description"), g.get("Qualified Name")) - continue - - # Digital Product Catalogs - digital_product_catalog_table: DataTable = DataTable(id="digital_product_catalog_table") - digital_product_catalog_table.add_columns("Digital Product Catalog Name", "Description", "Qualified Name") - digital_product_catalog_table.cursor_type = "row" - digital_product_catalog_table.zebra_stripes = True - try: - self.digital_product_catalog_data = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params = {"search_string" : "*", - "metadata_element_subtypes": ["DigitalProduct", "DigitalProductFamily"]}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving digital product catalog details: {e!s}") - self.exit(421) - return(421) - self.log(f"Digital Product Catalog data returned: {self.digital_product_catalog_data}") - self.digital_product_catalog_data_extract = self.digital_product_catalog_data.get("data") or [] - self.log(f"Digital Product Catalog data extracted: {self.digital_product_catalog_data_extract}") - if self.digital_product_catalog_data_extract == []: - self.log(f"No digital product catalog data found for user: {self.user_name}") - digital_product_catalog_table.add_row("No digital product catalogs found", "No data returned from Egeria", "") - else: - for catalog_item in self.digital_product_catalog_data_extract: - digital_product_catalog_table.add_row(catalog_item["Display Name"], - catalog_item["Description"], - catalog_item["Qualified Name"]) - continue - - # Data Dictionaries - data_dictionary_table: DataTable = DataTable(id="data_dictionary_table") - data_dictionary_table.add_columns("Data Dictionary Name", "Description", "Qualified Name") - data_dictionary_table.cursor_type = "row" - data_dictionary_table.zebra_stripes = True - try: - self.data_dictionary_data = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving data dictionary details: {e!s}") - self.exit(422) - return(422) - self.data_dictionary_data_extract = self.data_dictionary_data.get("data") or [] - if self.data_dictionary_data_extract == []: - self.log(f"No data dictionary details found for user: {self.user_name}") - data_dictionary_table.add_row("No data dictionaries found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.data_dictionary_data_extract} data dictionaries for user {self.user_name}") - data_dictionary_table.add_row("Display Name", "Description", "Qualified Name") - for dictionary in self.data_dictionary_data_extract: - data_dictionary_table.add_row(dictionary.get("Display Name", ""), - dictionary.get("Description", ""), - dictionary.get("Qualified Name", "")) - continue - - # Business Domains - business_domain_table: DataTable = DataTable(id="business_domain_table") - business_domain_table.add_columns("Business Area Name", "Type Name", "GUID") - business_domain_table.cursor_type = "row" - business_domain_table.zebra_stripes = True - try: - self.business_domain_data = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(423) - return(423) - self.business_domain_data_extract = self.business_domain_data.get("data") or [] - if self.business_domain_data_extract == []: - self.log(f"No business domains found for user {self.user_name}") - business_domain_table.add_row("No business domains found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.business_domain_data_extract} business domains for user {self.user_name}") - # business_domain_table.add_row("Business Domain Name", "Description", "GUID") - for domain in self.business_domain_data_extract: - business_domain_table.add_row(domain.get("Qualified Name", ""), - domain.get("Type Name", ""), - domain.get("GUID", "")) - continue - - # Data Specifications removed from the display, code is working but being replaced by Root Collections table - # data_specification_table: DataTable = DataTable(id="data_specification_table") - # data_specification_table.add_columns("Display Name", "Description", "Qualified Name") - # data_specification_table.cursor_type = "row" - # data_specification_table.zebra_stripes = True - # try: - # self.data_specification_data = exec_report_spec(format_set_name="Data-Specifications", - # output_format="DICT", - # params = {"search_string": "*"}, - # view_server=self.view_server, - # view_url=self.platform_url, - # user=self.user_name, - # user_pass=self.user_password) - # except PyegeriaException as e: - # self.log(f"Error retrieving data specification details: {e!s}") - # self.exit(423) - # return (423) - # - # if isinstance(self.data_specification_data, dict): - # self.data_specification_data_extract = self.data_specification_data.get("data") - # elif isinstance(self.data_specification_data, list): - # self.data_specification_data_extract = self.data_specification_data - # else: - # self.data_specification_data_extract = self.data_specification_data.get("Data-Specifications") or [] - # - # if self.data_specification_data_extract == [] or self.data_specification_data_extract == {} or self.data_specification_data_extract == None: - # self.log(f"No data specifications found for user {self.user_name}") - # data_specification_table.add_row("No data specifications found", "No data returned from Egeria", "") - # else: - # self.log(f"Found {self.data_specification_data_extract} data specifications for user {self.user_name}") - # - # for spec in self.data_specification_data_extract: - # data_specification_table.add_row(spec.get("Display Name", ""), - # spec.get("Description", ""), - # spec.get("Qualified Name", "")) - # continue - - # Root Collections - - self.root_collection_table = DataTable(id="root_collection_table") - self.root_collection_table.add_columns("Root Collection Name", "Description", "GUID") - self.root_collection_table.cursor_type = "row" - self.root_collection_table.zebra_stripes = True - - try: - self.collections = exec_report_spec(format_set_name="BasicCollections", - output_format="DICT", - params = {"search_string": "RootCollection"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - - except PyegeriaException as e: - print_basic_exception(e) - self.collections = "Error retrieving collections: " + str(e) - self.log(f"Found {len(self.collections)} root collections for user {self.user_name}") - self.log(f"Root collections: {self.collections}") - if isinstance(self.collections, str): - self.root_collection_table.add_row("No root collections found", self.collections, "") - elif isinstance(self.collections, dict) and self.collections.get("kind") == "json": - self.collections:list[dict] = self.collections.get("data") - for collection in self.collections: - self.root_collection_table.add_row(collection.get("Qualified Name"), collection.get("Type Name"), collection.get("GUID")) - continue - elif isinstance(self.collections, list): - for collection in self.collections: - self.root_collection_table.add_row(collection.get("Qualified Name"), collection.get("Type Name"), collection.get("GUID")) - continue - else: - self.log(f"Unexpected data type returned from Egeria: {type(self.collections)}") - self.root_collection_table.add_row("Invalid data type", "Unexpected data type returned from Egeria", "") - - # hand the data to the Screen for displaying - await self.push_screen(ShopForDataScreen(glossary_table, - digital_product_catalog_table, - data_dictionary_table, - business_domain_table, - # data_specification_table, - self.root_collection_table, - self.user_name, - self.user_password, - self.view_server, - self.platform_url), - callback = self.shop_for_data_callback) - elif selected_option == "Edit Profile": - await self.push_screen(EditProfileScreen( - self.user_name, - self.user_password, - self.view_server, - self.platform_url, - self.karma_points, - self.user_profile, - self.user_GUID - ), - callback = self.edit_profile_callback) - elif selected_option == "User Bookmarks": - pass - elif selected_option == "Subscriptions": - pass - else: - pass - - async def tech_type_callback(self, result) -> int: - """ Callback for Technology Types screen - If the result is int (4xx) it indicates an error in the screen - if the result is str it contains the GUID of the selected technology type - """ - #clear local data fields - tech_type_description = "" - self.tech_type_data = {} - self.tech_type_data_extracted = {} - self.tech_type_templates = [] - self.tech_type_processes = [] - # check that we got a valid result from the screen and process accordingly - if not result or isinstance(result, int) : - self.log(f"Technology Types screen cancelled or failed; return: {result}.") - await self.push_screen(MainScreen()) - return (result) - self.selected_t_node = str(result) - self.log(f"Technology Types screen returned: {self.selected_t_node}") - # Request details for selected tech type - try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) - self.tech_type_data = p_client.get_tech_type_detail(filter_string=self.selected_t_node, - output_format="JSON", - # body={"maxMermaidNodeCount": self.max_mermaid_node_count, - # "graphQueryDepth": self.graph_query_depth} - ) - except PyegeriaException as e: - print_basic_exception(e) - error_category = "Technology Type Process Detail Retrieval" - error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") - await self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - self.log(f"Technology Type Data: {self.tech_type_data}, type: {type(self.tech_type_data)}") - - if "specificationMermaidGraph" in self.tech_type_data: - del self.tech_type_data["specificationMermaidGraph"] - self.log(f"Technology Type Mermaid Graph Removed") - - self.tech_type_guid = self.tech_type_data.get("technologyTypeGUID") - self.tech_type_name = self.tech_type_data.get("displayName") - self.tech_type_templates = self.tech_type_data.get("catalogTemplates") - self.tech_type_templates = self.clean_structure(self.tech_type_templates) - self.tech_type_templates = self.bools_to_strings(self.tech_type_templates) - self.log(f"Templates: {self.tech_type_templates}, type: {type(self.tech_type_templates)}") - self.tech_type_processes = self.tech_type_data.get("governanceActionProcesses") - self.tech_type_processes = self.clean_structure(self.tech_type_processes) - self.tech_type_processes = self.bools_to_strings(self.tech_type_processes) - self.log(f"Processes: {self.tech_type_processes}, type: {type(self.tech_type_processes)}") - self.tech_type_description = self.tech_type_data.get("description") - - self.log(f"Technology Type GUID: {self.tech_type_guid}") - self.log(f"Technology Type Name: {self.tech_type_name}") - self.log(f"Technology Type Description: {self.tech_type_description}") - self.log(f"Technology Type Templates: {self.tech_type_templates}" or [{"templates": "None"}]) - self.log(f"Technology Type Processes: {self.tech_type_processes}" or [{"processes": "None"}]) - - await self.push_screen(TechnologyTypeOptionsScreen(self.tech_type_guid, - self.tech_type_name, - self.tech_type_description, - self.user_name, - self.user_password, - self.karma_points, - self.tech_type_templates, - self.tech_type_processes), callback = self.tech_type_options_callback) - return(200) - - async def tech_type_options_callback(self, result:list): - self.log(f"Technology Type Options screen returned: {result}") - self.selected_t_option = None - self.selected_t_option_selected = None - - if not result or result == None: - self.log(f"Technology Type Options screen returned no input; return: {result}.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) or isinstance(result, str): - if not result or result == "back": - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) and result == 200: - self.log("Technology Type Options screen returned successfully.") - await self.push_screen(MainScreen()) - elif isinstance(result, int) and result != 200: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - self.exit(415) - else: - self.log(f"Technology Type Options screen returned unexpected result: {result}, type: {type(result)}") - elif isinstance(result, list): - self.log(f"Input is a list: {result}, type: {type(result)}") - self.log(f"Result 0 = {result[0]}, 1 = {result[1]}") - if not result[0] or isinstance(result[0], int): - if isinstance(result[0], int) and result[0] == 200: - self.log("Technology Type Options screen returned successfully.") - await self.push_screen(MainScreen()) - elif isinstance(result, list) and result[0] == "back": - await self.push_screen(MainScreen()) - else: - self.selected_t_option = str(result[0]) - self.selected_t_option_selected = str(result[1]) - self.log(f"Technology Type Options screen returned: {self.selected_t_option} | {self.selected_t_option_selected}") - else: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, type: {type(result)}.") - await self.push_screen(MainScreen()) - - # display the screen so the objects we need to mount to are created in the DOM - # then we can build the display elements for the placeholder properties - # and mount them in the appropriate containers on the screen - - self.log(f"Selected: {self.selected_t_option} | {self.selected_t_option_selected}") - if self.selected_t_option == "template": - await self.push_screen(TechnologyTypeTemplatesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_templates), - callback=self.tech_type_templates_callback) - elif self.selected_t_option == "process": - await self.push_screen(TechnologyTypeProcessesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_processes), - callback=self.tech_type_processes_callback) - else: - self.log(f"Technology Type Options screen returned invalid option: {self.selected_t_option_selected}") - self.exit(416) - return(200) - - async def fetch_technology_types(self) -> int: - self.tech_type_list: list = [{}] - try: - self.autoc = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - # retrieve the tech type data - self.log(f"Fetching technology type hierarchy for tech_type='*'") - self.tech_type_response = await self.autoc._async_get_tech_type_hierarchy(filter_string = "*" ) - except Exception as e: - self.log(f"Exception in get_tech_type_hierarchy: {e}") - self.log(print_basic_exception(e)) - self.tech_type_list = [{}] - self.exit(416) - return(416) - - self.log (f"tech_type_response: {self.tech_type_response}") - # Copy the data into a working variable for the extraction routine - self.tech_type_list = self.tech_type_response - return(200) - - def unpack_egeria_data(self) -> int: - """ Unpack the data returned from Egeria """ - output_data: list[dict] = [{}] - output_data.clear() - if isinstance(self.tech_type_data, dict): - if "data" in self.tech_type_data: - output_data = self.tech_type_data.get("data") - else: - output_data = [self.tech_type_data] - elif isinstance(self.tech_type_data, list): - for entry in self.tech_type_data: - if isinstance(entry, dict): - output_data = entry.get("data") - elif isinstance(entry, list): - for subentry in entry: - if isinstance(subentry, dict): - output_data = [subentry] - else: - self.log(f"error unknown data structure for Tech Type data") - return(417) - else: - self.log(f"error unknown outer data structure for Tech Type data") - return (417) - else: - self.log(f"Tech Type data not dict or list") - return(417) - - self.log(f"output_data: {output_data}, {type(output_data)}") - # return the extracted data (dict) - self.tech_type_list = output_data - return (200) - - def tech_type_templates_callback(self, result): - """ Callback for Technology Type Templates screen - result contains up to 3 elements: - [0] = return code or 'input', [1] input data, [2] full template""" - # take the input data and use it in the form and - # create from template - self.log(f"Technology Type Templates screen returned: {result}") - # Check for return code - if isinstance(result, int): - self.log(f"Technology Type Templates screen returned: {result}, exiting.") - return(result) - # Check for unexpected return - if not result or result[0] != "input": - self.log(f"Technology Type Templates screen cancelled/failed; return: {result}, exiting.") - return(418) - - # make the keys to the input data match the keys in the template structure - # take each key and use the data value to replace the placeholder value in the template - # then use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - # make a working version of the full template if it is returned - if isinstance(result[2], dict): - self.full_template = result[2] - # create an empty dict for the returned data - my_placeholderPropertyValues: dict = {} - # load in the returned placeholder user data - if isinstance(result[1], dict): - self.placeholder_input = result[1] - # clean up the keys of the user data to match the template keys - if isinstance(self.placeholder_input, dict): - # for each data input item in the dict - for input_item, input_value in self.placeholder_input.items(): - self.log(f"input_item: {input_item}, input_value: {input_value}") - input_fix1 = input_item.replace("_", " ") - self.log(f"input_item after underscore removal: {input_fix1}") - input_fix2 = input_fix1.replace(" placeholder input", "") - self.log(f"input_item after placeholder removal: {input_fix2}") - - self.log(f"fixed input_item: {input_fix2}, {input_value}") - # build the placeholderPropertyValues dict - my_placeholderPropertyValues.update({input_fix2: input_value}) - continue - else: - self.log(f"placeholder_input is not a dict: {self.placeholder_input}") - return(419) - - # create a dict structure that matches the template structure - # we have the input data in my_placeholderPropertyValues and - # the selected template in self.full_template - # so we can use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - self.log(f"my_placeholderPropertyValues: {my_placeholderPropertyValues}") - self.log(f"self.placeholder_input: {self.placeholder_input}") - self.log(f"self.full_template: {self.full_template}") - - # copy the retrieved template into the request body - request_body:dict = { - "class" : "TemplateRequestBody", - "externalSourceGUID" : self.full_template.get("externalSourceGUID") or "", - "externalSourceName" : self.full_template.get("externalSourceName") or "", - "typeName" : self.full_template.get("typeName") or "", - "templateGUID" : self.full_template.get("Catalog Template GUID"), - "anchorGUID" : self.full_template.get("anchorGUID"), - "isOwnAnchor" : "false", - "effectiveFrom" : "2026-01-01", - "effectiveTo": "2030-12-31", - "replacementProperties" : self.full_template.get("replacementProperties") or {}, - "placeholderPropertyValues" : {}, - "parentGUID" : None, - "parentRelationshipTypeName" : None, - "parentRelationshipProperties" : None, - "parentAtEnd1" : self.full_template.get("parentAtEnd1") or True, - "effectiveTime" : self.full_template.get("effectiveTime") or datetime.datetime.now().isoformat(), - } - self.log(f"request_body: {request_body}") - # update the request body with the user provided input - for key, value in my_placeholderPropertyValues.items(): - request_body["placeholderPropertyValues"].update({key: value}) - continue - self.log(f"request_body after update: {request_body}") - # now instantiate and call the function to create the element from the template - try: - tokendata = self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - my_md_instance = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password, tokendata) - # new_guid = my_md_instance.create_metadata_element_from_template(self.template_guid, body = request_body) - new_guid = my_md_instance.initiate_gov_action_process(body=request_body) - except Exception as e: - self.log(f"Exception in create_element_from_template: {e}") - if isinstance(e, PyegeriaException): - self.log(print_basic_exception(e)) - else: - self.log(f"Exception in create_element_from_template: {e}") - self.push_screen(StatusScreen(f"Error creating element from Template/n{print_basic_exception(e)}"), callback=self.status_callback) - return(420) - - self.log(f"new_guid: {new_guid}") - self.push_screen(StatusScreen(f"Element created from Template/Metadata element created with GUID: '{new_guid}'"), callback=self.status_callback) - - def status_callback(self, status_callback_rc) -> None: - """ Callback routine from the status screen, - this screen is typically displayed at the end of a process - to indicate success or failure of the process.""" - - self.log(f"Status screen returned: {status_callback_rc}") - self.exit(status_callback_rc) - - def tech_type_processes_callback(self, result): - """ Callback for Technology Type Processes screen""" - # take the input data and use it to get the template for running the process, display it and allow - #the user to complete the data in the placeholders in order to run the process - self.type = None - self.full_process = None - self.result = result - if isinstance(result, int): - self.log(f"Technology Type Processes Screen returned: {result}, exiting.") - return(result) - if isinstance(self.result, list): - self.type = result[0] - self.input_data = result[1] - self.full_process = result[2] - self.log(f"Received process type: {self.type}, data: {self.input_data}, full process: {self.full_process}") - else: - self.log(f"Unexpected result type from TechnologyTypeProcessesScreen: {type(result)}, exiting.") - return(418) - if self.type != "input": - self.log(f"Process type is not 'input', {self.type}, display error screen") - error_category = "Governance Action Process" - error_message = "Unknown response from TechnologyTypeProcessesScreen: "+self.type - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - my_process_property_values: list[dict] = [{}] - if not isinstance(self.result, dict): - self.log("No process Request Parameter input data returned, this is possibly valid, but unlikely") - self.log(f" Input data type: {type(self.result)}, data: {self.result}") - self.log("Processing will proceed with no Supported Request Parameters for the process") - self.result = {None:None} - else: - for input_key, input_value in self.result.items(): - self.log(f"Processing input value: {input_key} with value: {input_value}") - input_fix1 = input_key.replace("_", " ") - input_fix2 = input_fix1.replace(" process input", "") - self.log(f"Process input key after cleaning: {input_fix2}") - my_process_property_values.append ( - {"class":"SupportedRequestParameter", - "specificationPropertyType": "SUPPORTED_REQUEST_PARAMETER", - "name": input_fix2, - "value": input_value, - "Description": "", - "dataType": "string", - "example": "", - "required": "False"}) - continue - - self.log(f"Process property values: {my_process_property_values}") - self.log(f"Original input: {self.result}") - self.log(f"full process: {self.full_process}") - - request_body: dict ={ - "displayName": self.result.get("displayName"), - "description": self.result.get("description"), - "additionalProperties": - {"templateGUID": self.result.get("additionalProperties")("templateGUID")}, - "specification": - {"supportedRequestParameter": - my_process_property_values - } - } - - def tech_type_processes_details(self, tech_type, selected_t_option_selected): - """ Retrieve process data for Technology Type Processes screen""" - # ************************************************************************************************** - # take the input data and use it to get the template for running the process, display it and allow - # the user to complete the data in the placeholders in order to run the process - # with the change to retrieve the JSON this specific processing is no longer needed - # ************************************************************************************************** - self.type = None - self.full_process = None - self.selected_q_name = None - required_process = None - self.type = str(tech_type) - self.selected_q_name = str(selected_t_option_selected) - self.log(f"Received process type: {self.type}, selected: {self.selected_q_name}") - - if self.selected_q_name != None: - self.log(f"Selected qualified name: {self.selected_q_name}") - try: - p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) - governance_actions = p_client.get_tech_type_detail(filter_string=self.selected_q_name, output_format="JSON") - except PyegeriaException as e: - print_basic_exception(e) - error_category = "Technology Type Process Detail Retrieval" - error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - self.log(f"Egeria returned: {governance_actions}, type: {type(governance_actions)}") - # load Json data - if isinstance(governance_actions, str): - if governance_actions != "No elements found": - technology_response = json.loads(governance_actions) - else: - technology_response = None - else: - technology_response = governance_actions - - self.log(f"Tech type process data retrieved: {technology_response}") - self.governance_action_processes = technology_response.get("governanceActionProcesses", []) - self.log(f"Retrieved {len(self.governance_action_processes)} governance action processes for {self.selected_q_name}") - self.log(f"Data: {self.governance_action_processes}") - for process in self.governance_action_processes: - self.log(f"Process: {process['name']}, Description: {process['description']}") - if process['name'] == self.selected_q_name: - self.log(f"Found requested process: {process['description']}") - # temporary end of function, more code needed here. - required_process = process - break - else: - continue - - return(required_process) - else: - self.log(f"selected_q_name in process_details function is None, returnng None to caller: {self.selected_q_name}") - return(None) - - async def shop_for_data_callback(self, result): - """ Callback for Shop For Data screen""" - if isinstance(result[0], int): - selection_type = int(result) - selection_parm_1 = None - selection_parm_2 = None - selection_parm_3 = None - self.log(f"Shop For Data screen returned: {selection_type}, type: {type(selection_type)}.") - else: - self.log(f"Shop For Data screen returned: {result}, type: {type(result)}.") - selection_type = result[0] - selection_parm_1 = result[1] - selection_parm_2 = result[2] - # selection_parm_3 = result[3] - self.log(f"Shop For Data screen returned: {selection_type}, with parameters: {selection_parm_1}, {selection_parm_2}") - - if isinstance(selection_type, int): - if selection_type == 200: - self.log(f"Shop For Data screen returned successfully.") - return(200) - elif selection_type == 201: - self.log(f"Shop For Data screen returned: {selection_type}, request to search for a term ") - await self.push_screen(SearchForTermScreen(self.user_name, - self.user_password, - self.view_server, - self.platform_url), callback=self.search_for_term_callback) - return(200) - elif selection_type == 211: - self.log(f"Shop For Data screen returned: {selection_type}, request to subscribe to data source ") - return(211) - elif selection_type == 212: - self.log(f"Shop For Data screen returned: {selection_type}, request to sample data source ") - self.request_to_sample_data_source(selection_parm_1, selection_parm_2, selection_parm_3) - else: - self.log(f"Shop For Data screen returned: {selection_type}.") - return(selection_type) - - if selection_type == "dictionary": - # data dictionary - self.log(f"Selected dictionary with qualified name: {selection_parm_1}") - self.build_dictionary_details(selection_parm_1, selection_parm_2) - elif selection_type == "domain": - # business domain - self.log(f"Selected business domain with qualified name: {selection_parm_1}") - self.build_domain_details(selection_parm_1, selection_parm_2) - elif selection_type == "catalog": - # data product catalog - self.log(f"Selected catalog with qualified name: {selection_parm_1}") - self.build_catalog_details(selection_parm_1, selection_parm_2) - elif selection_type == "glossary": - # glossary - self.log(f"Selected glossary with qualified name: {selection_parm_2}") - self.build_glossary_details(selection_parm_1, selection_parm_2) - elif selection_type == "collection": - # root collection - self.log(f"Selected Root Collection with qualified name: {selection_parm_1}") - self.build_root_collection_details(selection_parm_1, selection_parm_2) - else: - self.log(f"Unknown selection type: {selection_type}") - self.exit(429) - - def build_dictionary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a dictionary details screen""" - self.log(f"Building dictionary details for qualified name: {target_qualified_name}") - self.dictionary_qualified_name = target_qualified_name - self.dictionary_display_name = target_display_name - build_structure = {} - - try: - self.dictionary_details = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params={"search_string": self.dictionary_qualified_name, "filter_string": self.dictionary_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving dictionary details: {e!s}") - self.exit(420) - return (420) - self.log(f"Dictionary Details: {self.dictionary_details}") - if not self.dictionary_details or self.dictionary_details == None: - error_category = "Dictionary Details" - error_message = "No dictionary details found" - self.log(f"Error retrieving dictionary details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.dictionary_details.get("kind") == "empty": - dictionary_tree: Tree = Tree(label="Empty Dictionary", id="data_dictionary_tree") - dictionary_tree.root.expand() - dictionary_tree.root.content = "No dictionary terms found for this dictionary" - else: - dictionary_tree: Tree = Tree(label=self.dictionary_display_name, id="data_dictionary_tree") - dictionary_tree.root.expand() - dictionary_tree.auto_expand = True - self.dictionary_details_data = self.dictionary_details.get("data") - for term in self.dictionary_details_data: - self.log(f"Dictionary term: {term} being processed") - term_qualified_name = term.get("Qualified Name") or "" - term_subject = term.get("Subject Area") or "" - term_summary = term.get("Summary") or "" - # create dict structure for loading the tree - if term_subject not in build_structure: - build_structure[term_subject] = [] - build_structure[term_subject].append({term_qualified_name: term_summary}) - continue - # Once the structure is complete we can build the tree from it - for term_subject, terms in build_structure.items(): - self.log(f"Building tree for subject: {term_subject}, term: {terms}") - dictionary_branch = dictionary_tree.root.add(term_subject) - dictionary_branch.expand() - for term_dict in terms: - self.log(f"Adding term to tree: {term_dict}") - for term_qualified_name, term_summary in term_dict.items(): - dictionary_branch.add_leaf(term_summary, data=term_qualified_name) - dictionary_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("dictionary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=dictionary_tree), callback=self.overview_callback) - - def build_domain_details(self, target_qualified_name, target_type__name): - """ Build the details object for a business domain details screen""" - self.log(f"Building domain details for qualified name: {target_qualified_name}") - self.domain_qualified_name = target_qualified_name - self.domain_type__name = target_type__name - build_structure = {} - - try: - self.domain_details = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params={"search_string": self.domain_qualified_name, "filter_string": self.domain_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(420) - return (420) - self.log(f"domain_details: {self.domain_details}") - if not self.domain_details or self.domain_details == None: - error_category = "Business Domain Details" - error_message = "No domain details found" - self.log(f"Error retrieving business domain details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.domain_details.get("kind") == "empty": - domain_tree: Tree = Tree(label="Empty Business Domain", id="business_domain_tree") - domain_tree.root.expand() - domain_tree.root.content = "No domain details found for this business domain" - else: - self.domain_details_data = self.domain_details.get("data") - self.log(f"domain_details_data: {self.domain_details_data}") - self.domain_display_name = self.domain_details_data.get("Qualified Name") - domain_tree: Tree = Tree(label=self.domain_display_name, id="business_domain_tree") - domain_tree.root.expand() - domain_tree.auto_expand = True - for term in self.domain_details_data: - if term == None: - continue - term_qualified_name = term.get("Qualified Name") or "" - term_type = term.get("Type Name") or "" - term_GUID = term.get("GUID") or "" - term_members = term.get("Containing Members") - term_memberof = term.get("Member Of") - # create dict structure for loading the tree - build_structure[term_qualified_name] = { - "term_type": term_type, - "term_GUID": term_GUID, - "term_members": term_members, - "term_memberof": term_memberof - } - continue - # Once the structure is complete we can build the tree from it - for qualified_name, details in build_structure.items(): - domain_branch = domain_tree.root.add(qualified_name, id=qualified_name, data=[details["term_type"], details["term_GUID"]]) - if details["term_members"] != None: - domain_branch_members = domain_branch.add("Containing Members") - for member in details["term_members"]: - domain_branch_members.add_leaf(member) - if details["term_memberof"] != None: - for member in details["term_memberof"]: - domain_branch.add_leaf(member) - domain_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("domain", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=domain_tree), callback=self.overview_callback) - - def build_catalog_details(self, target_qualified_name, target_display_name): - """ Build the details object for a product catalog details screen""" - self.log(f"Building product catalog details for qualified name: {target_qualified_name}") - self.catalog_qualified_name = target_qualified_name - self.catalog_display_name = target_display_name - build_structure = {} - - try: - self.catalog_details = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params={"search_string": self.catalog_qualified_name, "filter_string": self.catalog_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving catalog details: {e!s}") - self.exit(420) - return (420) - self.log(f"catalog_details: {self.catalog_details}") - if not self.catalog_details or self.catalog_details == None: - error_category = "Catalog Details" - error_message = "No catalog details found" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.catalog_details.get("kind") == "empty": - catalog_tree: Tree = Tree(label="Empty Catalog", id="digital_product_catalog_tree") - catalog_tree.root.expand() - catalog_tree.root.content = "No catalog terms found for this catalog" - else: - catalog_tree: Tree = Tree(label=self.catalog_display_name, id="digital_product_catalog_tree") - catalog_tree.root.expand() - catalog_tree.auto_expand = True - self.catalog_details_data: list[dict] = self.catalog_details.get("data") - self.log(f"catalog_details_data: {self.catalog_details_data}") - if not self.catalog_details_data or self.catalog_details_data == None: - error_category = "Catalog Details" - error_message = "No catalog details found or the data dict entry is missing" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - for product in self.catalog_details_data: - self.log(f"product: {product}") - term_qualified_name = product.get("Qualified Name") or "" - term_subject = product.get("Display Name") or "" - term_summary = product.get("Description") or "" - # create dict structure for loading the tree - if term_subject not in build_structure: - build_structure[term_subject] = [] - build_structure[term_subject].append({term_qualified_name: term_summary}) - self.log(f"build_structure: {build_structure}") - continue - # Once the structure is complete we can build the tree from it - sample_data: list = [] - for instance, data_prods in build_structure.items(): - catalog_branch = catalog_tree.root.add(instance) - for data_prod in data_prods: - for term_qualified_name, term_summary in data_prod.items(): - catalog_branch.add_leaf(term_summary, data=term_qualified_name) - self.log(f"term_qualified_name: {term_qualified_name}, term summary: {term_summary}") - catalog_tree.root.expand() - - # get some sample data from the data source for each product - for product in self.catalog_details_data: - collection_memberships = product.get("Member Of") - if collection_memberships: - collection_membership_list = collection_memberships.split(",") - self.log(f"Membership List for product {product.get('Display Name')}: {collection_membership_list}") - for membership_qname in collection_membership_list: - self.log(f"Processing collection membership: {membership_qname}") - if "DigitalProduct" in membership_qname: - self.log(f"Processing dataset: {membership_qname}") - try: - # create client instance and retrieve data - dclient = DataEngineer(self.view_server, self.platform_url, self.user_name, - self.user_password) - token = dclient.create_egeria_bearer_token(self.user_name, self.user_password) - data_set_metadata = dclient.find_tabular_data_sets( - search_string=membership_qname, - start_from=0, - page_size=1, - output_format="DICT" - ) - self.log(f"Dataset metadata retrieved: {data_set_metadata}") - if isinstance(data_set_metadata, list) and len(data_set_metadata) > 0: - data_set_guid = data_set_metadata[0].get("GUID") - if data_set_guid: - data_set_data = dclient.get_tabular_data_set( - tabular_data_set_guid=data_set_guid, - start_from_row=0, - max_row_count=10, - output_format="MD" - ) - self.log(f"Dataset data retrieved: {data_set_data}") - sample_data.append(data_set_data) - else: - sample_data.append(f"No GUID found for dataset {membership_qname}") - else: - sample_data.append(f"No metadata found for dataset {membership_qname}") - continue - except PyegeriaException as e: - self.log(f"Error retrieving dataset data: {e}") - print_basic_exception(e) - else: - continue - self.log(f"Sample data after product {product.get('Display Name')}: {sample_data}") - self.log(f"Final sample data length: {len(sample_data)}") - - self.push_screen(SelectionOverviewScreen("catalog", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=catalog_tree, - data_samples=sample_data), callback=self.overview_callback) - - def build_glossary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a glossary details screen""" - self.log(f"Building glossary details for qualified name: {target_qualified_name}") - self.glossary_qualified_name = target_qualified_name - self.glossary_display_name = target_display_name - - for glossary_instance in self.glossary_data_extract: - if glossary_instance.get("Qualified Name") == target_qualified_name: - self.glossary_folders = glossary_instance.get("Folders") or None - self.log(f"glossary_folders: {self.glossary_folders}") - category = "Uncategorised" - if self.glossary_folders != None: - glossary_tree: Tree = Tree(label=self.glossary_display_name, id="glossary_details_tree") - glossary_tree.root.expand() - glossary_tree.auto_expand = True - - folder_entries = [f.strip() for f in self.glossary_folders.split(',')] - nodes = {(): glossary_tree.root} - prefixes = ["GlossaryCategory", "GlossaryTerm", "CollectionFolder"] - - for entry in folder_entries: - parts = entry.split('::') - if len(parts) == 1: - parts = entry.split('/') - - is_leaf = False - path_parts = [] - full_id = "" - if any(parts[0].startswith(p) for p in prefixes): - type_prefix = parts[0] - path_parts = parts[1:] - is_leaf = "Term" in type_prefix - if is_leaf: - full_id = path_parts[-1] - else: - path_parts = parts - - current_path = () - for i, part in enumerate(path_parts): - parent_path = current_path - current_path = current_path + (part,) - - if current_path not in nodes: - parent_node = nodes[parent_path] - if is_leaf and i == len(path_parts) - 1: - nodes[current_path] = parent_node.add_leaf(part, data=full_id) - else: - new_node = parent_node.add(part, data=part) - new_node.expand() - nodes[current_path] = new_node - - glossary_tree.refresh() - else: - self.log(f"No glossary folders found in the glossary data extract") - folder_category = "Empty Glossary" - category = glossary_tree.root.add(folder_category) - folder_term = "No glossary terms found for this glossary" - category.add_leaf(folder_term) - - self.push_screen(SelectionOverviewScreen("glossary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=glossary_tree), callback=self.overview_callback) - - def build_root_collection_details(self, target_qualified_name, target_display_name): - """ Build the details object for a root collection details screen""" - member_tree: Tree = Tree(label="Root Collection", id="root_collection_members_tree") - member_tree.root.expand() - member_tree.auto_expand = True - - self.log(f"Building root collection details for qualified name: {target_qualified_name}") - self.root_collection_qualified_name = target_qualified_name - collection_branch = member_tree.root.add(self.root_collection_qualified_name, expand=True) - self.log(f"self_collections: {self.collections}") - collection = self.collections[0] - self.log(f"collection: {collection}") - if target_qualified_name == collection.get("Qualified Name"): - root_collection_contains: str = collection.get("Containing Members") or None - self.log(f"root_collection_contains: {root_collection_contains}") - folders = str.split(root_collection_contains, ', ') - for folder in folders: - collection_branch.add_leaf(folder) - continue - - self.push_screen(SelectionOverviewScreen("collection", - self.view_server, - self.platform_url, - self.user_name, - self.user_password, - data_tree=member_tree), callback=self.overview_callback) - - - - def overview_callback(self, r_code): - """Callback function for handling overview screen actions.""" - if isinstance(r_code, list): - self.selected_item = r_code[1] - self.selected_tree = r_code[2] - r_code: int = r_code[0] - if r_code == 410: - error_category = "Collection Category" - error_message = "Unknown collection category returned" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 411: - error_category = "Glossary" - error_message = "query_one no matches found for glossary tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 412: - error_category = "Digital Product Catalog" - error_message = "query_one no matches found for digital product catalog tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 413: - error_category = "Data Dictionary" - error_message = "query_one no matches found for data dictionary tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif r_code == 414: - error_category = "Business Domain" - error_message = "query_one no matches fround for business domain tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - elif r_code == 415: - error_category = "Root Collections" - error_message = "query_one no matches found for root collections tree" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - else: - # good return from overview screen - self.log (f"Overview screen callback, return code : {r_code}") - if r_code == 211: - # Subscribe to selected item - self.log(f"Subscribing to selected item: {self.selected_item} from {self.selected_tree}") - try: - s_client = ProductManager(self.view_server, self.platform_url, self.user_name, self.user_password) - s_client.create_egeria_bearer_token(self.user_name, self.user_password) - s_client.create_digital_subscription(self.selected_item) - except (PyegeriaInvalidParameterException, PyegeriaAPIException, PyegeriaException): - self.log(f"Error creating digital subscription: {self.selected_item} from {self.selected_tree}") - # more error handling required here - """ Request Body layout, not all fields are required to be completed - { - "class" : "NewAgreementRequestBody", - "isOwnAnchor" : true, - "anchorScopeGUID" : "", - "parentGUID" : "", - "parentRelationshipTypeName" : "CollectionMembership", - "parentAtEnd1": true, - "properties": { - "class" : "DigitalSubscriptionProperties", - "qualifiedName": "DigitalSubscription::", - "displayName" : "display name", - "description" : "Add description of the subscription here", - "userDefinedStatus" : "DRAFT", - "identifier" : "Add subscription identifier here", - "supportLevel" : "Add the level of support agreed/requested", - "serviceLevels" : { - "property1Name" : "property1Value", - "property2Name" : "property2Value" - }, - "additionalProperties": { - "property1Name" : "property1Value", - "property2Name" : "property2Value" - } - }, - "initialStatus" : "OTHER", - "externalSourceGUID": "add guid here", - "externalSourceName": "add qualified name here", - "effectiveTime" : "{{$isoTimestamp}}", - "forLineage" : false, - "forDuplicateProcessing" : false - }""" - self.push_screen(CreateSubscriptionRequestScreen(), callback=self.create_subscription_callback) - else: - self.push_screen(ShopForDataScreen(self.view_server, self.platform_url, self.user_name, self.user_password, self.selected_item, self.selected_tree), callback=self.shop_for_data_callback) - - def create_subscription_callback(self, result): - """ Callback routine for create subscription request screen - This routine checks for valid input and then processes the subscription request """ - if result is None: - self.log("User cancelled subscription creation") - return - self.log(f"Subscription created: {result}") - - def extract_glossary_terms(self, text): - """ - Extracts GlossaryTerm items from a string structure. - Pattern: Starts with 'GlossaryTerm::', ends at the next ','. - """ - # Pattern explanation: - # GlossaryTerm:: - Matches the literal prefix - # ([^,\']+) - Captures one or more characters that are NOT a comma or a single quote - pattern = r"GlossaryTerm::([^,\']+)" - - # Find all matches in the text - matches = re.findall(pattern, text) - - self.log(f"Extracted {len(matches)} GlossaryTerm items from text") - - # Clean up whitespace and return as a list - return [match.strip() for match in matches] - - def display_glossary_term_details(self, term): - """ Displays the details of a GlossaryTerm item""" - self.target_term = term - try: - self.term_details = exec_report_spec(format_set_name="Glossary-Terms", - output_format="JSON", - params={"search_string": self.target_term, "filter_string": self.target_term}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving term details: {e!s}") - self.exit(440) - return (440) - - self.log(f"term_details: {self.term_details}") - if not self.term_details or self.term_details == None: - error_category = "Glossary Term Details" - error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.term_details.get("kind") == "empty": - self.log(f"No glossary term details found for qualified name: {self.target_term}") - error_category = "Glossary Term Details" - error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - self.term_details_data = self.term_details.get("data") - - if not self.term_details_data or self.term_details_data == None: - error_category = "Glossary Term Details" - error_message = "No glossary term details found or the data dict entry is missing" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - else: - self.term_details_container: ScrollableContainer = self.screen.query_one(DataTable) - self.term_details_container.mount() - for data_item_key, data_item_value in self.term_details_data.items(): - Static(f"Field: {data_item_key}, Value: {data_item_value}").mount(self.term_details_container) - continue - # Once all the fields are complete we can leave it to the screen processing to handle the rest - return (200) - - @on(DataTable.RowSelected, "#roles_table") - def handle_roles_table_row_selection(self, event): - role_table = event.data_table - selected_row_key = event.row_key - selected_row_data = role_table.get_row(selected_row_key) - selected_role_description = selected_row_data[2] - selected_role_name = selected_row_data[0] - selected_role_type = selected_row_data[1] - selected_role_guid = selected_row_data[3] - self.log(f"Selected role: {selected_row_data}") - team_members_list: list = [] - self.team_members = [] - - # provide list of team members for team leader - if "TeamLeader" in selected_role_name or "TeamLeader" in selected_role_type: - self.log(f"Selected role is a TeamLeader: {selected_role_name}") - team_members_list, team_display_name, team_qualified_name, team_category, team_description = self.find_team_members(selected_role_name) - elif "TeamMembers" in selected_role_name or "TeamMembers" in selected_role_type: - self.log(f"Selected role is a TeamMember: {selected_role_name}") - team_members_list, team_display_name, team_qualified_name, team_category, team_description = self.find_team_members(selected_role_name) - else: - self.log(f"Selected role is not a TeamLeader or TeamMember: {selected_role_name}") - return (201) - - self.log(f"team_members_list: {team_members_list}") - # Process team data for display on screen - team_member_properties: list = [] - team_properties: list = [] - team_properties.append(team_display_name) - team_properties.append(team_qualified_name) - team_properties.append(team_category) - team_properties.append(team_description) - self.log(f"team_properties: {team_properties}") - # Process team member data for display on screen - for team_member in team_members_list: - if isinstance(team_member, list) and "Selection Error" in team_member[0]: - self.log(f"Team member details contain 'Selection Error': {team_member}") - self.team_members.append(team_member) - break - else: - self.log(f"Processing team member properties: {team_member}") - team_member_properties = [] - team_member_properties.append(team_member.get("Individual")) - team_member_properties.append(team_member.get("Assignment Type")) - team_member_properties.append(team_member.get("Individual GUID")) - self.log(f"team_member_properties: {team_member_properties}") - self.team_members.append(team_member_properties) - self.log(f"team_members: {self.team_members}") - continue - self.log(f"team_members: {self.team_members}") - self.log(f"team_properties: {team_properties}") - self.log(f"User name: {self.user_name}") - self.push_screen(MyTeam(self.team_members, team_properties, self.user_name), callback=self.my_team_callback) - - def find_team_members(self, role_name): - # common routine for finding team members given a role name - # extract the Department::nnn from the name to use as the search key - selected_role_name = role_name - team_members_list: list = [] - self.team_members = [] - - search_key_parts = selected_role_name.split("::") - role_search_key = '::'.join(search_key_parts[1:]) - self.log(f"Role search key: {role_search_key}") - # get the team member details for that department - team_member_data: dict = exec_report_spec(format_set_name="Team-Members", - output_format="DICT", - params={"search_string": role_search_key}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - self.log(f"team_member_data: {team_member_data}") - # team members? - if team_member_data.get("kind") != "empty": - team_members_data_struct: list[dict] = team_member_data.get("data") - self.log(f"team members data extracted: {team_members_data_struct}") - else: - self.log(f"No team members found for role: {role_search_key}") - error_category = "Team Members" - error_message = "No team members found" - self.log(f"Error retrieving team members: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - team_members_list.clear() - for team_member in team_members_data_struct: - # get team details - team_display_name: str = team_member.get("Display Name", None) - team_qualified_name: str = team_member.get("Qualified Name", None) - team_category: str = team_member.get("Category", None) - team_description: str = team_member.get("Description", None) - # extract the team member entries - team_member_structure: list[dict] = team_member.get("Members", None) - - if team_member_structure != None: - for entry in team_member_structure: - team_members_list.append(entry) - - # if this is for a team leader then: - # get team members profile for contact information and contribution record (karma points) - - # if "TeamLeader" in selected_role_name: - # member_guid = entry.get("Individual GUID", None) - # - - continue - return(team_members_list, team_display_name, team_qualified_name, team_category, team_description) - else: - error_category = "Team Member Details" - error_message = "No team member details found" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return (430) - - def my_team_callback(self, status) -> None: - self.log(f"Callback received with status: {status}") - if status == 200: - self.log("MyTeam screen completed successfully") - else: - self.log(f"Error in MyTeam screen: {status}") - self.push_screen("main") - - def search_for_term_callback(self, status) -> None: - self.log(f"Callback received with status: {status}") - if status == 200: - self.log("Search for term screen completed successfully") - self.push_screen("main") - elif status == 201: - glossary_table = self.query_one("g#glossary_table", DataTable) - digital_product_catalog_table = self.query_one("d#digital_product_catalog_table", DataTable) - data_dictionary_table = self.query_one("d#data_dictionary_table", DataTable) - business_domain_table = self.query_one("b#business_domain_table", DataTable) - data_specification_table = self.query_one("d#data_specification_table", DataTable) - self.log("No matches found for search term") - self.push_screen(ShopForDataScreen(glossary_table, - digital_product_catalog_table, - data_dictionary_table, - business_domain_table, - data_specification_table, - self.user_name, - self.user_password, - self.view_server, - self.platform_url), - callback=self.shop_for_data_callback) - - else: - self.log(f"Error in Search for term screen: {status}") - error_category = "Search for Term" - error_message = "Error retrieving Term details" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - - def request_to_sample_data_source(self, selection_parm_1, selection_parm_2, selection_parm_3): - """ The user has requested to see a sample of the data from the selected data source """ - self.row_highlighted = selection_parm_1 - self.cursor_row_highlighted = selection_parm_2 - self.data_table_highlighted = selection_parm_3 - self.log(f"Data selected: row={selection_parm_1}, cursor_row={selection_parm_2}, data_table={selection_parm_3}") - selected_item = self.query_one("#"+self.data_table_highlighted, DataTable) - item_content = selected_item.get_row(self.row_highlighted) - - - def truncate_at_sequence(self, data, target="specificationMermaidGraph"): - """ - Recursively traverses data. Truncates strings containing the target - and drops all following sibling items within lists or dictionaries. - Returns (truncated_data, should_terminate_parent_container) - """ - # Handle Strings - if isinstance(data, str): - if target in data: - # Drop the sequence and everything after it - truncated_str = data.split(target)[0] - return truncated_str, True - return data, False - - # Handle Dictionaries - elif isinstance(data, dict): - new_dict = {} - for key, value in data.items(): - # First, check if the key itself contains the target - if isinstance(key, str) and target in key: - truncated_key = key.split(target)[0] - if truncated_key: # Keep key fragment if not empty - new_dict[truncated_key] = value - return new_dict, True - - # Recursively check the value - updated_value, terminate = self.truncate_at_sequence(value, target) - new_dict[key] = updated_value - if terminate: - return new_dict, True - return new_dict, False - - # Handle Lists and Tuples - elif isinstance(data, (list, tuple)): - new_list = [] - for item in data: - updated_item, terminate = self.truncate_at_sequence(item, target) - new_list.append(updated_item) - if terminate: - break - # Maintain original type integrity - return (tuple(new_list) if isinstance(data, tuple) else new_list), terminate - - # Handle Booleans, Numbers, None - return data, False - - # Helper wrapper for clean execution - def clean_structure(self, data, target="specificationMermaidGraph"): - result, _ = self.truncate_at_sequence(data, target) - return result - - def bools_to_strings(self, data): - """ - Recursively traverses data structures and converts - all Boolean values into their corresponding string format. - """ - # Check explicitly for bool first (since bool is a subclass of int in Python) - if isinstance(data, bool): - return str(data) - - # Handle Dictionaries - elif isinstance(data, dict): - return {key: self.bools_to_strings(value) for key, value in data.items()} - - # Handle Lists and Tuples - elif isinstance(data, (list, tuple)): - converted = [self.bools_to_strings(item) for item in data] - return tuple(converted) if isinstance(data, tuple) else converted - - # Return all other types unchanged (ints, floats, strings, None) - return data - - def user_identities_callback(self): - """ Callback roiutine for the user identities screen - the user has requested to exit the screen and so will will - push the main screen again""" - self.push_screen("main") - - def edit_identities_callback(self, rows_with_keys): - """ Callback for EditIdentitiesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#user_identity_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_communities_callback(self, rows_with_keys): - """ Callback for EditCommunitiesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#communities_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_roles_callback(self, rows_with_keys): - """ Callback for EditRolesScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#roles_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_teams_callback(self, rows_with_keys): - """ Callback for EditTeamsScreen """ - if isinstance(rows_with_keys, list): - main_screen = self.get_screen("main") - table = main_screen.query_one("#teams_table", DataTable) - table.clear() - for key_str, cell_values in rows_with_keys: - table.add_row(*cell_values, key=key_str) - self.push_screen("main") - - def edit_profile_callback(self, return_c): - """ Callback routine for the edit profile screen """ - if isinstance(return_c, int): - if return_c == 200: - self.push_screen("main") - else: - self.log(f"Error returned from EditProfileScreen: {return_c}") - self.log("Returning to main screen") - self.push_screen("main") - elif isinstance(return_c, str): - if return_c == "identity": - main_screen = self.get_screen("main") - self.identities_table = main_screen.query_one("#user_identity_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.identities_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.identities_table.rows: - rows_with_keys.append((row_key.value, self.identities_table.get_row(row_key))) - self.push_screen(EditIdentitiesScreen(columns, rows_with_keys), callback=self.edit_identities_callback) - elif return_c == "community": - main_screen = self.get_screen("main") - self.communities_table = main_screen.query_one("#communities_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.communities_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.communities_table.rows: - rows_with_keys.append((row_key.value, self.communities_table.get_row(row_key))) - self.push_screen(EditCommunitiesScreen(columns, rows_with_keys), callback=self.edit_communities_callback) - elif return_c == "role": - main_screen = self.get_screen("main") - self.roles_table = main_screen.query_one("#roles_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.roles_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.roles_table.rows: - rows_with_keys.append((row_key.value, self.roles_table.get_row(row_key))) - self.push_screen(EditRolesScreen(columns, rows_with_keys), callback=self.edit_roles_callback) - elif return_c == "team": - main_screen = self.get_screen("main") - self.teams_table = main_screen.query_one("#teams_table", DataTable) - # Extract Columns from table - columns = [col.label.plain for col in self.teams_table.columns.values()] - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in self.teams_table.rows: - # row_key.value gets the string representation of the RowKey - # get_row() returns the list of cell values for that row - rows_with_keys.append((row_key.value, self.teams_table.get_row(row_key))) - self.push_screen(EditTeamsScreen(columns, rows_with_keys), callback=self.edit_teams_callback) - else: - self.log(f"Unexpected return type from EditProfileScreen: {type(return_c)}") - self.push_screen("main") - - -if __name__ == "__main__": - app = MyProfileApp() - app.run() -# -# if __name__ == "__main__": -# main() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/my_profile_tui.py b/my_egeria/my_egeria/DemoCode/Deprecated/my_profile_tui.py deleted file mode 100644 index 698940ab..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/my_profile_tui.py +++ /dev/null @@ -1,2199 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from __future__ import annotations - -import datetime -import re -from typing import Any - -from pyegeria import copy_to_clipboard - -from pyegeria.view.format_set_executor import exec_report_spec -from pyegeria import MyProfile, PyegeriaException, print_basic_exception, AutomatedCuration -from pyegeria import load_app_config, settings -from pyegeria import MetadataExpert - -from textual.css.query import NoMatches - -from textual.widgets._tree import TreeNode -from textual import on, work -from textual.app import App, ComposeResult -from textual.containers import ScrollableContainer, Container -from textual.screen import ModalScreen, Screen -from textual.widgets import ( - Button, - DataTable, - Footer, - Header, - Input, - Static, Tree, OptionList, TextArea, Placeholder, -) -from textual.widgets._option_list import Option - -class SelectionOverviewScreen(Screen): - """Screen to display the selection of the user's data sources.""" - BINDINGS = [("q", "quit", "Quit"), - ("b", "back", "Go back")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, category, view_server, url, user, pwd): - self.category = category - self.view_server = view_server - self.platform_url = url - self.user_name = user - self.user_password = pwd - if self.category == "glossary": - self.data_tree: Tree = app.query_one("#glossary_details_tree", Tree) - elif self.category == "catalog": - self.data_tree: Tree = app.query_one("#digital_product_catalog_tree", Tree) - elif self.category == "dictionary": - self.data_tree: Tree = app.query_one("#data_dictionary_tree", Tree) - elif self.category == "domain": - self.data_tree: Tree = app.query_one("#business_domain_tree", Tree) - elif self.category == "specification": - self.data_tree: Tree = app.query_one("#data_specification_tree", Tree) - else: - # unknown category - # push status screen, logic error in code. - error_category = "Collection Category" - error_message = "Unknown collection category returned" - self.log(f"Error in selection overview processing: {error_category}, {error_message}") - app.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=app.status_callback) - super().__init__() - - def compose(self) -> ComposeResult: - """ Compose the UI components for the SelectionOverviewScreen screen.""" - self.title = f"Shopping for Data, Data Selection:" - self.sub_title = f"Category: {self.category}" - yield Header(show_clock=True) - yield Static("Please select an item from the tree [blink]:[/]") - yield ScrollableContainer( - self.data_tree, - id="data_tree_container" - ) - yield ScrollableContainer( - Placeholder(id="data_details_placeholder"), - id="data_details_placeholder_container" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss(210) - - def action_back(self) -> None: - """ The back option in the footer has been selected. Dismiss the screen.""" - self.dismiss(200) - - @on(Tree.NodeSelected, "#data_tree") - def handle_tree_node_selected(self, event: Tree.NodeSelected): - # Modify to handle for each tree? - self.node_selected = event.node - self.log(f"Node selected: {self.node_selected}") - self.node_label = self.node_selected.label - # the provided id can be either a GUID or a qualified name! - # the variable is labeled guid but it could contain a qualified name, both guid and qualified name are strings. - self.node_GUID = self.node_selected.data - self.log(f"Node label: {self.node_label}, GUID: {self.node_GUID}") - if self.node_label == "glossary": - self.display_selected_term_details(self.node_GUID) - elif self.node_label == "digital_product_catalog": - self.display_selected_digital_product(self.node_GUID) - elif self.node_label == "data_dictionary": - self.display_selected_data_dictionary(self.node_GUID) - elif self.node_label == "business_domain": - self.display_selected_business_domain(self.node_GUID) - elif self.node_label == "data_specification": - self.display_selected_data_specification(self.node_GUID) - - def display_selected_term_details(self, term_GUID) -> Any: - """ The user has selected a glossary term, build a display of the term details, - and show along side the glossary tree """ - self.term_GUID = term_GUID - self.log(f"Selected glossary term GUID: {self.term_GUID}") - try: - self.glossary_term_data = exec_report_spec(format_set_name="Glossary-Terms", - output_format="DICT", - params={"search_string": self.term_GUID, "filter_string": self.term_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving glossary details: {e!s}") - self.dismiss(421) - return (421) - if not self.glossary_term_data or self.glossary_term_data == []: - self.log(f"No glossary term data returned for GUID: {self.term_GUID}") - Static(f"No glossary term data returned for GUID: {self.term_GUID}").mount(self.query_one("#glossary_term_details_container")) - else: - self.log(f"Glossary term data returned for GUID: {self.term_GUID}") - TextArea(f"Glossary term data returned for GUID: {self.term_GUID}", id="glossary_term_details_text_area", read_only=True).mount(self.query_one("#glossary_term_details_container")) - TextArea.data = self.glossary_term_data - - def display_selected_digital_product(self, digital_product_GUID) -> Any: - """ The user has selected a glossary term, build a display of the term details, - and show along side the glossary tree """ - self.digital_product_GUID = digital_product_GUID - self.log(f"Selected digital product GUID: {self.digital_product_GUID}") - try: - self.digital_product_data = exec_report_spec(format_set_name="Digital-Products", - output_format="DICT", - params={"search_string": self.digital_product_GUID, - "filter_string": self.digital_product_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving digital product details: {e!s}") - self.dismiss(421) - return (421) - if not self.digital_product_data or self.digital_product_data == []: - self.log(f"No digital product data returned for GUID: {self.digital_product_GUID}") - Static(f"No digital product data returned for GUID: {self.digital_product_GUID}").mount( - self.query_one("#digital_product_details_container")) - else: - self.log(f"Digital product data returned for GUID: {self.digital_product_GUID}") - TextArea(f"Digital product data returned for GUID: {self.digital_product_GUID}", id="digital_product_details_text_area", - read_only=True).mount(self.query_one("#digital_product_details_container")) - TextArea.data = self.digital_product_data - - def display_selected_data_dictionary(self, data_dictionary_GUID) -> Any: - """ The user has selected a data dictionary, build a display of the dictionary details, - and show along side the dictionary tree """ - self.data_dictionary_GUID = data_dictionary_GUID - self.log(f"Selected data dictionary GUID: {self.data_dictionary_GUID}") - try: - self.data_dictionary_data = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params={"search_string": self.data_dictionary_GUID, - "filter_string": self.data_dictionary_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving data dictionary details: {e!s}") - self.dismiss(421) - return (421) - if not self.data_dictionary_data or self.data_dictionary_data == []: - self.log(f"No data dictionary data returned for GUID: {self.data_dictionary_GUID}") - Static(f"No data dictionary data returned for GUID: {self.data_dictionary_GUID}").mount( - self.query_one("#data_dictionary_details_container")) - else: - self.log(f"Data dictionary data returned for GUID: {self.data_dictionary_GUID}") - TextArea(f"Data dictionary data returned for GUID: {self.data_dictionary_GUID}", - id="data_dictionary_details_text_area", - read_only=True).mount(self.query_one("#data_dictionary_details_container")) - TextArea.data = self.data_dictionary_data - - def display_selected_business_domain(self, business_domain_GUID) -> int: - """ The user has selected a business domain, build a display of the domain details, - and show along side the glossary tree """ - self.business_domain_GUID = business_domain_GUID - self.log(f"Selected business domain GUID: {self.business_domain_GUID}") - try: - self.business_domain_data = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params={"search_string": self.business_domain_GUID, - "filter_string": self.business_domain_GUID}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving business domain details: {e!s}") - self.dismiss(421) - return (421) - if not self.business_domain_data or self.business_domain_data == []: - self.log(f"No business domain data returned for GUID: {self.business_domain_GUID}") - Static(f"No business domain data returned for GUID: {self.business_domain_GUID}").mount( - self.query_one("#business_domain_details_container")) - else: - self.log(f"Business domain data returned for GUID: {self.business_domain_GUID}") - TextArea(f"Business domain data returned for GUID: {self.business_domain_GUID}", - id="business_domain_details_text_area", - read_only=True).mount(self.query_one("#business_domain_details_container")) - TextArea.data = self.business_domain_data - - def display_selected_data_specification(self, data_specification_GUID) -> int: - """ The user has selected a data specification, build a display of the specification details - and show alongside data specifications tree """ - - self.data_specification_qualified_name = data_specification_GUID - self.log(f"Data specification selected: {self.data_specification_qualified_name}") - try: - self_data_specification_data = exec_report_spec(format_set_name="Data-Specification", - output_format="DICT", - params={"search_string": self.data_specification_qualified_name, - "filter_string": self.data_specification_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving data specification details: {e!s}") - self.dismiss(431) - return (431) - if not self_data_specification_data or self_data_specification_data == []: - self.log(f"No data specification data returned for qualified name: {self.data_specification_qualified_name}") - Static(f"No data specification data returned for qualified name: {self.data_specification_qualified_name}").mount( - self.query_one("#data_specification_details_container")) - else: - self.log(f"Data specification data returned for qualified name: {self.data_specification_qualified_name}") - TextArea(f"Data specification data returned for qualified name: {self.data_specification_qualified_name}", - id="data_specification_details_text_area", - read_only=True).mount(self.query_one("#data_specification_details_container")) - TextArea.data = self_data_specification_data - - -class ShopForDataScreen(Screen): - """ Screen to Present a choice of different data sources to the user.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "back", "Go back")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__ (self, glossary_table, digital_product_catalog_table, data_dictionary_table, business_domain_table, data_specification_table ): - """Initialize the ShopForDataScreen screen.""" - self.glossary_table: DataTable = glossary_table - self.digital_product_catalog_table: DataTable = digital_product_catalog_table - self.data_dictionary_table: DataTable = data_dictionary_table - self.business_domain_table: DataTable = business_domain_table - self.data_specification_table: DataTable = data_specification_table - super().__init__() - - def compose(self) -> ComposeResult: - """ Compose the UI components for the ShopForDataScreen screen.""" - yield Header(show_clock=True) - yield TextArea(f"\n\n\nPlease click on a line to select a data source:") - yield ScrollableContainer( - Static("Glossary"), - self.glossary_table) - yield ScrollableContainer( - Static("Digital Product Catalog"), - self.digital_product_catalog_table) - yield ScrollableContainer( - Static("Data Dictionary"), - self.data_dictionary_table) - yield ScrollableContainer( - Static("Business Domain"), - self.business_domain_table) - yield ScrollableContainer( - Static("Data Specification"), - self.data_specification_table) - yield Footer() - - @on(DataTable.RowSelected, "#glossary_table") - def handle_glossary_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.glossary_table.get_row(row_selected) - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["glossary", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#digital_product_catalog_table") - def handle_digital_product_catalog_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - self.log(f"Digital product row selected: {row_selected}, event: {event}") - # row_values = self.glossary_table.get_row(event.row_key) - row_values = self.query_one("#digital_product_catalog_table", DataTable).get_row(event.row_key) - self.log(f"Digital product row values: {row_values}") - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["product", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#data_dictionary_table") - def handle_data_dictionary_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#data_dictionary_table", DataTable).get_row(event.row_key) - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["dictionary", row_qualified_name, row_display_name]) - - @on(DataTable.RowSelected, "#business_domain_table") - def handle_business_domain_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#business_domain_table", DataTable).get_row(event.row_key) - row_qualified_name = row_values[0] - row_type_name = row_values[1] - row_guid = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, type name: {row_type_name}, guid: {row_guid}") - self.dismiss (["domain", row_qualified_name, row_type_name]) - - @on(DataTable.RowSelected, "#data_specification_table") - def handle_data_specification_table_selection(self, event: DataTable.RowSelected): - row_selected = event.row_key - row_values = self.query_one("#data_specification_table", DataTable).get_row(event.row_key) - row_display_name = row_values[0] - row_description = row_values[1] - row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") - self.dismiss (["specification", row_qualified_name, row_display_name]) - - def action_back(self) -> None: - """ The back option in the footer has been selected. Dismiss the screen.""" - self.dismiss(200) - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss(210) - - -class StatusScreen(ModalScreen[Any]): - """Modal screen to display the status of the application.""" - BINDINGS = [("q", "quit", "Quit"), - ("return", "successful", "Continue"), - ("b", "unsuccessful", "Bad result"), - ("c", "copy GUID to clipboard", "Copy GUID to clipboard"), - ] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, status_message: str) -> None: - """Initialize the StatusScreen screen.""" - super().__init__() - self.status_message = status_message - - def compose(self) -> ComposeResult: - """ Compose the UI components for the StatusScreen screen.""" - yield Header(show_clock=True) - yield TextArea(self.status_message, id="status_message_text_area", read_only=True) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss(200) - - def action_unsuccessful(self) -> None: - """ The bad result option in the footer has been selected. Dismiss the screen.""" - self.dismiss(400) - - def action_copy_guid_to_clipboard(self) -> None: - """ The copy GUID to clipboard option in the footer has been selected. Copy the GUID to the clipboard.""" - # The GUID is between single ' marks in the status_message when a good result is recorded. - match = re.search(r"'(.*?)'", self.status_message) - self.log(f"Match: {match}") - if match: - guid = match.group(1) - copy_to_clipboard(guid) - self.log(f"Copied GUID to clipboard: {guid}") - else: - self.dismiss(400) - self.dismiss(200) - - def action_successful(self): - """ The successful option in the footer has been selected. Dismiss the screen.""" - self.dismiss(200) - -class TechnologyTypeProcessesScreen(ModalScreen[Any]): - """Modal screen to display technology type templates in Egeria.""" - BINDINGS = [("q", "quit", "Quit"), - ("b", "back", "Go back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__ (self, - user_name: str, - user_kpts: int, - tech_type_name: str, - tech_type_description: str, - selected_t_option, - tech_type_option_selected, - tech_type_processes - ) -> None: - """Initialize the Technology_Type_Templatess screen.""" - super().__init__() - self.user_name = user_name - self.karma_points = user_kpts - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.selected_t_option = selected_t_option - self.selected_t_option_selected = tech_type_option_selected - self.tech_type_processes = tech_type_processes - self.full_process = None - load_app_config() - - async def on_mount(self) -> None: - """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - - if self.selected_t_option == "process": - self.log(f"Processing processes, with data: {self.selected_t_option_selected}") - # get selected process from the tech_type data - self.log(f"Technology Type Process: {self.tech_type_processes}") - if isinstance(self.tech_type_processes, list): - for process in self.tech_type_processes: - if process.get("Governance Process Name") == self.selected_t_option_selected: - self.full_process = process - self.selected_t_process = process.get("Qualified Name") - self.log(f"Selected Process: {self.selected_t_process}") - break - else: - continue - self.log(f"Selected Process: {self.selected_t_process}") - - for placeholder in self.selected_t_process: - name = placeholder.get("Property Name") or None - Description = placeholder.get("Description") or None - Type = placeholder.get("Data Type") or None - Example = placeholder.get("Example") or None - Required = placeholder.get("Required") or False - - # Sanitize the name for use as a CSS ID - safe_name = name.replace(" ", "_") if name else f"placeholder_{id(placeholder)}" - placeholder_text: TextArea = TextArea( - f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", - id=f"{safe_name}_placeholder_text_area", - read_only=True - ) - # Ensure TextArea is visible - placeholder_text.styles.height = 8 - - placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") - - # Mount the TextArea and the associated Input field into the ScrollableContainer - try: - load_point = self.query_one("#technology_type_templates_input") - await load_point.mount(placeholder_text, before="#submit_button") - await load_point.mount(placeholder_input, before="#submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") - continue - except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") - self.app.dismiss(416) - - def compose(self) -> ComposeResult: - """ Compose the UI components for the Technology_Type_Templatess screen.""" - yield Header(show_clock=True) - yield Static("Please complete the required fields and any optional fields you prefer:") - yield ScrollableContainer( - Static("Technology Type Template Input"), - Button("Submit", id="submit_button"), - id="technology_type_templates_input" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200", ) - - @on(Button.Pressed, "#submit_button") - def handle_submit_button_pressed(self, event: Button.Pressed) -> None: - """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") - save_input_data: dict = {} - for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget}") - save_input_data.update({input_widget.id: input_widget}) - self.log(f"Save input data: {save_input_data}") - self.dismiss(["input", save_input_data, self.full_process]) - - @on(Input.Changed, "#technology_type_templates_input") - def handle_input_changed(self, event: Input.Changed) -> None: - """ The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") - - -class TechnologyTypeTemplatesScreen(ModalScreen[Any]): - """Modal screen to display technology type templates in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "back", "Go back"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__ (self, - user_name: str, - user_kpts: int, - tech_type_name: str, - tech_type_description: str, - selected_t_option, - tech_type_option_selected, - tech_type_templates - ) -> None: - """Initialize the Technology_Type_Templatess screen.""" - super().__init__() - self.user_name = user_name - self.karma_points = user_kpts - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.selected_t_option = selected_t_option - self.selected_t_option_selected = tech_type_option_selected - self.tech_type_templates = tech_type_templates - self.full_template = None - load_app_config() - - async def on_mount(self) -> None: - """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - - if self.selected_t_option == "template": - self.log(f"Processing templates, with data: {self.selected_t_option_selected}") - # get selected template from the tech_type data - self.log(f"Technology Type Templates: {self.tech_type_templates}") - if isinstance(self.tech_type_templates, list): - for template in self.tech_type_templates: - if template.get("Catalog Template Name") == self.selected_t_option_selected: - self.full_template = template - self.selected_t_template = template.get("Placeholder Properties") - self.log(f"Selected Template: {self.selected_t_template}") - break - else: - continue - self.log(f"Selected Template: {self.selected_t_template}") - - for placeholder in self.selected_t_template: - name = placeholder.get("Property Name") or None - Description = placeholder.get("Description") or None - Type = placeholder.get("Data Type") or None - Example = placeholder.get("Example") or None - Required = placeholder.get("Required") or False - - # Sanitize the name for use as a CSS ID - safe_name = name.replace(" ", "_") if name else f"placeholder_{id(placeholder)}" - placeholder_text: TextArea = TextArea( - f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", - id=f"{safe_name}_placeholder_text_area", - read_only=True - ) - # Ensure TextArea is visible - placeholder_text.styles.height = 8 - - placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") - - # Mount the TextArea and the associateds Input field into the ScrollableContainer - try: - load_point = self.query_one("#technology_type_templates_input") - await load_point.mount(placeholder_text, before="#submit_button") - await load_point.mount(placeholder_input, before="#submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") - continue - except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") - self.app.dismiss(416) - - def compose(self) -> ComposeResult: - """ Compose the UI components for the Technology_Type_Templatess screen.""" - yield Header(show_clock=True) - yield Static("Please complete the required fields and any optional fields you prefer:") - yield ScrollableContainer( - Static("Technology Type Template Input"), - Button("Submit", id="submit_button"), - id="technology_type_templates_input" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200", ) - - @on(Button.Pressed, "#submit_button") - def handle_submit_button_pressed(self, event: Button.Pressed) -> None: - """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") - save_input_data: dict = {} - for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget}") - save_input_data.update({input_widget.id: input_widget}) - self.log(f"Save input data: {save_input_data}") - self.dismiss(["input", save_input_data, self.full_template]) - - @on(Input.Changed, "#technology_type_templates_input") - def handle_input_changed(self, event: Input.Changed) -> None: - """ The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") - - -class TechnologyTypeOptionsScreen(ModalScreen): - """ Modal screen to display a technology type's templates and processes.""" - BINDINGS = [("q", "dismiss", "Quit"), - ("b", "back", "Go back"), - ("ctl+s", "Select", "tech_type_option_select")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, tech_type_guid: str, - tech_type_name: str, - tech_type_description: str, - user_name: str, - user_pwd:str, - user_kpts: int, - tech_type_templates: list[dict], - tech_type_processes: list[dict]) -> None: - """Initialize the TechnologyTypeOptions screen with a technology type's templates and processes.""" - super().__init__() - self.selected_process_index = None - self.tech_type_guid = tech_type_guid - self.tech_type_name = tech_type_name - self.tech_type_description = tech_type_description - self.user_name = user_name - self.user_password = user_pwd - self.karma_points = user_kpts - self.tech_type_templates = tech_type_templates - self.tech_type_processes = tech_type_processes - self.selected_template_guid = None - self.selected_process_guid = None - self.option_type_selected = None - self.selected_template = None - self.selected_template_index = None - self.selected_template_data = None - self.selected_process = None - self.selected_process_data = None - - load_app_config("config/config.json") - app_config = settings.Environment - app_user = settings.User_Profile - - if not self.user_name: - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - - def compose(self) -> ComposeResult: - - """ Compose the UI components for the TechnologyTypeOptions screen.""" - yield Header(show_clock=True) - yield Static(f"Description: {self.tech_type_description}") - yield ScrollableContainer( - Static(f"Technology Type: {self.tech_type_name}"), - Container( - Static("Select a template or process to continue."), - Static(f"Templates", id="number_of_templates_label"), - ScrollableContainer( - Static("Available Templates", id="template_options_label"), - OptionList(id="template_options"), - Button("Select Template", id="select_template_btn"), - id="template_options_container"), - Static(f"Processes:", id="number_of_processes_label"), - ScrollableContainer( - Static("Available Processes",id="process_options_label"), - OptionList(id="process_options"), - id="process_options_container"), - Button( - "Select Process", - id="select_process_btn"))) - yield Footer() - - async def on_mount(self) -> int: - """Mount the TechnologyTypeOptions screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - self.log(f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}") - - for widget in self.query(): - self.log(f"Widgets: {widget}") - - self.log(f"Templates: {self.tech_type_templates}, Processes: {self.tech_type_processes}") - self.log(f"Templates Type: {type(self.tech_type_templates)}, Processes Type: {type(self.tech_type_processes)}") - - if self.tech_type_templates and self.tech_type_templates != "None": - for t in self.tech_type_templates: - try: - self.log(f"Template: {t}") - templates = self.query_one("#template_options", OptionList).add_option( - Option(t.get("Catalog Template Name"))) - self.log(f"Added option: {t.get('Catalog Template Name')}") - await templates.mount(after=self.query_one("#template_options_label")) - self.log(f"Mounted option") - self.query_one("#select_template_btn", Button).disabled = False - self.log(f"Enabled the template button") - except NoMatches as e: - try: - templates: OptionList = OptionList(id="template_options") - await templates.mount(after=self.query_one("#template_options_label")) - templates.add_option(Option(t.get("Catalog Template Name"))) - templates.refresh() - self.query_one("#select_template_btn", Button).disabled = False - continue - except Exception as e: - self.log(f"Error creating template option list: {e}, (410") - return (410) - except Exception as e: - self.log(f"Error creating template option list: {e} (411)") - return (411) - else: - try: - self.log(f" No Templates") - templates = self.query_one("#template_options", OptionList).add_option( - Option("No Templates Found for this Tech Type")) - await templates.mount(after=self.query_one("#template_options_label")) - self.query_one("#select_template_btn", Button).disabled = True - except NoMatches as e: - try: - templates: OptionList = OptionList(id="template_options") - await templates.mount(after=self.query_one("#template_options_label")) - templates.add_option(Option("No Templates found for this Tech Type")) - self.query_one("#select_template_btn", Button).disabled = True - except Exception as e: - self.log(f"Error creating template option list: {e}") - return (409) - except Exception as e: - self.log(f"Error creating template option list: {e}") - return (408) - - if self.tech_type_processes and self.tech_type_processes != "None": - for p in self.tech_type_processes: - try: - self.log(f"Process: {p}") - processes = self.query_one("#process_options", OptionList).add_option( - Option(p.get("Governance Process Name"))) - await processes.mount(after=self.query_one("#process_options_label")) - self.query_one("#select_process_btn", Button).disabled = False - except NoMatches as e: - try: - processes: OptionList = OptionList(id="process_options") - await processes.mount(after=self.query_one("#process_options_label")) - processes.add_option(Option(p.get("Governance Process Name"))) - processes.refresh() - self.query_one("#select_process_btn", Button).disabled = False - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (407) - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (406) - else: - try: - self.log(f" No Processes") - processes = self.query_one("#process_options", OptionList).add_option( - Option("No processes found for this Tech Type")) - await processes.mount(after=self.query_one("#process_options_label")) - self.query_one("#select_process_btn", Button).disabled = True - except NoMatches as e: - try: - processes: OptionList = OptionList(id="process_options") - await processes.mount(after=self.query_one("#process_options_label")) - processes.add_option(Option(" No Processes found for this Tech Type")) - processes.refresh() - self.query_one("#select_process_btn", Button).disabled = True - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (405) - except Exception as e: - self.log(f"Error creating process option list: {e}") - return (404) - - return(200) - - @on(OptionList.OptionHighlighted, "#template_options") - def handle_template_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: - """Handle the highlighting of an option in the template option list.""" - self.log(f"Template option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.selected_template_index = event.option_index - selected_option = event.option - self.selected_template = selected_option.prompt - self.log(f"Highlighted Template: {self.selected_template}, index: {self.selected_template_index}") - - @on(OptionList.OptionSelected, "#template_options") - def handle_template_option_selected(self, event: OptionList.OptionSelected) -> None: - """Handle the selection of an option from the template or process option lists.""" - self.log(f"Template selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.option_type_selected = event.option_list.name - self.selected_template_index = event.option_index - selected_option = event.option - self.selected_template = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_template}, index: {self.selected_template_index}") - - @on(OptionList.OptionHighlighted, "#process_options") - def handle_process_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: - """Handle the highlighting of an option in the process option list.""" - self.log(f"Process option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.selected_process_index = event.option_index - selected_option = event.option - self.selected_process = selected_option.prompt - self.log(f"Highlighted Process: {self.selected_process}, index: {self.selected_process_index}") - - @on(OptionList.OptionSelected, "#process_options") - def handle_process_option_selected(self, event: OptionList.OptionSelected) -> None: - """Handle the selection of an option from the template or process option lists.""" - self.log(f"Process selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") - self.option_type_selected = event.option_list.name - self.selected_process_index = event.option_index - selected_option = event.option - self.selected_process = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_process}, index: {self.selected_process_index}") - - def action_back(self) -> None: - """Handle the back button press.""" - self.dismiss("back") - - @on(Button.Pressed, "#select_template_btn") - def handle_template_selected(self, event: Button.Pressed) -> None: - """Handle the selection of a template option.""" - if self.selected_template is None: - return - self.log(f"Template option selected: {self.selected_template}") - self.dismiss(["template", self.selected_template]) - - @on(Button.Pressed, "#select_process_btn") - def handle_process_selected(self, event: Button.Pressed) -> None: - """Handle the selection of a process option.""" - if self.selected_process is None: - return - self.log(f"Process option selected: {self.selected_process}") - self.dismiss(["process", self.selected_process]) - - -class TechnologyTypesScreen(ModalScreen): - """Modal screen to display technology types in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("ctl+e", "expand", "Toggle Twisties")] - - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self, ttlist, user_name, user_pwd, user_kpts): - """Initialize the TechnologyTypes screen with a list of technology types.""" - super().__init__() - self.tech_type_list = ttlist - self.user_name = user_name - self.user_password = user_pwd - self.karma_points = user_kpts - self.tech_type_tree: Tree[str] = Tree(label="Technology Types", id="technology_types_tree") - self.tech_type_tree.root.expand() - self.tech_type_tree.auto_expand = True - self.selected_t_node = None - self.selected_t_node_label = None - self.node_id = None - self.node_status = "expanded" - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}" - self.sub_title = "Select a technology type" - - def compose(self) -> ComposeResult: - """ Compose and display the technology type screen""" - self.tech_type_tree.refresh() - if self.tech_type_list: - self.log(f"Technology types: {self.tech_type_list}, type: {type(self.tech_type_list)}") - self.render_tech_type_hierarchy_to_tree(self.tech_type_list, self.tech_type_tree) - else: - self.tech_type_tree.root.add("No technology types found", expand=True) - self.tech_type_tree.refresh() - self.log(f"Technology types tree: {self.tech_type_tree}") - - yield Header(show_clock=True) - yield ScrollableContainer( - Static("Display technology types in Egeria"), - self.tech_type_tree, - Button("Select", id="select_tech_type_btn"), - id="technology_types_table" - ) - yield Footer() - - def action_quit(self) -> None: - """ The quit option in the footer has been selected. Dismiss the screen.""" - self.dismiss("200") - - @on(Button.Pressed, "#select_tech_type_btn") - def handle_select_tech_type(self, event: Button.Pressed) -> str|None: - """ The select button on the screen has been pressed.""" - self.log(f"Select button pressed, button: {event.button}") - if self.selected_t_node: - self.log(f"Selected node: {self.selected_t_node}, label: {self.selected_t_node.label}") - self.dismiss(str(self.selected_t_node.label)) - else: - return "No technology type selected" - - @on(Tree.NodeSelected) - def handle_tree_node_selected(self, event: Tree.NodeSelected) -> None: - """ The user has selected a node in the tree. """ - self.log(f"Tree node selected, node: {event.node}") - self.selected_t_node = event.node - self.selected_t_node_label = event.node.label - return - - @on(Tree.NodeCollapsed) - def handle_tree_node_collapsed(self, event: Tree.NodeCollapsed) -> None: - # handle the twisty to close a node in the tree - # logger.debug(f"TreeNodeCollapsed: {event.node.id}") - self.node_id = str(event.node.id) - self.node_status = "collapsed" - - @on(Tree.NodeExpanded) - def handle_tree_node_expanded(self, event: Tree.NodeExpanded) -> None: - # handle the twisty to open a node in the tree - # logger.debug(f"TreeNodeExpanded: {event.node.id}") - self.node_id = str(event.node.id) - self.node_status = "expanded" - - def action_expand(self): - # handle the hot key to expand/collapse all nodes in the tree (see bindings) - self.tech_type_tree = self.query_one("#technology_types_tree", Tree) - if self.node_status == "collapsed": - self.tech_type_tree.root.expand_all() - else: - self.tech_type_tree.root.collapse_all() - return - - def render_tech_type_hierarchy_to_tree(self, - data: dict | list[dict], - tree_or_node: Tree[str] | TreeNode[str], - label_attr: str = "displayName", - guid_attr: str = "guid", - children_attr: str = "subTypes" - ) -> None: - """Recursively render a DICT) hierarchy into a Textual Tree. - - Args: - data: The data structure from pyegeria.get_tech_type_hierarchy. - tree_or_node: The Tree object or a TreeNode to add children to. - label_attr: The attribute in the data for the node label. - guid_attr: The attribute in the data for the node data (GUID). - children_attr: The attribute in the data containing the list of children. - """ - if isinstance(data, list): - for item in data: - self.render_tech_type_hierarchy_to_tree(item, tree_or_node, label_attr, guid_attr, children_attr) - return - - if not isinstance(data, dict): - return - - label = str(data.get(label_attr) or data.get("Display Name") or "Unknown") - guid = str(data.get(guid_attr) or data.get("GUID") or "") - - # If it's a Tree, we add to root. If it's a TreeNode, we add to it. - if isinstance(tree_or_node, Tree): - node = tree_or_node.root.add(label, data=guid, expand=True) - else: - node = tree_or_node.add(label, data=guid, expand=True) - - children = data.get(children_attr) - if children and isinstance(children, list): - for child in children: - self.render_tech_type_hierarchy_to_tree(child, node, label_attr, guid_attr, children_attr) - - -class CreateProfileScreen(ModalScreen[int]): - """Modal screen to create a new user profile in Egeria. - - Dismisses with: - 200 on success - 4xx on failure - """ - - BINDINGS = [("q", "dismiss(200)", "Quit")] - CSS_PATH = "../My_Profile/my_profile.tcss" - - def __init__(self): - super().__init__() - load_app_config() - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - self.karma_points = 0 - - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server - self.platform_url = app_config.egeria_platform_url - print("Platform:", app_config.egeria_platform_url) - print("View Server:", app_config.egeria_view_server) - - def on_mount(self) -> None: - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Create Egeria Profile for user: {self.user_name}" - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static("Create a new profile in Egeria") - yield ScrollableContainer( - Static("Fill in your information below:"), - Input(placeholder="Courtesy Title", id="user_title"), - Input(placeholder="Given Names", id="user_given_names"), - Input(placeholder="Family Name", id="user_family_name"), - Input(placeholder="Preferred Name", id="user_preferred_name"), - Input(placeholder="Pronouns", id="user_pronouns"), - Input(placeholder="Job Title", id="user_job_title"), - Input(placeholder="Description", id="user_description"), - Input(placeholder="Employee ID", id="user_employee_id"), - Input(placeholder="Preferred Language", id="user_preferred_language"), - Input(placeholder="Resident Country", id="user_resident_country"), - Input(placeholder="Time Zone", id="user_time_zone"), - Button("Create Profile", id="create_profile_btn"), - id="create_profile_form", - ) - - yield Footer() - - @on(Button.Pressed, "#create_profile_btn") - def create_profile(self) -> int: - """Create a new profile in Egeria from data provided in Input fields.""" - - self.new_element_request_body:dict = {"class": "NewElementRequestBody", - "isOwnAnchor": True, - "properties": { - "class": "PersonProperties", - "qualifiedName": "Person" + self.query_one("#user_employee_id", - Input).value + - self.query_one("#user_resident_country", - Input).value + - self.query_one("#user_given_names", Input).value + - self.query_one("#user_family_name", Input).value, - "displayName": self.query_one("#user_preferred_name", Input).value, - "courtesyTitle": self.query_one("#user_title", Input).value, - "initials": "PAT", - "givenNames": self.query_one("#user_given_names", Input).value, - "surname": self.query_one("#user_family_name", Input).value, - "fullName": self.query_one("#user_preferred_name", Input).value, - "pronouns": self.query_one("#user_pronouns", Input).value, - "jobTitle": self.query_one("#user_job_title", Input).value, - "employeeNumber": self.query_one("#user_employee_id", Input).value, - "employeeType": "Full-Time", - "preferredLanguage": self.query_one("#user_preferred_language", - Input).value, - "residentCountry": self.query_one("#user_resident_country", - Input).value, - "timeZone": self.query_one("#user_time_zone", Input).value, - "description": self.query_one("#user_description", Input).value, - } - } - try: - new_profile_inst = MyProfile(self.app.view_server, self.app.platform_url, self.app.user, self.app.password) - new_profile_guid = new_profile_inst.add_my_profile(self.new_element_request_body) - self.log(f"Profile created with GUID: {new_profile_guid}") - self.dismiss(200) - return(200) - except PyegeriaException as e: - self.log(f"Error creating profile: {e!s} | request={self.new_element_request_body}") - self.dismiss(401) - return(401) - - def action_quit(self) -> int: - self.dismiss(200) - return(200) - - -class MyProfileTui(App): - """My Profile App. - - Retrieves a user's profile from Egeria and displays current work items. - If no profile is found, offers a UI to create one. - """ - - BINDINGS = [("q", "quit", "Quit")] - CSS_PATH = "../My_Profile/my_profile.tcss" - - SCREENS = { - "create_profile": CreateProfileScreen, - "tech_types": TechnologyTypesScreen, - "tech_type_options": TechnologyTypeOptionsScreen, - "tech_type_templates": TechnologyTypeTemplatesScreen, - "tech_type_processes": TechnologyTypeProcessesScreen, - "status": StatusScreen, - "shop_4_data": ShopForDataScreen, - "overview": SelectionOverviewScreen - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.contribution_record = None - self.heading = "My_Profile" - self.subheading = "Egeria Profile for current user" - self.description = "Display the user related items for the current user." - load_app_config("config/config.json") - app_config = settings.Environment - app_user = settings.User_Profile - # config_logging() - print("Platform:", app_config.egeria_platform_url) - print("View Server:", app_config.egeria_view_server) - self.user_name = app_user.user_name or "garygeeke" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - - # Ensure compose() is safe before data loads - self.actor_profile: dict = {} - self.projects = [] - self.communities = [] - self.roles = [] - self.actions = [] - self.teams = [] - self.other_function_list = [] - self.tech_type_json: str - self.tech_type_response = None - self.tech_type_list = [] - self.tech_type_guid = "" - self.tech_type_name = "" - self.tech_type_description = "" - self.selected_t_node = None - self.selected_t_node_label = None - self.karma_points = 0 - self.tech_type_templates = [{}] - self.tech_type_processes = [{}] - self.full_template = None - self.glossary_data_extract = None - self.business_glossary_data_extract = None - self.display_glossary_data_extract = None - self.digital_glossary_data_extract = None - - def compose(self) -> ComposeResult: - - # Create tables up-front; populate them in on_mount() - self.projects_table = DataTable(id="projects_table") - self.communities_table = DataTable(id="communities_table") - self.roles_table = DataTable(id="roles_table") - self.actions_table = DataTable(id="actions_table") - self.teams_table = DataTable(id="teams_table") - self.user_identity_table = DataTable(id="user_identity_table") - self.other_function_list = OptionList(id="other_function_list") - - # place widgets into grid on screen, note sequence determines position! - yield Header(show_clock=True) - - yield ScrollableContainer( - Static("Projects"), - self.projects_table - ) - - yield ScrollableContainer( - Static("Communities"), - self.communities_table - ) - - yield ScrollableContainer( - Static(f"Other Functions"), - Static(f"[b]Select a function[/b]"), - OptionList( - Option("User Identities", disabled=True), - Option("Catalogs/Shop for Data"), - Option("Edit Profile", disabled=True), - Option("Subscriptions", disabled=True), - Option("Technology Types"), - Option("Update Profile", disabled=True), - Option("User Bookmarks", disabled=True), - id="other_function_list" - ), - id="other_function_container" - ) - - yield ScrollableContainer( - Static("Roles"), - self.roles_table - ) - - yield ScrollableContainer( - Static("Teams"), - self.teams_table - ) - - yield ScrollableContainer( - Static("Actions"), - self.actions_table - ) - yield Footer() - - async def on_mount(self) -> None: - """Load profile; if missing, prompt to create it; then populate tables.""" - # DOMInfo.attach_to(self) - # Initiate clipboard session - - await self._load_or_create_profile() - await self._populate_tables() - - - async def _load_or_create_profile(self) -> None: - try: - #instantiate the class - self.my_profile_inst = MyProfile(self.view_server, self.platform_url, self.user_name, self.user_password) - self.my_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) - #retrieve the profile - self.my_profile_data = await self.my_profile_inst._async_get_my_profile( - report_spec="My-User-MD", - output_format="DICT", - ) - self.log(f"retrieve profile result: {self.my_profile_data}") - except (PyegeriaException) as e: - self.log(f"Error retrieving profile: {e!s}") - print_basic_exception(e) - self.exit(402) - return - - if self.my_profile_data == []: - self.log(f"Error retrieving profile. Prompting to create one...") - self.log(f"To create a profile you must have a valid userid in the system, please contact your system administrator to create one if needed") - await self.push_screen(CreateProfileScreen(), callback = self.new_profile_return) - else: - self.new_profile_return(200) - - def new_profile_return(self, result: int) -> None: - """ This function handles either the return from the create new profile screen or - when the user already has a profile continue processing """ - self.log(f"Profile creation result: {result}") - if not result or result != 200: - self.log(f"Profile creation cancelled/failed; return: {result}, exiting.") - self.exit(403) - return - - self.result = result - - # Retry after creation if necessary - try: - self.user_profile_struct = self.my_profile_inst.get_my_profile( - output_format="DICT", - report_spec="My-User-MD", - ) - self.log(f"Profile retrieved successfully: {self.user_profile_struct}") - except PyegeriaException as e2: - self.log(f"Error retrieving user profile: {e2!s}") - self.exit(412) - return - if self.user_profile_struct is []: - self.log(f"Error retrieving user profile. Exiting.") - self.exit(413) - return - - # strip out the individual profile elements - self.user_profile = self.user_profile_struct[0] - self.contribution_record = self.user_profile.get("Contribution Record") or {} - for c in self.contribution_record: - self.karma_points = c.get("Karma Points") or 0 - break - self.my_projects_data = self.user_profile.get("Projects") or [] - self.my_teams_data = self.user_profile.get("Teams") or [] - self.my_communities_data = self.user_profile.get("Communities") or [] - self.my_roles_data = self.user_profile.get("Roles") or [] - self.my_actions_data = self.user_profile.get("Actions") or [] - self.log(f"Contribution Record: {self.contribution_record}") - self.log(f"Karma Points: {self.karma_points}") - self.log(f"my_projects_data: {self.my_projects_data}") - self.log(f"my_teams_data: {self.my_teams_data}") - self.log(f"my_communities_data: {self.my_communities_data}") - self.log(f"my_roles_data: {self.my_roles_data}") - self.log(f"my_actions_data: {self.my_actions_data}") - # User Identities - try: - self.user_identities = self.my_profile_inst.get_my_profile( - report_spec="User-Identities", - output_format="DICT", - ) - self.log(f"User-Identities: {self.user_identities}, type: {type(self.user_identities)}") - except PyegeriaException as e: - self.log(f"Error retrieving User-Identities: {e!s}") - self.user_identities = {} - - # Normalize expected keys - self.full_name = self.user_profile.get("Full Name") or "" - self.sub_title = f"{self.full_name} ({self.user_profile.get('User ID')}, Karma Points: {self.karma_points})" - self.projects = self.my_projects_data or [] - self.communities = self.my_communities_data or [] - self.roles = self.my_roles_data or [] - self.actions = self.user_profile.get("actions") or [] - self.teams = self.my_teams_data or [] - if isinstance(self.user_identities, list): - self.user_identity = self.user_identities - else: - self.user_identity = self.user_identities.get("User-Identities") or [] - - async def _populate_tables(self) -> None: - """Populates tables from normalized profile data""" - assert self.projects_table is not None - assert self.communities_table is not None - assert self.roles_table is not None - assert self.actions_table is not None - assert self.user_identity_table is not None - assert self.teams_table is not None - - self.projects_table.clear(columns=True) - self.projects_table.add_columns("Project Name", "Description", "Qualified Name") - - self.communities_table.clear(columns=True) - self.communities_table.add_columns("Assignment Type", "Community Name", "Description", "Qualified Name") - - self.roles_table.clear(columns=True) - self.roles_table.add_columns("Role Name", "Description","GUID") - - self.teams_table.clear(columns=True) - self.teams_table.add_columns("Assignment Type", "Team Name", "Description","GUID") - - self.actions_table.clear(columns=True) - self.actions_table.add_columns("Action Name", "Status", "Description") - - self.user_identity_table.clear(columns=True) - self.user_identity_table.add_columns("Display Name", "User ID", "Distinguished Name") - - # Populate rows - for p in self.projects if isinstance(self.projects, list) else []: - self.projects_table.add_row( - str(p.get("Name", "")), - str(p.get("Description", "")), - str(p.get("Qualified Name", "")), - ) - - for c in self.communities if isinstance(self.communities, list) else []: - self.communities_table.add_row( - str(c.get("Assignment Type", "")), - str(c.get("Display Name", "")), - str(c.get("Description", "")), - str(c.get("Qualified Name", "")) - ) - - for r in self.roles if isinstance(self.roles, list) else []: - self.roles_table.add_row( - str(r.get("Name", "")), - str(r.get("Type", "")), - str(r.get("GUID", "")), - ) - - for a in self.actions if isinstance(self.actions, list) else []: - self.actions_table.add_row( - str(a.get("Display Name", "")), - str(a.get("Description", "")), - ) - - for t in self.teams if isinstance(self.teams, list) else []: - self.teams_table.add_row( - str(t.get("Assignment Type", "")), - str(t.get("Name", "")), - str(t.get("Description", "")), - str(t.get("GUID", "")), - ) - - def action_quit(self) -> None: - # quit selected by user, so exit app - self.exit(200) - - @on(OptionList.OptionSelected, "#other_function_list") - async def handle_option_selected(self, event: OptionList.OptionSelected): - # option selected by user - selected_option = event.option.prompt - selected_option_id = event.option.id - self.log(f"Selected option: {selected_option} ({selected_option_id})") - if selected_option == "Technology Types": - self.log("Fetching technology types...") - await self.fetch_technology_types() - self.log(f"Tech Type Response: {self.tech_type_response} | {self.tech_type_list}") - if self.tech_type_response == []: - self.log("No technology types found.") - self.exit(200) - elif len(self.tech_type_response) == 3 and int(self.tech_type_response) >= 400: - self.log("Error fetching technology types.") - self.exit(int(self.tech_type_response)) - self.log("Technology types fetched successfully.") - self.log("Displaying technology types...") - await self.push_screen(TechnologyTypesScreen(self.tech_type_list, self.user_name, self.user_password, self.karma_points), callback=self.tech_type_callback) - self.log("Technology types displayed successfully.") - elif selected_option == "User Identities": - pass - elif selected_option == "Catalogs/Shop for Data": - """ Push new Screen, Show Glossaries, Digital Product Catalogs, Data Dictionaries and - Business Domains, allow the user to select from one of the 4 categories and use that selection to - display a list of available collections of the chosen type and allow the user to subscribe to them - """ - - # start by gathering the data using Pyegeria to access the Egeria backend servers - - # Glossaries - glossary_table: DataTable = DataTable(id="glossary_table") - glossary_table.add_columns("Glossary Name", "Description", "Qualified Name") - glossary_table.cursor_type = "row" - glossary_table.zebra_stripes = True - try: - self.glossary_data = exec_report_spec(format_set_name="Glossaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving glossary details: {e!s}") - self.exit(420) - return(420) - self.log(f"Glossary data returned: {self.glossary_data}") - self.glossary_data_extract = self.glossary_data.get("data") or [] - self.log(f"Glossary data extracted: {self.glossary_data_extract}") - if self.glossary_data_extract == []: - self.log(f"No glossary data found for search string: {self.selected_t_node}") - glossary_table.add_row("No glossaries found", "No data returned from Egeria", "") - else: - for g in self.glossary_data_extract: - glossary_table.add_row(g.get("Display Name"), g.get("Description"), g.get("Qualified Name")) - continue - - # Digital Product Catalogs - digital_product_catalog_table: DataTable = DataTable(id="digital_product_catalog_table") - digital_product_catalog_table.add_columns("Digital Product Catalog Name", "Description", "Qualified Name") - digital_product_catalog_table.cursor_type = "row" - digital_product_catalog_table.zebra_stripes = True - try: - self.digital_product_catalog_data = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving digital product catalog details: {e!s}") - self.exit(421) - return(421) - self.log(f"Digital Product Catalog data returned: {self.digital_product_catalog_data}") - self.digital_product_catalog_data_extract = self.digital_product_catalog_data.get("data") or [] - self.log(f"Digital Product Catalog data extracted: {self.digital_product_catalog_data_extract}") - if self.digital_product_catalog_data_extract == []: - self.log(f"No digital product catalog data found for user: {self.user_name}") - digital_product_catalog_table.add_row("No digital product catalogs found", "No data returned from Egeria", "") - else: - for catalog_item in self.digital_product_catalog_data_extract: - digital_product_catalog_table.add_row(catalog_item["Display Name"], - catalog_item["Description"], - catalog_item["Qualified Name"]) - continue - - # Data Dictionaries - data_dictionary_table: DataTable = DataTable(id="data_dictionary_table") - data_dictionary_table.add_columns("Data Dictionary Name", "Description", "Qualified Name") - data_dictionary_table.cursor_type = "row" - data_dictionary_table.zebra_stripes = True - try: - self.data_dictionary_data = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving data dictionary details: {e!s}") - self.exit(422) - return(422) - self.data_dictionary_data_extract = self.data_dictionary_data.get("data") or [] - if self.data_dictionary_data_extract == []: - self.log(f"No data dictionary details found for user: {self.user_name}") - data_dictionary_table.add_row("No data dictionaries found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.data_dictionary_data_extract} data dictionaries for user {self.user_name}") - data_dictionary_table.add_row("Display Name", "Description", "Qualified Name") - for dictionary in self.data_dictionary_data_extract: - data_dictionary_table.add_row(dictionary.get("Display Name", ""), - dictionary.get("Description", ""), - dictionary.get("Qualified Name", "")) - continue - - # Business Domains - business_domain_table: DataTable = DataTable(id="business_domain_table") - business_domain_table.add_columns("Business Area Name", "Type Name", "GUID") - business_domain_table.cursor_type = "row" - business_domain_table.zebra_stripes = True - try: - self.business_domain_data = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params = {"search_string" : "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(423) - return(423) - self.business_domain_data_extract = self.business_domain_data.get("data") or [] - if self.business_domain_data_extract == []: - self.log(f"No business domains found for user {self.user_name}") - business_domain_table.add_row("No business domains found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.business_domain_data_extract} business domains for user {self.user_name}") - # business_domain_table.add_row("Business Domain Name", "Description", "GUID") - for domain in self.business_domain_data_extract: - business_domain_table.add_row(domain.get("Qualified Name", ""), - domain.get("Type Name", ""), - domain.get("GUID", "")) - continue - - # Data Specifications - data_specification_table: DataTable = DataTable(id="data_specification_table") - data_specification_table.add_columns("Display Name", "Description", "Qualified Name") - data_specification_table.cursor_type = "row" - data_specification_table.zebra_stripes = True - try: - self.data_specification_data = exec_report_spec(format_set_name="Data-Specifications", - output_format="DICT", - params = {"search_string": "*"}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - self.log(f"Error retrieving data specification details: {e!s}") - self.exit(423) - return (423) - - if isinstance(self.data_specification_data, dict): - self.data_specification_data_extract = self.data_specification_data.get("data") - elif isinstance(self.data_specification_data, list): - self.data_specification_data_extract = self.data_specification_data - else: - self.data_specification_data_extract = self.data_specification_data.get("Data-Specifications") or [] - - if self.data_specification_data_extract == [] or self.data_specification_data_extract == {} or self.data_specification_data_extract == None: - self.log(f"No data specifications found for user {self.user_name}") - data_specification_table.add_row("No data specifications found", "No data returned from Egeria", "") - else: - self.log(f"Found {self.data_specification_data_extract} data specifications for user {self.user_name}") - - for spec in self.data_specification_data_extract: - data_specification_table.add_row(spec.get("Display Name", ""), - spec.get("Description", ""), - spec.get("Qualified Name", "")) - continue - - # hand the data to the Screen for displaying - await self.push_screen(ShopForDataScreen(glossary_table, digital_product_catalog_table, data_dictionary_table, business_domain_table, data_specification_table ), callback = self.shop_for_data_callback) - - elif selected_option == "User Bookmarks": - pass - elif selected_option == "Subscriptions": - pass - else: - pass - - async def tech_type_callback(self, result) -> int: - """ Callback for Technology Types screen - If the result is int (4xx) it indicates an error in the screen - if the result is str it contains the GUID of the selected technology type - """ - #clear local data fields - tech_type_description = "" - self.tech_type_data = {} - self.tech_type_data_extracted = {} - self.tech_type_templates = [] - self.tech_type_processes = [] - # check that we got a valid result from the screen - if not result or isinstance(result, int) : - self.log(f"Technology Types screen cancelled/failed; return: {result}, exiting.") - self.exit(result) - return (result) - self.selected_t_node = str(result) - self.log(f"Technology Types screen returned: {self.selected_t_node}") - # Request details for selected tech type - try: - self.tech_type_data = exec_report_spec(format_set_name="Tech-Type-Details-MD", - output_format="DICT", - params={"search_string": self.selected_t_node, "filter_string": self.selected_t_node}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving technology type details: {e!s}") - self.exit(414) - return (414) - - self.log(f"Technology Type Data: {self.tech_type_data}, type: {type(self.tech_type_data)}") - - if self.tech_type_data.get("kind") is not None: - self.tech_type_data_extracted = self.tech_type_data.get("data") - else: - self.tech_type_data_extracted = {"Error": "No data found for this technology type."} - - self.log (f"Technology Type Data Extracted: {self.tech_type_data_extracted}") - - for dataset in self.tech_type_data_extracted: - self.tech_type_guid = dataset.get("GUID") - self.tech_type_name = dataset.get("Display Name") - self.tech_type_templates = dataset.get("Catalog Templates") - self.tech_type_processes = dataset.get("Governance Processes") - self.tech_type_description = tech_type_description - self.log(f"Technology Type GUID: {self.tech_type_guid}") - self.log(f"Technology Type Name: {self.tech_type_name}") - self.log(f"Technology Type Description: {self.tech_type_description}") - self.log(f"Technology Type Templates: {self.tech_type_templates}" or [{"templates": "None"}]) - self.log(f"Technology Type Processes: {self.tech_type_processes}" or [{"processes": "None"}]) - - await self.push_screen(TechnologyTypeOptionsScreen(self.tech_type_guid, - self.tech_type_name, - self.tech_type_description, - self.user_name, - self.user_password, - self.karma_points, - self.tech_type_templates, - self.tech_type_processes), callback = self.tech_type_options_callback) - return(200) - - async def tech_type_options_callback(self, result: list): - self.log(f"Technology Type Options screen returned: {result}") - if not result[0] or isinstance(result[0], int): - if isinstance(result[0], int) and result[0] == 200: - self.log("Technology Type Options screen returned successfully.") - return(200) - else: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - self.exit(415) - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") - self.exit(415) - return - - self.selected_t_option = str(result[0]) - self.selected_t_option_selected = result[1] - self.log(f"Technology Type Options screen returned: {self.selected_t_option} | {self.selected_t_option_selected}") - - # display the screen so the objects we need to mount to are created in the DOM - # then we can build the display elements for the placeholder properties - # and mount them in the appropriatwe containers on the screen - if self.selected_t_option == "template": - await self.push_screen(TechnologyTypeTemplatesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_templates), - callback=self.tech_type_templates_callback) - elif self.selected_t_option == "process": - await self.push_screen(TechnologyTypeProcessesScreen(self.user_name, - self.karma_points, - self.tech_type_name, - self.tech_type_description, - self.selected_t_option, - self.selected_t_option_selected, - self.tech_type_processes), - callback=self.tech_type_processes_callback) - else: - self.log(f"Technology Type Options screen returned invalid option: {self.selected_t_option_selected}") - self.exit(416) - return(200) - - async def fetch_technology_types(self) -> int: - self.tech_type_list: list = [{}] - try: - self.autoc = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) - self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - # retrieve the tech type data - self.log(f"Fetching technology type hierarchy for tech_type='*'") - self.tech_type_response = await self.autoc._async_get_tech_type_hierarchy(filter_string = "*" ) - except Exception as e: - self.log(f"Exception in get_tech_type_hierarchy: {e}") - self.log(print_basic_exception(e)) - self.tech_type_list = [{}] - self.exit(416) - return(416) - - self.log (f"tech_type_response: {self.tech_type_response}") - # Copy the data into a working variable for the extraction routine - self.tech_type_list = self.tech_type_response - return(200) - - def unpack_egeria_data(self) -> int: - """ Unpack the data returned from Egeria """ - output_data: list[dict] = [{}] - output_data.clear() - if isinstance(self.tech_type_data, dict): - if "data" in self.tech_type_data: - output_data = self.tech_type_data.get("data") - else: - output_data = [self.tech_type_data] - elif isinstance(self.tech_type_data, list): - for entry in self.tech_type_data: - if isinstance(entry, dict): - output_data = entry.get("data") - elif isinstance(entry, list): - for subentry in entry: - if isinstance(subentry, dict): - output_data = [subentry] - else: - self.log(f"error unknown data structure for Tech Type data") - return(417) - else: - self.log(f"error unknown outer data structure for Tech Type data") - return (417) - else: - self.log(f"Tech Type data not dict or list") - return(417) - - self.log(f"output_data: {output_data}, {type(output_data)}") - # return the extracted data (dict) - self.tech_type_list = output_data - return (200) - - def tech_type_templates_callback(self, result): - """ Callback for Technology Type Templates screen - result contains up to 3 elements: - [0] = return code or 'input', [1] input data, [2] full template""" - # take the input data and use it in the form and - # create from template - self.log(f"Technology Type Templates screen returned: {result}") - # Check for return code - if isinstance(result, int): - self.log(f"Technology Type Templates screen returned: {result}, exiting.") - self.exit(result) - # Check for unexpected return - if not result or result[0] != "input": - self.log(f"Technology Type Templates screen cancelled/failed; return: {result}, exiting.") - self.exit(418) - return - - # make the keys to the input data match the keys in the template structure - # take each key and use the data value to replace the placeholder value in the template - # then use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - # make a working version of the full template if it is returned - if isinstance(result[2], dict): - self.full_template = result[2] - # create an empty dict for the returned data - my_placeholderPropertyValues: dict = {} - # load in the returned placeholder user data - if isinstance(result[1], dict): - self.placeholder_input = result[1] - # clean up the keys of the user data to match the template keys - if isinstance(self.placeholder_input, dict): - # for each data input item in the dict - for input_item, input_value in self.placeholder_input.items(): - self.log(f"input_item: {input_item}, input_value: {input_value}") - input_fix1 = input_item.replace("_", " ") - self.log(f"input_item after underscore removal: {input_fix1}") - input_fix2 = input_fix1.replace(" placeholder input", "") - self.log(f"input_item after placeholder removal: {input_fix2}") - - self.log(f"fixed input_item: {input_fix2}, {input_value}") - # build the placeholderPropertyValues dict - my_placeholderPropertyValues.update({input_fix2: input_value}) - continue - else: - self.log(f"placeholder_input is not a dict: {self.placeholder_input}") - return(419) - - # create a dict structure that matches the template structure - # we have the input data in my_placeholderPropertyValues and - # the selected template in self.full_template - # so we can use the pyegeria create_metadata_element_from template to create the metadata element - # finally display a status screen to confirm the creation of the metadata element - - self.log(f"my_placeholderPropertyValues: {my_placeholderPropertyValues}") - self.log(f"self.placeholder_input: {self.placeholder_input}") - self.log(f"self.full_template: {self.full_template}") - - # copy the retrieved template into the request body - request_body:dict = { - "class" : "TemplateRequestBody", - "externalSourceGUID" : self.full_template.get("externalSourceGUID") or "", - "externalSourceName" : self.full_template.get("externalSourceName") or "", - "typeName" : self.full_template.get("typeName") or "", - "templateGUID" : self.full_template.get("Catalog Template GUID"), - "anchorGUID" : self.full_template.get("anchorGUID"), - "isOwnAnchor" : "false", - "effectiveFrom" : "2026-01-01", - "effectiveTo": "2030-12-31", - "replacementProperties" : self.full_template.get("replacementProperties") or {}, - "placeholderPropertyValues" : {}, - "parentGUID" : None, - "parentRelationshipTypeName" : None, - "parentRelationshipProperties" : None, - "parentAtEnd1" : self.full_template.get("parentAtEnd1") or True, - "effectiveTime" : self.full_template.get("effectiveTime") or datetime.datetime.now().isoformat(), - } - self.log(f"request_body: {request_body}") - # update the request body with the user provided input - for key, value in my_placeholderPropertyValues.items(): - request_body["placeholderPropertyValues"].update({key: value}) - continue - self.log(f"request_body after update: {request_body}") - # now instantiate and call the function to create the element from the template - try: - tokendata = self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) - my_md_instance = MetadataExpert(self.view_server, self.platform_url, self.user_name, self.user_password, tokendata) - # new_guid = my_md_instance.create_metadata_element_from_template(self.template_guid, body = request_body) - new_guid = my_md_instance.create_metadata_element_from_template(body=request_body) - except Exception as e: - self.log(f"Exception in create_element_from_template: {e}") - if isinstance(e, PyegeriaException): - self.log(print_basic_exception(e)) - else: - self.log(f"Exception in create_element_from_template: {e}") - self.push_screen(StatusScreen(f"Error creating element from Template/n{print_basic_exception(e)}"), callback=self.status_callback) - return(420) - - self.log(f"new_guid: {new_guid}") - self.push_screen(StatusScreen(f"Element created from Template/Metadata element created with GUID: '{new_guid}'"), callback=self.status_callback) - - def status_callback(self, status_callback_rc) -> None: - """ Callback routine from the status screen, - this screen is typically displayed at the end of a process - to indicate success or failure of the process.""" - - self.log(f"Status screen returned: {status_callback_rc}") - self.exit(status_callback_rc) - - def tech_type_processes_callback(self, result): - """ Callback for Technology Type Processes screen""" - # take the input data and use it to run the command/process - pass - - def shop_for_data_callback(self, result): - """ Callback for Shop For Data screen""" - selection_type = result[0] - selection_parm_1 = result[1] - selection_parm_2 = result[2] - - if isinstance(selection_type, int): - if selection_type == 200: - return(200) - else: - self.log(f"Shop For Data screen returned: {selection_type}, exiting.") - self.exit(selection_type) - - if selection_type == "dictionary": - self.log(f"Selected dictionary with qualified name: {selection_parm_1}") - self.build_dictionary_details(selection_parm_1, selection_parm_2) - elif selection_type == "domain": - self.log(f"Selected business domain with qualified name: {selection_parm_1}") - self.build_domain_details(selection_parm_1, selection_parm_2) - elif selection_type == "catalog": - self.log(f"Selected catalog with qualified name: {selection_parm_1}") - self.build_catalog_details(selection_parm_1, selection_parm_2) - elif selection_type == "glossary": - self.log(f"Selected glossary with qualified name: {selection_parm_2}") - self.build_glossary_details(selection_parm_1, selection_parm_2) - elif selection_type == "specification": - self.log(f"Selected data specification with qualified name: {selection_parm_2}") - self.build_data_specification_details(selection_parm_1, selection_parm_2) - else: - self.log(f"Unknown selection type: {selection_type}") - self.exit(429) - - @work(thread=True, exclusive=True) - def build_dictionary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a dictionary details screen""" - self.log(f"Building dictionary details for qualified name: {target_qualified_name}") - self.dictionary_qualified_name = target_qualified_name - self.dictionary_display_name = target_display_name - build_structure = {} - - try: - self.dictionary_details = exec_report_spec(format_set_name="Data-Dictionaries", - output_format="DICT", - params={"search_string": self.dictionary_qualified_name, "filter_string": self.dictionary_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving dictionary details: {e!s}") - self.exit(420) - return (420) - - if not self.dictionary_details or self.dictionary_details == None: - error_category = "Dictionary Details" - error_message = "No dictionary details found" - self.log(f"Error retrieving dictionary details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.dictionary_details.get("kind") == "empty": - dictionary_tree: Tree = Tree(label="Empty Dictionary", id="dictionary_details") - dictionary_tree.root.expand() - dictionary_tree.root.content = "No dictionary terms found for this dictionary" - else: - dictionary_tree: Tree = Tree(label=self.dictionary_display_name, id="dictionary_details") - dictionary_tree.root.expand() - dictionary_tree.auto_expand = True - self.dictionary_details_data = self.dictionary_details.get("data") - for term in self.dictionary_details_data: - term_qualified_name = term.get("Qualified Name") or "" - term_subject = term.get("Subject Area") or "" - term_summary = term.get("Summary") or "" - # create dict structure for loading the tree - build_structure.update({term_subject: [{term_qualified_name: term_summary}]}) - continue - # Once the structure is complete we can build the tree from it - for term_subject in build_structure: - dictionary_branch = dictionary_tree.add(Tree(label=term_subject, id=term_subject)) - for term_qualified_name, term_summary in build_structure[term_subject]: - dictionary_branch.add_leaf(Tree(label=term_summary, id=term_summary, data=term_qualified_name)) - dictionary_tree.root.expand() - self.push_screen(SelectionOverviewScreen("dictionary", - self.view_server, - self.platform_url, - self.user_name, - self.user_password), callback=self.overview_callback) - - @work(thread=True, exclusive=True) - def build_domain_details(self, target_qualified_name, target_type__name): - """ Build the details object for a business domain details screen""" - self.log(f"Building domain details for qualified name: {target_qualified_name}") - self.domain_qualified_name = target_qualified_name - self.domain_type__name = target_type__name - build_structure = {} - - try: - self.domain_details = exec_report_spec(format_set_name="BusinessCapabilities", - output_format="DICT", - params={"search_string": self.domain_qualified_name, "filter_string": self.domain_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving business domain details: {e!s}") - self.exit(420) - return (420) - - if not self.domain_details or self.domain_details == None: - error_category = "Business Domain Details" - error_message = "No domain details found" - self.log(f"Error retrieving business domain details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.domain_details.get("kind") == "empty": - domain_tree: Tree = Tree(label="Empty Business Domain", id="domain_details") - domain_tree.root.expand() - domain_tree.root.content = "No domain details found for this business domain" - else: - self.domain_details_data = self.domain_details.get("data") - self.domain_display_name = self.domain_details_data.get("displayName") - domain_tree: Tree = Tree(label=self.domain_display_name, id="domain_details") - domain_tree.root.expand() - domain_tree.auto_expand = True - self.domain_details_data = self.domain_details.get("data") - for term in self.domain_details_data: - if term == None: - continue - term_qualified_name = term.get("Qualified Name") or "" - term_subject = term.get("Subject Area") or "" - term_summary = term.get("Summary") or "" - # create dict structure for loading the tree - build_structure.update({term_subject: [{term_qualified_name: term_summary}]}) - continue - # Once the structure is complete we can build the tree from it - for term_subject in build_structure: - domain_branch = domain_tree.root.add(Tree(label=term_subject, id=term_subject)) - for term_qualified_name, term_summary in build_structure[term_subject]: - domain_branch.add_leaf(Tree(label=term_summary, id=term_summary, data=term_qualified_name)) - domain_tree.root.expand() - self.push_screen(SelectionOverviewScreen("domain", - self.view_server, - self.platform_url, - self.user_name, - self.user_password), callback=self.overview_callback) - - @work(thread=True, exclusive=True) - def build_catalog_details(self, target_qualified_name, target_display_name): - """ Build the details object for a catalog details screen""" - self.log(f"Building catalog details for qualified name: {target_qualified_name}") - self.catalog_qualified_name = target_qualified_name - self.catalog_display_name = target_display_name - build_structure = {} - - try: - self.catalog_details = exec_report_spec(format_set_name="Digital-Product-Catalog", - output_format="DICT", - params={"search_string": self.catalog_qualified_name, "filter_string": self.catalog_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving catalog details: {e!s}") - self.exit(420) - return (420) - - if not self.catalog_details or self.catalog_details == None: - error_category = "Catalog Details" - error_message = "No catalog details found" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.catalog_details.get("kind") == "empty": - catalog_tree: Tree = Tree(label="Empty Catalog", id="catalog_details") - catalog_tree.root.expand() - catalog_tree.root.content = "No catalog terms found for this catalog" - else: - catalog_tree: Tree = Tree(label=self.catalog_display_name, id="catalog_details") - catalog_tree.root.expand() - catalog_tree.auto_expand = True - self.catalog_details_data = self.catalog_details.get("data") - - if not self.catalog_details_data or self.catalog_details_data == None: - error_category = "Catalog Details" - error_message = "No catalog details found or the data dict entry is missing" - self.log(f"Error retrieving catalog details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - - for term in self.catalog_details_data: - term_qualified_name = term.get("Qualified Name") or "" - term_subject = term.get("Subject Area") or "" - term_summary = term.get("Summary") or "" - # create dict structure for loading the tree - build_structure.update({term_subject: [{term_qualified_name: term_summary}]}) - continue - # Once the structure is complete we can build the tree from it - for term_subject in build_structure: - catalog_branch = catalog_tree.root.add(Tree(label=term_subject, id=term_subject)) - for term_qualified_name, term_summary in build_structure[term_subject]: - catalog_branch.add_leaf(Tree(label=term_summary, id=term_summary, data=term_qualified_name)) - catalog_tree.root.expand() - self.push_screen(SelectionOverviewScreen("catalog", - self.view_server, - self.platform_url, - self.user_name, - self.user_password), callback=self.overview_callback) - - @work(thread=True, exclusive=True) - def build_glossary_details(self, target_qualified_name, target_display_name): - """ Build the details object for a glossary details screen""" - self.log(f"Building glossary details for qualified name: {target_qualified_name}") - self.glossary_qualified_name = target_qualified_name - self.glossary_display_name = target_display_name - build_structure: dict = {} - try: - self.glossary_details = exec_report_spec(format_set_name="Glossary-Terms", - output_format="JSON", - params={"search_string": self.glossary_qualified_name, "filter_string": self.glossary_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except PyegeriaException as e: - print_basic_exception(e) - self.log(f"Error retrieving glossary details: {e!s}") - self.exit(420) - return (420) - - self.log(f"glossary_details: {self.glossary_details}") - - if not self.glossary_details or self.glossary_details == None: - error_category = "Glossary Details" - error_message = "No glossary details found" - self.log(f"Error retrieving glossary details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - elif self.glossary_details.get("kind") == "empty": - glossary_tree: Tree = Tree(label="Empty Glossary", id="glossary_details_tree") - glossary_tree.root.expand() - glossary_tree.root.content = "No glossary terms found for this glossary" - else: - glossary_tree: Tree = Tree(label=self.glossary_display_name, id="glossary_details_tree") - glossary_tree.root.expand() - glossary_tree.auto_expand = True - - self.glossary_details_data = self.glossary_details.get("data") - - if not self.glossary_details_data or self.glossary_details_data == None: - error_category = "Glossary Details" - error_message = "No glossary details found or the data dict entry is missing" - self.log(f"Error retrieving glossary details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - else: - for term in self.glossary_details_data: - if term == None: - continue - term_qualified_name = term.get("Qualified Name") or "" - term_subject = term.get("Subject Area") or "" - term_summary = term.get("Summary") or "" - # create dict structure for loading the tree - build_structure.update({term_subject: [{term_qualified_name: term_summary}]}) - continue - - # Once the structure is complete we can build the tree from it - for term_subject in build_structure: - glossary_branch = glossary_tree.root.add(Tree(label=term_subject, id=term_subject)) - for term_qualified_name, term_summary in build_structure[term_subject]: - glossary_branch.add_leaf(Tree(label=term_summary, id=term_summary, data=term_qualified_name)) - glossary_tree.root.expand() - - self.push_screen(SelectionOverviewScreen("glossary", self.view_server, self.platform_url, self.user_name, self.user_password), callback=self.overview_callback) - - def build_data_specification_details(self, target_qualified_name, target_display_name): - """ Build the details object for a data specification details screen""" - self.log(f"Building data specification details for qualified name: {target_qualified_name}") - self.data_specification_qualified_name = target_qualified_name - self.data_specification_display_name = target_display_name - - try: - self.data_specification_details = exec_report_spec(format_set_name="Data-Specifications", - output_format="JSON", - params={"search_string": self.data_specification_qualified_name, "filter_string": self.data_specification_qualified_name}, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user_name, - user_pass=self.user_password) - except Exception as e: - self.log(f"Error retrieving data specification details: {e}") - return - - if not self.data_specification_details or self.data_specification_details == None or self.data_specification_details.get("kind") == "empty": - self.log(f"No data specification details found for qualified name: {self.data_specification_qualified_name}") - error_category = "Data Specification Details" - error_message = "No data specification details found" - self.log(f"Error retrieving data specification details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - - self.log(f"Data specification details retrieved successfully for qualified name: {self.data_specification_qualified_name}") - self.log(f"Data specification details: {self.data_specification_details}") - - data_spec_tree: Tree = Tree(label=self.data_specification_display_name, id="data_specification_details") - data_spec_tree.root.expand() - data_spec_tree.auto_expand = True - self.data_specification_details_data = self.data_specification_details.get("data") - self.log(f"Data specification details data: {self.data_specification_details_data}") - - if not self.data_specification_details_data or self.data_specification_details_data == None: - error_category = "Data Specification Details" - error_message = "No data specification details found or the data dict entry is missing" - self.log(f"Error retrieving data specification details: {error_category}, {error_message}") - self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - return - - specified_id = 0 - for spec in self.data_specification_details_data: - spec_qualified_name = spec.get("properties", {}).get("qualifiedName") or "" - spec_type = spec.get("properties", {}).get("typeName") or "" - spec_description = spec.get("properties", {}).get("description") or "" - spec_url = spec.get("properties", {}).get("URL") or "" - spec_display_name = spec.get("properties", {}).get("displayName") or "" - spec_fixed_label = spec_display_name.replace(" ", "") - spec_fixed_label = spec_fixed_label.replace(":", "") - self.log(f"Creating tree node for spec: {spec_fixed_label}, with id: {"id"+str(specified_id)}") - spec_branch = data_spec_tree.root.add(Tree(label=spec_fixed_label, id="id"+str(specified_id), data=[(spec_display_name, spec_qualified_name, spec_type, spec_description, spec_url)])) - spec_branch.expand() - data_spec_tree.root.expand() - specified_id +=1 - self.push_screen(SelectionOverviewScreen("specification", self.view_server, self.platform_url, self.user_name, self.user_password), callback=self.overview_callback) - - def overview_callback(self): - """Callback function for handling overview screen actions.""" - pass - -if __name__ == "__main__": - app = MyProfileTui() - app.run() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/report_spec.py b/my_egeria/my_egeria/DemoCode/Deprecated/report_spec.py deleted file mode 100644 index 64a40b90..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/report_spec.py +++ /dev/null @@ -1,177 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import os - -from pydantic import ValidationError -from textual import on -from textual.app import ComposeResult, App -from textual.containers import Container, Horizontal, Grid, Vertical -from textual.widgets import Static, ListView, ListItem, Button, Footer, Header - -from pyegeria.base_report_formats import report_spec_list -from pyegeria.format_set_executor import exec_report_spec - -from report_spec_splash_screen import SplashScreen -from report_spec_details import ReportSpecDetails - -CSS_PATH = ["report_specs.tcss"] - -class ReportSpec(App): - - SCREENS = { - "splash": SplashScreen, - "spec_details": ReportSpecDetails, - } - - BINDINGS = [ - ("q", "quit", "Quit"), - ("escape", "back", "Back"), - ] - - # CSS_PATH = ["report_specs.tcss"] - - class GridChildrenApp(App): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def compose(self): - self.query_one("#spec_grid", Grid).mount() - return - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - self.selected_report_spec: str = "" - self.items: list = [] - - def compose(self) -> ComposeResult: - self.heading = "Report Specs" - self.subheading = "Select a report spec to execute:" - self.description = "Select a report spec to execute." - self.report_spec_list = report_spec_list() - self.log(f"report_spec_list: {self.report_spec_list}, type: {type(self.report_spec_list)}") - # Split the report spec string into individual report spec names and insert into ListView - self.report_specs_listview: ListView = ListView() - self.report_specs_listview.id = "report_specs_listview" - for item in self.report_spec_list: - self.items.append(ListItem(Static(f"{item}"))) - - yield Header(show_clock=True) - yield Vertical( - Static(f"{self.subheading}, {self.description}"), - Static(f"Server: {self.view_server} | Platform: {self.platform_url} | User: {self.user}"), - id="connection_info") - yield Container( - Static(f"Start of Report Specification List:", id="report_start"), - ListView(*self.items, id="report_specs_listview"), - Static(f"End of Report Specification List:", id = "report_end"), - id = "report_specs_listview_container", - ) - yield Horizontal( - Button("Quit", id="quit"), - Button("Back", id="back"), - id="action_row" - ) - yield Footer() - def on_mount(self) -> None: - # Apply heights after DOM is built - self.query_one("#connection_info", Vertical).styles.height = "10%" - self.query_one("#report_specs_listview_container", Container).styles.height = "80%" - self.query_one("#action_row", Horizontal).styles.height = "10%" - self.push_screen("splash") - - - def on_splash_screen_splash_continue(self, Message) -> None: - # continue button pressed on the splash screen, pop the splash screen - self.pop_screen() - - def on_list_view_selected(self, event: ListView.Selected): - """ Handle ListView selection event """ - selected_item = event.item.query_one(Static).content - self.log(f"Selected item: {selected_item} type: {type(selected_item)}") - selected_item_text = selected_item - self.log(f"Selected item text: {selected_item_text}, type: {type(selected_item_text)}") - self.selected_report_spec = selected_item_text - self.log(f"Selected report spec: {self.selected_report_spec}") - self.execute_selected_report_spec() - - @on(Button.Pressed, "#quit") - def handle_button_pressed(self, event: Button.Pressed) -> None: - """ Quit the application gracefully with a "good" return code (200) """ - self.log(f"Quit button clicked") - self.exit(200) - return - - @on(Button.Pressed, "#back") - def handle_back_button_pressed(self, event: Button.Pressed) -> None: - """ Return to the main menu screen """ - self.log(f"Back button clicked") - self.report_spec_list = "" - self.selected_report_spec = "" - self.items.clear() - self.switch_screen("splash") - - def execute_selected_report_spec(self): - # execute the selected report spec - self.log(f"Executing report spec: {self.selected_report_spec}") - try: - reponse = exec_report_spec(format_set_name=self.selected_report_spec, - output_format="DICT", - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password) - self.log(f"Return from exec_report_spec:") - self.log(f"reponse: {reponse}") - except (ValidationError) as e: - self.log(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - self.log(f"Exception: {e}") - reponse = {"error": f"Exception: {e}"} - # self.display_response(reponse) - spec_details = ReportSpecDetails(reponse) - self.push_screen(spec_details) - - def on_report_spec_report_details_back(self, Message) -> None: - """ Return to the main menu screen """ - self.pop_screen() - - def on_report_spec_report_details_quit(self, Message) -> None: - """ Quit the application gracefully with a "good" return code (200) """ - self.exit(200) - - def on_report_spec_report_details_continue(self, Message) -> None: - """ Return to the main menu screen """ - self.pop_screen() - -if __name__ == "__main__": - try: - import pydevd_pycharm - pydevd_pycharm.settrace( - "127.0.0.1", # Ensure it's localhost since your app runs on the same machine as PyCharm - port=5679, # Choose an available port - stdout_to_server=True, - stderr_to_server=True, - suspend=False # Set to False to let the application continue running after starting - ) - except ImportError: - print("pydevd-pycharm not installed or setup failed. Debugger won't be active.") - except Exception as e: - pass - - app = ReportSpec() - os.environ.setdefault("EGERIA_PLATFORM_URL", "https://127.0.0.1:9443") - os.environ.setdefault("EGERIA_VIEW_SERVER", "qs-view-server") - os.environ.setdefault("EGERIA_USER", "erinoverview") - os.environ.setdefault("EGERIA_USER_PASSWORD", "secret") - app.run() diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/report_spec_details.py b/my_egeria/my_egeria/DemoCode/Deprecated/report_spec_details.py deleted file mode 100644 index 26e53c05..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/report_spec_details.py +++ /dev/null @@ -1,192 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import time -from datetime import datetime -from typing import Optional -from textual import on -from textual.app import ComposeResult -from textual.containers import Horizontal, Container -from textual.message import Message -from textual.screen import ModalScreen -from textual.widgets import Static, ListView, Button, Footer, Header -from textual.reactive import reactive -from textual.widgets._data_table import DataTable - -CSS_PATH = ["report_specs.tcss"] - -class ReportSpecDetails(ModalScreen): - """Screen to display report specification details.""" - - message = reactive("Loop is stopped") - _interval_task = None # To store the interval task handle - - class ReportDetailsContinue(Message): - """Message to reeturn to the previous screen.""" - pass - - class ReportDetailsQuit(Message): - """Message to terminate application gracefully.""" - def __init__(self, return_code:Optional[int]=200): - super().__init__() - self.return_code = return_code - - class ReportDetailsBack(Message): - """Message to return to the previous screen.""" - pass - - def __init__(self, response,**kwargs): - super().__init__(**kwargs) - # response = details from pyegeria call - self.response = response - """create a DataTable of key/value pairs""" - self.spec_datatable: DataTable = DataTable() - self.log(f"self.spec_datatable : {self.spec_datatable} has been created") - self.spec_datatable.id = "spec_data_table" - self.log(f"self.spec_datatable has been assigned an id : {self.spec_datatable.id} ") - self.spec_datatable.add_columns("Key", "Value", "Description") - self.log(f"self.spec_datatable has been assigned columns {self.spec_datatable.columns}") - self.spec_datatable.border = True - self.spec_datatable.zebra_stripes = True - self.log(f"DataTable created and configured {self.spec_datatable}, id: {self.spec_datatable.id}") - # instantiate connection data for screen - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - - def compose(self) -> ComposeResult: - # Process response data into the DataTable and mount it on the screen - # self.display_response( self.response) - # Now build the screen content - self.heading = "Report Spec Details" - self.subheading = "Details from executing selected report spec." - yield Header(show_clock=True) - yield Static(f"{self.subheading}") - yield Static( - f"Server: {self.view_server} | Platform: {self.platform_url} | User: {self.user}", - id="detail_connection_info", - ) - yield Container( - Static(f"Start of Report Specification List:", id="report_start"), - self.spec_datatable, - Static(f"End of Report Specification List:", id="report_end"), - id="report_details") - yield Horizontal( - Button("Quit", id="quit"), - Button("Back", id="back"), - Button("continue", id="continue"), - id="action_row" - ) - yield Footer() - - def on_mount(self) -> None: - """ On mount process the data and load the datatable""" - # populate table data - self.display_response() - - def display_response(self): - """process reesponse data into a DataTable for display""" - response_data: list[dict] = self.response.get("data") - self.log(f"response_data: {response_data}") - self.log(f"Find address of DataTable") - # self.spec_datatable = self.query_one("#spec_data_table", DataTable) - - if not response_data or response_data == None or response_data == "": - response_data = [{"NoData": "No data found for selected item"}] - if isinstance(response_data, list): - for list_item in response_data: - response_data: dict = list_item - self.log(f"list_item: {list_item}") - if isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_datatable.add_row(key_str, value_str) - continue - else: - self.log(f"else: key {key} value: {value}") - self.spec_datatable.add_row(key, value) - continue - if isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.spec_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.spec_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_datatable.add_row(key_str, value_str) - continue - else: - self.spec_datatable.add_row(key, value) - continue - self.log(f"Start Refresh DataTable task") - if self._interval_task is None: - # Set an interval to call update_message every 1 second - self._interval_task = self.set_interval(5, self.update_message) - self.notify("Loop started!") - else: - self.notify("Loop is already running.") - - # self.spec_datatable.refresh() - return - - def update_message(self) -> None: - """A function to be called repeatedly by the interval.""" - now = datetime.now() - current_time_str = now.strftime("%H:%M:%S") - self.message = f"Loop is running! Current time: {current_time_str}" - self.spec_datatable.refresh() - - def action_stop_loop(self) -> None: - if self._interval_task is not None: - # Cancel the interval task - self._interval_task.stop() - self._interval_task = None - self.message = "Loop is stopped." - self.notify("Loop stopped!") - else: - self.notify("Loop is already stopped.") - - @on(Button.Pressed, "#continue") - def handle_continue(self, event: Button.Pressed) -> None: - """ handler for continue button""" - self.action_stop_loop() - self.post_message(self.ReportDetailsContinue()) - - @on(Button.Pressed, "#quit") - def handle_quit(self, event: Button.Pressed) -> None: - """ handler for quit button""" - self.action_stop_loop() - self.post_message(self.ReportDetailsQuit()) - - @on(Button.Pressed, "#back") - def handle_back(self, event: Button.Pressed) -> None: - """ handler for back button""" - self.action_stop_loop() - self.post_message(self.ReportDetailsBack()) \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/report_spec_splash_screen.py b/my_egeria/my_egeria/DemoCode/Deprecated/report_spec_splash_screen.py deleted file mode 100644 index f46cba68..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/report_spec_splash_screen.py +++ /dev/null @@ -1,119 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import Container -from textual.message import Message -from textual.screen import ModalScreen -from textual.widgets import Static, Label, Button, Footer, Header, TextArea - -CSS_PATH = ["report_specs_splash.tcss"] - -class SplashScreen(ModalScreen): - """ Egeria Exemplar UI Splash screen """ - - class SplashContinue(Message): - """ Message to indicate user is ready to continue to application""" - def __init__(self): - super().__init__() - - - def __init__(self) -> None: - super().__init__() - self.app_title = "My Egeria" - self.app_version = "6.0.0" - self.app_build_date = "2025-09-08" - self.app_build_time = "00:00" - self.app_build_author = "Peter Coldicott" - self.app_build_commit = "00000000000000000" - self.app_build_branch = "main" - self.app_build_platform = "MacOS" - self.welcome_text = ( - "\n\n" - "This is example UI code package that leverages the Textual/Rich open source UI Frameworks,\n" - "and the pyegeria package which is part of the Egeria Project.\n\n" - "The UI is written in Python and is certainly not meant to demonstrate best coding practices!\n\n" - "Textual/Rich frameworks originally authored by Will McGugan (Textualize).\n\n" - "My_Egeria SPDX-License-Identifier: Apache-2.0, " - "Copyright Contributors to the ODPi Egeria project.\n" - ) - - def compose(self) -> ComposeResult: - top = Container( - Label( - f"Welcome to {self.app_title} v{self.app_version} " - f"({self.app_build_date} {self.app_build_time})", - id="splash_title", - ), - TextArea(self.welcome_text, id="splash_text"), - Label( - f"Build Author: {self.app_build_author} | " - f"Commit: {self.app_build_commit} | " - f"Branch: {self.app_build_branch} | " - f"Platform: {self.app_build_platform}", - id="splash_meta", - ), - id="splash_top" - ) - yield Header(show_clock=True) - yield Static("Welcome to the Egeria Exemplar UI!") - yield top - yield Button("Continue", variant="primary", id="continue_splash") - yield Footer() - - async def on_mount(self): - - # Place content in top half, center horizontally - top = self.query_one("#splash_top", Container) - - top.styles.dock = "top" - top.styles.height = "50%" - top.styles.width = "100%" - top.styles.padding = (1, 2) - top.styles.align_horizontal = "center" - top.styles.align_vertical = "top" - top.styles.gap = 1 - - title = self.query_one("#splash_title", Label) - title.styles.text_align = "center" - title.styles.text_style = "bold" - - # Fixed visible rows for vertical centering math - VISIBLE_ROWS = 15 - - ta = self.query_one("#splash_text", TextArea) - ta.styles.width = "90%" - ta.styles.height = VISIBLE_ROWS # fixed rows to make vertical centering predictable - ta.styles.border = ("solid", "white") # solid white border - ta.styles.text_style = "bold" - ta.styles.padding = 1 - ta.styles.text_align = "center" # horizontal centering of text - - # Vertically center the content by adding top padding lines - raw_text = self.welcome_text.strip("\n") - content_lines = raw_text.splitlines() or [raw_text] - content_rows = len(content_lines) - - # Compute usable rows considering numeric padding - pad_top = int(getattr(ta.styles.padding, "top", 0) or 0) - pad_bottom = int(getattr(ta.styles.padding, "bottom", 0) or 0) - usable_rows = max(VISIBLE_ROWS - (pad_top + pad_bottom), 1) - top_pad = max((usable_rows - content_rows) // 2, 0) - - ta.value = ("\n" * top_pad) + raw_text - - meta = self.query_one("#splash_meta", Label) - meta.styles.text_align = "center" - - btn = self.query_one("#continue_splash", Button) - btn.styles.margin = (1, 0, 0, 0) - - @on(Button.Pressed, "#continue_splash") - def handle_continue_splash(self, event: Button.Pressed) -> None: - self.post_message(self.SplashContinue()) - return diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/report_specs.tcss b/my_egeria/my_egeria/DemoCode/Deprecated/report_specs.tcss deleted file mode 100644 index d7c1eb37..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/report_specs.tcss +++ /dev/null @@ -1,73 +0,0 @@ -/* *********************************************************************************************** */ -/* PDX-License-Identifier: Apache-2.0 */ -/* Copyright Contributors to the ODPi Egeria project. */ -/* */ -/* This file provides a set of report specification related functions for my_egeria. */ -/* */ -/* Unless you are running Microsoft's Studio Product with the Textual Extension installed */ -/* the file will show multiple errors, for example I use the PyCharm IDE for whoich there are */ -/* no extensions for Textual, regarless of the errors the file will run correctly with Textual. */ -/* *********************************************************************************************** */ - - -Screen { - background: $surface; - border: solid $primary; - padding: 1; - height: auto; -} -.box { - height: 100%; - border: solid green; -} -#connection_info { - background: $panel; - color: $text; - border: round $primary; - height: 5%; - width: 1fr; - } -#report_specs_listview_container { - height: 85%; - width: 1fr; - border: round $primary; - color: $text; -} -#report_specs_listview { - height: 90%; - width: 1fr; - border: round $primary; - color: $text; -} -Header { - background: $primary; - color: $text; - border: round $primary; - height: 5%; - width: 1fr; -} -Footer { - background: $primary; - color: $text; - border: round $primary; - height: 5%; - width: 1fr; -} -#action_row { - background: $primary; - color: $text; - border: round $primary; - height: 10%; - width: 1fr; -} -#detail_connection_info { - background: $panel; - color: $text; - border: round $primary; - height: 5%; -} -#report_details { - background: $panel; - color: $text; - height: 90%; -} diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/report_specs_splash.tcss b/my_egeria/my_egeria/DemoCode/Deprecated/report_specs_splash.tcss deleted file mode 100644 index 63e8ff69..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/report_specs_splash.tcss +++ /dev/null @@ -1,45 +0,0 @@ -/*"""*/ -/* PDX-License-Identifier: Apache-2.0*/ -/* Copyright Contributors to the ODPi Egeria project.*/ - -/* This file provides a set of report specification related functions for my_egeria.*/ - -/*"""*/ - -.Screen { - background: $surface; - background-color: rgb(255 255 128); - border: solid $primary; - padding: 1; - height: auto; -} -.box { - height: 100%; - border: solid green; -} -#connection_info { - background: $panel; - color: $text; - border: round $primary; - height: 1fr; - width: 1fr; - } -#splash_title { - height: auto; - width: auto; - color: $primary; - text-align: center; - background: $surface; -} -#splash_meta { - height: auto; - width: auto; - color: $text; - text-align: center; - background: $surface; -} -#splash_top { - height: auto; - width: auto; - color: $text; -} diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/tech_type.tcss b/my_egeria/my_egeria/DemoCode/Deprecated/tech_type.tcss deleted file mode 100644 index 831b069a..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/tech_type.tcss +++ /dev/null @@ -1,72 +0,0 @@ -/* *********************************************************************************************** */ -/* PDX-License-Identifier: Apache-2.0 */ -/* Copyright Contributors to the ODPi Egeria project. */ -/* */ -/* This file provides a set of report specification related functions for my_egeria. */ -/* */ -/* Unless you are running Microsoft's Studio Product with the Textual Extension installed */ -/* the file will show multiple errors, for example I use the PyCharm IDE for whoich there are */ -/* no extensions for Textual, regarless of the errors the file will run correctly with Textual. */ -/* *********************************************************************************************** */ - -.Screen { - background: $surface; - border: solid $primary; - padding: 1; - height: auto; -} -.box { - height: 100%; - border: solid green; -} -#connection_info { - background: $panel; - color: $text; - border: round $primary; - height: 5%; - width: 1fr; - } -#report_specs_listview_container { - height: 85%; - width: 1fr; - border: round $primary; - color: $text; -} -#report_specs_listview { - height: 90%; - width: 1fr; - border: round $primary; - color: $text; -} -Header { - background: $primary; - color: $text; - border: round $primary; - height: 5%; - width: 1fr; -} -Footer { - background: $primary; - color: $text; - border: round $primary; - height: 5%; - width: 1fr; -} -#action_row { - background: $primary; - color: $text; - border: round $primary; - height: 10%; - width: 1fr; -} -#detail_connection_info { - background: $panel; - color: $text; - border: round $primary; - height: 5%; -} -#report_details { - background: $panel; - color: $text; - height: 90%; -} diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_details.py b/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_details.py deleted file mode 100644 index a041c9ec..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_details.py +++ /dev/null @@ -1,72 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -import os - -from pydantic import ValidationError -from textual import on -from textual.app import ComposeResult, App -from textual.containers import Container, Horizontal -from textual.message import Message -from textual.screen import ModalScreen -from textual.widgets import Header, Static, Button, Footer - -CSS_PATH = ["tech_type.tcss"] - -class TechTypeDetails(ModalScreen): - - def __init__(self, response, **kwargs): - super().__init__() - # response = details from calls to pyegeria - self.response = response - # instantiate connection data for screen - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - - class TechTypeContinue(Message): - """ Message to indicate user is ready to continue to application""" - def __init__(self): - super().__init__() - pass - - class TechTypeBack(Message): - """ Message to terminate application gracefully """ - def __init__(self): - super().__init__() - - - class TechTypeQuit(Message): - """ Message to terminate application gracefully """ - def __init__(self): - super().__init__() - pass - -def comnpose(self) -> ComposeResult: - """ Compose the screen """ - self.heading = "Technology Type Details" - self.subheading = "Details from querying selected technology type." - yield Header(show_clock=True) - yield Static(f"{self.subheading}") - yield Static( - f"Server: {self.view_server} | Platform: {self.platform_url} | User: {self.user}", - id="detail_connection_info", - ) - yield Container( - Static(f"Start of Technology Type Details:", id="report_start"), - # self.tech_type_datatable, - Static(f"End of Technology Type Details:", id="report_end"), - id="report_details") - yield Horizontal( - Button("Quit", id="quit"), - Button("Back", id="back"), - Button("continue", id="continue"), - id="action_row" - ) - yield Footer() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_splash.tcss b/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_splash.tcss deleted file mode 100644 index 08e07839..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_splash.tcss +++ /dev/null @@ -1,48 +0,0 @@ -/* *********************************************************************************************** */ -/* PDX-License-Identifier: Apache-2.0 */ -/* Copyright Contributors to the ODPi Egeria project. */ -/* */ -/* This file provides a set of report specification related functions for my_egeria. */ -/* */ -/* Unless you are running Microsoft's Studio Product with the Textual Extension installed */ -/* the file will show multiple errors, for example I use the PyCharm IDE for whoich there are */ -/* no extensions for Textual, regarless of the errors the file will run correctly with Textual. */ -/* *********************************************************************************************** */ - -.Screen { - background: $surface; - background-color: rgb(255 255 128); - border: solid $primary; - padding: 1; - height: auto; -} -.box { - height: 100%; - border: solid green; -} -#connection_info { - background: $panel; - color: $text; - border: round $primary; - height: 1fr; - width: 1fr; - } -#splash_title { - height: auto; - width: auto; - color: $primary; - text-align: center; - background: $surface; -} -#splash_meta { - height: auto; - width: auto; - color: $text; - text-align: center; - background: $surface; -} -#splash_top { - height: auto; - width: auto; - color: $text; -} diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_splash_screen.py b/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_splash_screen.py deleted file mode 100644 index dea29816..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/tech_type_splash_screen.py +++ /dev/null @@ -1,119 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a set of report specification related functions for my_egeria. - -""" -from textual import on -from textual.app import ComposeResult -from textual.containers import Container -from textual.message import Message -from textual.screen import ModalScreen -from textual.widgets import Static, Label, Button, Footer, Header, TextArea - -CSS_PATH = ["tech_type_splash.tcss"] - -class SplashScreen(ModalScreen): - """ Egeria Exemplar UI Splash screen """ - - class SplashContinue(Message): - """ Message to indicate user is ready to continue to application""" - def __init__(self): - super().__init__() - - - def __init__(self) -> None: - super().__init__() - self.app_title = "My Egeria" - self.app_version = "6.0.0" - self.app_build_date = "2025-09-08" - self.app_build_time = "00:00" - self.app_build_author = "Peter Coldicott" - self.app_build_commit = "00000000000000000" - self.app_build_branch = "main" - self.app_build_platform = "MacOS" - self.welcome_text = ( - "\n\n" - "This is example UI code package that leverages the Textual/Rich open source UI Frameworks,\n" - "and the pyegeria package which is part of the Egeria Project.\n\n" - "The UI is written in Python and is certainly not meant to demonstrate best coding practices!\n\n" - "Textual/Rich frameworks originally authored by Will McGugan (Textualize).\n\n" - "My_Egeria SPDX-License-Identifier: Apache-2.0, " - "Copyright Contributors to the ODPi Egeria project.\n" - ) - - def compose(self) -> ComposeResult: - top = Container( - Label( - f"Welcome to {self.app_title} v{self.app_version} " - f"({self.app_build_date} {self.app_build_time})", - id="splash_title", - ), - TextArea(self.welcome_text, id="splash_text"), - Label( - f"Build Author: {self.app_build_author} | " - f"Commit: {self.app_build_commit} | " - f"Branch: {self.app_build_branch} | " - f"Platform: {self.app_build_platform}", - id="splash_meta", - ), - id="splash_top" - ) - yield Header(show_clock=True) - yield Static("Welcome to the Egeria Exemplar UI!") - yield top - yield Button("Continue", variant="primary", id="continue_splash") - yield Footer() - - async def on_mount(self): - - # Place content in top half, center horizontally - top = self.query_one("#splash_top", Container) - - top.styles.dock = "top" - top.styles.height = "50%" - top.styles.width = "100%" - top.styles.padding = (1, 2) - top.styles.align_horizontal = "center" - top.styles.align_vertical = "top" - top.styles.gap = 1 - - title = self.query_one("#splash_title", Label) - title.styles.text_align = "center" - title.styles.text_style = "bold" - - # Fixed visible rows for vertical centering math - VISIBLE_ROWS = 15 - - ta = self.query_one("#splash_text", TextArea) - ta.styles.width = "90%" - ta.styles.height = VISIBLE_ROWS # fixed rows to make vertical centering predictable - ta.styles.border = ("solid", "white") # solid white border - ta.styles.text_style = "bold" - ta.styles.padding = 1 - ta.styles.text_align = "center" # horizontal centering of text - - # Vertically center the content by adding top padding lines - raw_text = self.welcome_text.strip("\n") - content_lines = raw_text.splitlines() or [raw_text] - content_rows = len(content_lines) - - # Compute usable rows considering numeric padding - pad_top = int(getattr(ta.styles.padding, "top", 0) or 0) - pad_bottom = int(getattr(ta.styles.padding, "bottom", 0) or 0) - usable_rows = max(VISIBLE_ROWS - (pad_top + pad_bottom), 1) - top_pad = max((usable_rows - content_rows) // 2, 0) - - ta.value = ("\n" * top_pad) + raw_text - - meta = self.query_one("#splash_meta", Label) - meta.styles.text_align = "center" - - btn = self.query_one("#continue_splash", Button) - btn.styles.margin = (1, 0, 0, 0) - - @on(Button.Pressed, "#continue_splash") - def handle_continue_splash(self, event: Button.Pressed) -> None: - self.post_message(self.SplashContinue()) - return diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/test1.py b/my_egeria/my_egeria/DemoCode/Deprecated/test1.py deleted file mode 100644 index ab75c88c..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/test1.py +++ /dev/null @@ -1,30 +0,0 @@ -from textual.app import App - -from pyegeria import EgeriaTech - -import os -from pyegeria import Egeria, PyegeriaException, EgeriaTech - -class Test1App(App): - - def __init___(self): - # 1. Initialize the Egeria environment variables - view_server = os.environ.get("VIEW_SERVER", "view-server") - url = os.environ.get("EGERIA_VIEW_SERVER_URL", "https://localhost:7443") - user_id = os.environ.get("EGERIA_USER", "peterprofile") - user_pwd = os.environ.get("EGERIA_USER_PASSWORD", "password") - - # 2. Instantiate the EgeriaTech client wrapper - egeria_tech = EgeriaTech(view_server, url, user_id, user_pwd) - egeria_tech.create_egeria_bearer_token() - - # Print every method available in your installed pyegeria client containing 'comment' - comment_methods = [method for method in dir(egeria_tech) if 'comment' in method.lower()] - - print("Available comment methods on egeria_tech:") - for method in comment_methods: - print(f"- {method}") - -if __name__ == "__main__": - app = Test1App() - app.run() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/test_my_profile.py b/my_egeria/my_egeria/DemoCode/Deprecated/test_my_profile.py deleted file mode 100644 index be670296..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/test_my_profile.py +++ /dev/null @@ -1,2639 +0,0 @@ -[ - { - "Full Name": "Mr Gary Geeke", - "Job Title": "IT Infrastructure Lead", - "Employee Number": "199995", - "Employee Type": "FTP", - "User ID": "garygeeke", - "Job Status": null, - "Contact Methods": [ - { - "type": "ContactDetails", - "guid": "c02ed974-ffdf-4b88-a581-4d4a00c83b7b", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorGUID": "d1f4e085-355d-4c0a-9740-6a0873de2de8", - "anchorTypeName": "Person", - "anchorDomainName": "Actor" - } - }, - "name": "Work Email Address", - "displayName": "Work Email Address", - "contactType": "Work Email", - "contactMethodType": "EMAIL", - "contactMethodService": "Email address assigned by Coco Pharmaceuticals.", - "contactMethodValue": "GaryGeeke@Coco-Pharmaceuticals.org", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "6cb9af43-184e-4dfa-854a-1572bcf0fe75", - "typeName": "ContactThrough", - "typeVersion": 1, - "typeDescription": "he contact details associated with an actor profile.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "b3c2b848-b359-490b-9ae3-148b1f9680d2" - }, - "relatedElementAtEnd1": false - } - ], - "Roles": [ - { - "type": "GovernanceRole", - "guid": "a52fadeb-099e-41e7-8cba-085320b796e3", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "LOCAL_COHORT", - "homeMetadataCollectionId": "9905c3cb-94c5-4494-9229-0d6f69c0b842", - "homeMetadataCollectionName": "qs-metadata-store" - }, - "versions": { - "createdBy": "qsstorenpa", - "updatedBy": "qsstorenpa", - "maintainedBy": [ - "qsstorenpa" - ], - "createTime": "2026-08-05T17:13:09.612+00:00", - "updateTime": "2026-08-05T17:13:09.612+00:00", - "version": 1, - "lastRequestId": "d780b90e-a0f0-4c81-a463-babaa17ced66" - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorGUID": "a52fadeb-099e-41e7-8cba-085320b796e3", - "anchorTypeName": "GovernanceRole", - "anchorDomainName": "Actor" - } - }, - "name": "IT Sustainability Champion", - "qualifiedName": "governance-representative::IT-SUSTAINABILITY-CHAMP", - "identifier": "IT-SUSTAINABILITY-CHAMP", - "displayName": "IT Sustainability Champion", - "description": "Provides support and education to Coco Pharmaceuticals employees to help them improve the sustainability of their work.", - "actorRoleGroups": [ - "governance-representative" - ], - "headCountLimitSet": false, - "headCount": 0, - "domainIdentifier": 9, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "7ca35247-7e9a-44d9-bf1a-a667467cf1f4" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Community", - "guid": "16a55429-fd5e-4245-ad2b-039dc2d585d6", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "assignmentType": "Contributor", - "name": "Sustainability Community", - "qualifiedName": "Community::Egeria:ValidMetadataValue::domainIdentifier-(9)", - "displayName": "Sustainability Community", - "description": "Community supporting Sustainability that is lead by the governance domain leader and includes all the people supporting the domain.", - "mission": "To provide a mechanism for communication and coordination of work across Coco Pharmaceuticals that supports this governance domain.", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "c2a0faac-4804-4726-adc3-bf4fc3c230c2" - }, - "relatedElementAtEnd1": false - }, - { - "type": "BusinessCapability", - "guid": "8dc3ff8d-4b2f-40c4-b74e-1b61b2139228", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "BusinessCapability", - "anchorDomainName": "AuthoredReferenceable" - } - }, - "name": "IT", - "qualifiedName": "BusinessArea::IT", - "identifier": "IT", - "displayName": "IT", - "description": "Provision of computing services for Coco Pharmaceuticals.", - "businessCapabilityType": "BUSINESS_AREA", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "95169e5c-7b8d-4cc5-ae9e-aba1a06601d8" - }, - "relatedElementAtEnd1": false - } - ] - }, - { - "type": "GovernanceRole", - "guid": "09f69c89-595d-4aad-9e3a-b32f9935f994", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "LOCAL_COHORT", - "homeMetadataCollectionId": "9905c3cb-94c5-4494-9229-0d6f69c0b842", - "homeMetadataCollectionName": "qs-metadata-store" - }, - "versions": { - "createdBy": "qsstorenpa", - "updatedBy": "qsstorenpa", - "maintainedBy": [ - "qsstorenpa" - ], - "createTime": "2026-08-05T17:13:08.409+00:00", - "updateTime": "2026-08-05T17:13:08.409+00:00", - "version": 1, - "lastRequestId": "d780b90e-a0f0-4c81-a463-babaa17ced66" - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorGUID": "09f69c89-595d-4aad-9e3a-b32f9935f994", - "anchorTypeName": "GovernanceRole", - "anchorDomainName": "Actor" - } - }, - "name": "Governance Officer for IT Infrastructure", - "qualifiedName": "GovernanceRole: Egeria:ValidMetadataValue::domainIdentifier-(4)", - "identifier": "GOV_OFFICER:4", - "displayName": "Governance Officer for IT Infrastructure", - "actorRoleGroups": [ - "governance-officer" - ], - "headCountLimitSet": true, - "headCount": 1, - "domainIdentifier": 4, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "31e287ea-7c70-4ace-997e-4335022df870" - }, - "relatedElementAtEnd1": true - }, - { - "type": "PersonRole", - "guid": "dd13664d-b53e-467a-9f4f-2b60b79d9ec1", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "TeamLeader::Department::3067", - "qualifiedName": "TeamLeader::Department::3067", - "identifier": "TeamLeader::Department::3067", - "actorRoleGroups": [ - "team-leader" - ], - "headCountLimitSet": true, - "headCount": 1, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "c5585f66-7804-4cc3-8614-291634f07ddc" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Team", - "guid": "fa805aa6-e280-455f-a604-dfa169016245", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "Team", - "anchorDomainName": "Actor" - } - }, - "assignmentType": "Leader", - "name": "IT Operations Team", - "qualifiedName": "Department::3067", - "identifier": "3067", - "displayName": "IT Operations Team", - "description": "Team managing shared IT resources for Coco Pharmaceuticals.", - "teamType": "Dept", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "7c5f2806-59ae-46f7-b766-67a5f6bf44e3" - }, - "relatedElementAtEnd1": false - } - ] - }, - { - "type": "PersonRole", - "guid": "e09da92d-ab72-45eb-a714-838571c367f6", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "Creating an IT Systems Inventory project manager", - "qualifiedName": "Project::Creating an IT Systems Inventory:ProjectManager", - "identifier": "CSM-SYSTEMS-INVENTORY:ProjectManager", - "displayName": "Creating an IT Systems Inventory project manager", - "description": "Person responsible for ensuring the project stays on track, meeting its goals with the time and resources budget and reporting status and issues to the sponsor.", - "scope": "Within organization", - "actorRoleGroups": [ - "project-manager" - ], - "headCountLimitSet": true, - "headCount": 1, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "da4c2c0b-f1fa-4fc3-ad29-39f421c05866" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Project", - "guid": "d1c26050-97f6-47af-a632-67220ee973a8", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "projectKinds": [ - { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "37142317-4125-4046-9514-71dc5031563f", - "typeName": "GovernanceProject", - "superTypeNames": [ - "ProjectKind" - ], - "typeVersion": 1, - "typeDescription": "Identifies that a project is rolling out new capability to support the governance program.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "GovernanceProject", - "classificationProperties": { - "typeName": "GovernanceProject" - }, - "name": "GovernanceProject" - } - ], - "assignmentType": "Leader", - "name": "Creating an IT Systems Inventory", - "qualifiedName": "Project::Creating an IT Systems Inventory", - "identifier": "CSM-SYSTEMS-INVENTORY", - "displayName": "Creating an IT Systems Inventory", - "description": "Coco Pharmaceuticals needs an inventory of their IT systems to ensure they are properly maintained and secured. It is also needed for capital planning and budgeting. This project creates the inventory.\n", - "url": "https://egeria-project.org/practices/coco-pharmaceuticals/scenarios/cataloguing-infrastructure/overview/", - "mission": "To ensure all IT systems are defined in the IT Systems inventory so that they can be governed.", - "projectStatus": "Completed", - "priority": 0, - "actualStartDate": "2026-07-23T15:43:32.605+00:00", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "dcbb4298-826e-455e-bed3-9378c3c262b5" - }, - "relatedElementAtEnd1": false - } - ] - }, - { - "type": "PersonRole", - "guid": "27d4d87a-d8d4-4a18-b0f1-c327cbe9c9e0", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "Define the security strategy for Coco Pharmaceuticals project team", - "qualifiedName": "Project::Define the security strategy for Coco Pharmaceuticals:ProjectTeam", - "identifier": "CSM-STRATEGY:ProjectTeam", - "displayName": "Define the security strategy for Coco Pharmaceuticals project team", - "description": "Project team members that perform the assign tasks to meet the projects goals, paying attention to their use of time and resources as directed by the project leader.", - "scope": "Within organization", - "actorRoleGroups": [ - "team-member" - ], - "headCountLimitSet": false, - "headCount": 0, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "40123619-cf97-4bbb-98b4-17f7a7a227ab" - }, - "relatedElementAtEnd1": true - }, - { - "type": "PersonRole", - "guid": "df0d927f-55a0-4e65-b5fd-7e424bdc275a", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "Develop cyber attack response project team", - "qualifiedName": "Project::Develop cyber attack response:ProjectTeam", - "identifier": "CSM-CYBER-ATTACK-RESPONSE:ProjectTeam", - "displayName": "Develop cyber attack response project team", - "description": "Project team members that perform the assign tasks to meet the projects goals, paying attention to their use of time and resources as directed by the project leader.", - "scope": "Within organization", - "actorRoleGroups": [ - "team-member" - ], - "headCountLimitSet": false, - "headCount": 0, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "c4b1f946-280d-4ef6-8798-ee63d4a937bc" - }, - "relatedElementAtEnd1": true - }, - { - "type": "PersonRole", - "guid": "1ec912f5-3bba-4fc5-a8bf-1689b1182b34", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "Add cyber monitoring and response foundation project team", - "qualifiedName": "Project::Add cyber monitoring and response foundation:ProjectTeam", - "identifier": "CSM-FOUNDATION:ProjectTeam", - "displayName": "Add cyber monitoring and response foundation project team", - "description": "Project team members that perform the assign tasks to meet the projects goals, paying attention to their use of time and resources as directed by the project leader.", - "scope": "Within organization", - "actorRoleGroups": [ - "team-member" - ], - "headCountLimitSet": false, - "headCount": 0, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "c13dd94b-7013-4d9e-8f74-8a902b0686a8" - }, - "relatedElementAtEnd1": true - }, - { - "type": "GovernanceRole", - "guid": "39a1b35d-7cea-43d6-9617-48c50309066a", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "LOCAL_COHORT", - "homeMetadataCollectionId": "9905c3cb-94c5-4494-9229-0d6f69c0b842", - "homeMetadataCollectionName": "qs-metadata-store" - }, - "versions": { - "createdBy": "qsstorenpa", - "updatedBy": "qsstorenpa", - "maintainedBy": [ - "qsstorenpa" - ], - "createTime": "2026-08-05T17:13:09.689+00:00", - "updateTime": "2026-08-05T17:13:09.689+00:00", - "version": 1, - "lastRequestId": "d780b90e-a0f0-4c81-a463-babaa17ced66" - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorGUID": "39a1b35d-7cea-43d6-9617-48c50309066a", - "anchorTypeName": "GovernanceRole", - "anchorDomainName": "Actor" - } - }, - "name": "New York Data Center Leader", - "qualifiedName": "location-owner::NY-DC-LEADER", - "identifier": "NY-DC-LEADER", - "displayName": "New York Data Center Leader", - "description": "Leader responsible for the care and compliance of the New York Data Center.", - "actorRoleGroups": [ - "location-owner" - ], - "headCountLimitSet": true, - "headCount": 1, - "domainIdentifier": 9, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "5eaf0743-f839-4808-aa2c-db702f0ff777" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Community", - "guid": "16a55429-fd5e-4245-ad2b-039dc2d585d6", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "assignmentType": "Contributor", - "name": "Sustainability Community", - "qualifiedName": "Community::Egeria:ValidMetadataValue::domainIdentifier-(9)", - "displayName": "Sustainability Community", - "description": "Community supporting Sustainability that is lead by the governance domain leader and includes all the people supporting the domain.", - "mission": "To provide a mechanism for communication and coordination of work across Coco Pharmaceuticals that supports this governance domain.", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "831c5869-de7a-4b80-83cc-4ab148147755" - }, - "relatedElementAtEnd1": false - }, - { - "type": "Location", - "guid": "f0b49b0a-9e15-4c60-9c34-5ffde01bfe13", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "glossaryTermKinds": [ - { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "bc111963-80c7-444f-9715-946c03142dd2", - "typeName": "FixedLocation", - "typeVersion": 1, - "typeDescription": "A location linked to a physical place.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "FixedLocation", - "classificationProperties": { - "typeName": "FixedLocation" - }, - "name": "FixedLocation" - } - ], - "locationKinds": [ - { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "bc111963-80c7-444f-9715-946c03142dd2", - "typeName": "FixedLocation", - "typeVersion": 1, - "typeDescription": "A location linked to a physical place.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "FixedLocation", - "classificationProperties": { - "typeName": "FixedLocation" - }, - "name": "FixedLocation" - } - ], - "name": "New York Data Center", - "qualifiedName": "Data centre::NY-DC", - "identifier": "NY-DC", - "displayName": "New York Data Center", - "description": "", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "811b9361-10fb-4c0f-9fce-19b89d300120" - }, - "relatedElementAtEnd1": false - } - ] - }, - { - "type": "PersonRole", - "guid": "487b15ca-971a-473d-b866-095da528cd7f", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "TeamLeader::Department::0013", - "qualifiedName": "TeamLeader::Department::0013", - "identifier": "TeamLeader::Department::0013", - "actorRoleGroups": [ - "team-leader" - ], - "headCountLimitSet": true, - "headCount": 1, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "4d92db99-e3a5-4aab-bdfb-e89fe9f4cd33" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Team", - "guid": "d43c6582-4d52-4d1f-b032-c545a84ffefd", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "Team", - "anchorDomainName": "Actor" - } - }, - "assignmentType": "Leader", - "name": "Austin Information Technology Team", - "qualifiedName": "Department::0013", - "identifier": "0013", - "displayName": "Austin Information Technology Team", - "description": "Team managing IT resources for Austin manufacturing.", - "teamType": "Dept", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "c050524b-2e8d-4b8a-a631-922c77f98124" - }, - "relatedElementAtEnd1": false - } - ] - }, - { - "type": "PersonRole", - "guid": "ae2ee55d-8aee-431a-bee2-ae753275afd0", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "Review user audit project team", - "qualifiedName": "Project::Review user audit:ProjectTeam", - "identifier": "CSM-USER-AUDIT:ProjectTeam", - "displayName": "Review user audit project team", - "description": "Project team members that perform the assign tasks to meet the projects goals, paying attention to their use of time and resources as directed by the project leader.", - "scope": "Within organization", - "actorRoleGroups": [ - "team-member" - ], - "headCountLimitSet": false, - "headCount": 0, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "38c3ee50-b840-4dca-807e-d8adccb0ff5a" - }, - "relatedElementAtEnd1": true - }, - { - "type": "PersonRole", - "guid": "1517fc1c-37f5-4414-9d7b-b32b02ea808f", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "TeamMembers::Department::0004", - "qualifiedName": "TeamMembers::Department::0004", - "identifier": "TeamMembers::Department::0004", - "actorRoleGroups": [ - "team-member" - ], - "headCountLimitSet": true, - "headCount": 3, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "bf34536a-4cfc-49fb-b214-843d63df13e6" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Team", - "guid": "3f6aaef5-f23e-43cf-94f0-797a10f5f290", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "Team", - "anchorDomainName": "Actor" - } - }, - "assignmentType": "Contributor", - "name": "Global Information Technology Team", - "qualifiedName": "Department::0004", - "identifier": "0004", - "displayName": "Global Information Technology Team", - "description": "Team managing shared IT resources for Coco Pharmaceuticals.", - "teamType": "Dept", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "8138f23f-531f-4047-8a8c-1b5cee4a8a70" - }, - "relatedElementAtEnd1": false - } - ] - }, - { - "type": "GovernanceRole", - "guid": "6e12d091-c163-4b6c-b320-322eda7fd72b", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "LOCAL_COHORT", - "homeMetadataCollectionId": "9905c3cb-94c5-4494-9229-0d6f69c0b842", - "homeMetadataCollectionName": "qs-metadata-store" - }, - "versions": { - "createdBy": "qsstorenpa", - "updatedBy": "qsstorenpa", - "maintainedBy": [ - "qsstorenpa" - ], - "createTime": "2026-08-05T17:13:09.651+00:00", - "updateTime": "2026-08-05T17:13:09.651+00:00", - "version": 1, - "lastRequestId": "d780b90e-a0f0-4c81-a463-babaa17ced66" - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorGUID": "6e12d091-c163-4b6c-b320-322eda7fd72b", - "anchorTypeName": "GovernanceRole", - "anchorDomainName": "Actor" - } - }, - "name": "London Data Center Leader", - "qualifiedName": "location-owner::LDN-DC-LEADER", - "identifier": "LDN-DC-LEADER", - "displayName": "London Data Center Leader", - "description": "Leader responsible for the care and compliance of the London Data Center.", - "actorRoleGroups": [ - "location-owner" - ], - "headCountLimitSet": true, - "headCount": 1, - "domainIdentifier": 9, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "af7740b0-5aba-4200-9148-64641753cdc8" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Community", - "guid": "16a55429-fd5e-4245-ad2b-039dc2d585d6", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "assignmentType": "Contributor", - "name": "Sustainability Community", - "qualifiedName": "Community::Egeria:ValidMetadataValue::domainIdentifier-(9)", - "displayName": "Sustainability Community", - "description": "Community supporting Sustainability that is lead by the governance domain leader and includes all the people supporting the domain.", - "mission": "To provide a mechanism for communication and coordination of work across Coco Pharmaceuticals that supports this governance domain.", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "5774a9ba-96a9-49a5-a6d6-07df499e3a93" - }, - "relatedElementAtEnd1": false - }, - { - "type": "Location", - "guid": "b16ff53a-dac5-4aab-a60e-3cc100ea7d0f", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "glossaryTermKinds": [ - { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "bc111963-80c7-444f-9715-946c03142dd2", - "typeName": "FixedLocation", - "typeVersion": 1, - "typeDescription": "A location linked to a physical place.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "FixedLocation", - "classificationProperties": { - "typeName": "FixedLocation" - }, - "name": "FixedLocation" - } - ], - "locationKinds": [ - { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "bc111963-80c7-444f-9715-946c03142dd2", - "typeName": "FixedLocation", - "typeVersion": 1, - "typeDescription": "A location linked to a physical place.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "FixedLocation", - "classificationProperties": { - "typeName": "FixedLocation" - }, - "name": "FixedLocation" - } - ], - "name": "London Data Centre", - "qualifiedName": "Data centre::LDN-DC", - "identifier": "LDN-DC", - "displayName": "London Data Centre", - "description": "", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "357d9607-97be-494a-a686-638f17bf85d4" - }, - "relatedElementAtEnd1": false - } - ] - }, - { - "type": "PersonRole", - "guid": "713d0b7a-1f0d-4d10-afdf-549c7762eb4b", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "PersonRole", - "anchorDomainName": "Actor" - } - }, - "name": "TeamLeader::Department::0010", - "qualifiedName": "TeamLeader::Department::0010", - "identifier": "TeamLeader::Department::0010", - "actorRoleGroups": [ - "team-leader" - ], - "headCountLimitSet": true, - "headCount": 1, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "5ebe1290-a1a2-4769-b4f3-c3ff37b2f958" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Team", - "guid": "1c1bec70-7e78-4d1b-a9ca-26a8dc51ac2c", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorTypeName": "Team", - "anchorDomainName": "Actor" - } - }, - "assignmentType": "Leader", - "name": "New York Information Technology Team", - "qualifiedName": "Department::0010", - "identifier": "0010", - "displayName": "New York Information Technology Team", - "description": "Team managing IT resources for New York Site.", - "teamType": "Dept", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "2d71812c-ccce-4925-b355-638c439cfbda" - }, - "relatedElementAtEnd1": false - } - ] - }, - { - "type": "GovernanceRole", - "guid": "52d04cfa-cddd-4a01-9023-ccfb1d1d1f29", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "anchor": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "aa44f302-2e43-4669-a1e7-edaae414fc6e", - "typeName": "Anchors", - "typeVersion": 1, - "typeDescription": "Identifies the anchor entity for an element that is part of a large composite object such as an asset.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "LOCAL_COHORT", - "homeMetadataCollectionId": "9905c3cb-94c5-4494-9229-0d6f69c0b842", - "homeMetadataCollectionName": "qs-metadata-store" - }, - "versions": { - "createdBy": "qsstorenpa", - "updatedBy": "qsstorenpa", - "maintainedBy": [ - "qsstorenpa" - ], - "createTime": "2026-08-05T17:13:09.644+00:00", - "updateTime": "2026-08-05T17:13:09.644+00:00", - "version": 1, - "lastRequestId": "d780b90e-a0f0-4c81-a463-babaa17ced66" - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "Anchors", - "classificationProperties": { - "typeName": "Anchors", - "anchorGUID": "52d04cfa-cddd-4a01-9023-ccfb1d1d1f29", - "anchorTypeName": "GovernanceRole", - "anchorDomainName": "Actor" - } - }, - "name": "Amsterdam Data Center Leader", - "qualifiedName": "location-owner::AMS-DC-LEADER", - "identifier": "AMS-DC-LEADER", - "displayName": "Amsterdam Data Center Leader", - "description": "Leader responsible for the care and compliance of the Amsterdam Data Center.", - "actorRoleGroups": [ - "location-owner" - ], - "headCountLimitSet": true, - "headCount": 1, - "domainIdentifier": 9, - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "4a316abe-bcce-4d11-ad5a-4bfb4079b80b", - "typeName": "PersonRoleAppointment", - "typeVersion": 1, - "typeDescription": "Relationship identifying a person's roles.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "9d5e97d1-56bb-43fc-a92f-e6792025fa4c" - }, - "relatedElementAtEnd1": true, - "nested_elements": [ - { - "type": "Community", - "guid": "16a55429-fd5e-4245-ad2b-039dc2d585d6", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "assignmentType": "Contributor", - "name": "Sustainability Community", - "qualifiedName": "Community::Egeria:ValidMetadataValue::domainIdentifier-(9)", - "displayName": "Sustainability Community", - "description": "Community supporting Sustainability that is lead by the governance domain leader and includes all the people supporting the domain.", - "mission": "To provide a mechanism for communication and coordination of work across Coco Pharmaceuticals that supports this governance domain.", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "6a29af51-386d-4285-a90b-53ff8698041d" - }, - "relatedElementAtEnd1": false - }, - { - "type": "Location", - "guid": "7ade0bba-e5c2-4803-b965-320794955508", - "status": "ACTIVE", - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "version": 1784821412270, - "created_by": "Egeria Project", - "create_time": "2021-12-20T07:20:40.038+00:00", - "updated_by": "Egeria Project", - "update_time": "2021-12-20T07:20:40.038+00:00", - "class": "ElementHeader", - "headerVersion": 0, - "glossaryTermKinds": [ - { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "bc111963-80c7-444f-9715-946c03142dd2", - "typeName": "FixedLocation", - "typeVersion": 1, - "typeDescription": "A location linked to a physical place.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "FixedLocation", - "classificationProperties": { - "typeName": "FixedLocation" - }, - "name": "FixedLocation" - } - ], - "locationKinds": [ - { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "bc111963-80c7-444f-9715-946c03142dd2", - "typeName": "FixedLocation", - "typeVersion": 1, - "typeDescription": "A location linked to a physical place.", - "typeCategory": "CLASSIFICATION_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "classificationOrigin": "ASSIGNED", - "classificationName": "FixedLocation", - "classificationProperties": { - "typeName": "FixedLocation" - }, - "name": "FixedLocation" - } - ], - "name": "Amsterdam Data Centre", - "qualifiedName": "Data centre::AMS-DATA-CENTER", - "identifier": "AMS-DATA-CENTER", - "displayName": "Amsterdam Data Centre", - "description": "Shared systems data located on the Amsterdam Site.", - "relationshipHeader": { - "headerVersion": 0, - "status": "ACTIVE", - "type": { - "typeId": "e3fdafe3-692a-46c6-a595-c538cc189dd9", - "typeName": "AssignmentScope", - "typeVersion": 1, - "typeDescription": "Links a profile, role, or project to the elements that they are responsible for managing.", - "typeCategory": "RELATIONSHIP_DEF" - }, - "origin": { - "sourceServer": "qs-metadata-store", - "originCategory": "CONTENT_PACK", - "homeMetadataCollectionId": "655b1965-4c29-4a0e-8a5d-3f55a37b3799", - "homeMetadataCollectionName": "Coco Pharmaceuticals Combination", - "license": "Apache 2.0" - }, - "versions": { - "createdBy": "Egeria Project", - "updatedBy": "Egeria Project", - "maintainedBy": [ - "Egeria Project" - ], - "createTime": "2021-12-20T07:20:40.038+00:00", - "updateTime": "2021-12-20T07:20:40.038+00:00", - "version": 1784821412270 - }, - "guid": "0ac3fa12-e863-46a1-a99b-cccdbba1b8be" - }, - "relatedElementAtEnd1": false - } - ] - } - ], - "Teams": null, - "Communities": null, - "Projects": null, - "Note Logs": [ - { - "class": "BlogEntryProperties", - "typeName": "BlogEntry", - "qualifiedName": "Blog::Blog-2026-08-11T09:30:35.729346", - "displayName": "Testing Add Blog Entry", - "description": "This is a test of the add blog entry function", - "priority": 0, - "situation": "Testing my profile app", - "time": "2026-08-11T14:30:36.833+00:00", - "title": "Testing Add Blog Entry", - "text": "This is a test of the add blog entry function" - }, - { - "class": "BlogEntryProperties", - "typeName": "BlogEntry", - "qualifiedName": "Blog::Blog-2026-08-11T09:11:57.940914", - "displayName": "Blkog testing", - "description": "Text of test blog entry", - "priority": 0, - "situation": "testing my profile", - "time": "2026-08-11T14:11:59.045+00:00", - "title": "Blkog testing", - "text": "Text of test blog entry" - }, - { - "class": "BlogEntryProperties", - "typeName": "BlogEntry", - "qualifiedName": "Blog::Blog-2026-08-11T09:31:25.745618", - "displayName": "Testing add blog entry 2", - "description": "This is a second test of adding a blog entry", - "priority": 0, - "situation": "Testing my profile app", - "time": "2026-08-11T14:31:26.791+00:00", - "title": "Testing add blog entry 2", - "text": "This is a second test of adding a blog entry" - }, - { - "class": "JournalEntryProperties", - "typeName": "JournalEntry", - "qualifiedName": "garygeeke:Journal 1 on August 11th, 2026:JournalEntry:2026-08-11T09:37:41.114267", - "displayName": "Journal 1 on August 11th, 2026", - "description": "This is my first test of the add journal entry function", - "priority": 0, - "situation": "Testing my profile app", - "time": "2026-08-11T14:37:42.519+00:00", - "title": "Journal 1 on August 11th, 2026", - "text": "This is my first test of the add journal entry function" - }, - { - "class": "JournalEntryProperties", - "typeName": "JournalEntry", - "qualifiedName": "garygeeke:Journal 2 on August 11th, 2026:JournalEntry:2026-08-11T09:38:25.889236", - "displayName": "Journal 2 on August 11th, 2026", - "description": "Testing add journal entry function", - "priority": 0, - "situation": "Testing my profile app", - "time": "2026-08-11T14:38:27.428+00:00", - "title": "Journal 2 on August 11th, 2026", - "text": "Testing add journal entry function" - } - ] - } -] \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Deprecated/testing.yaml b/my_egeria/my_egeria/DemoCode/Deprecated/testing.yaml deleted file mode 100644 index dd0ffe7b..00000000 --- a/my_egeria/my_egeria/DemoCode/Deprecated/testing.yaml +++ /dev/null @@ -1 +0,0 @@ -# Testing file for my_prfile_tui.py \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/Copying_DataTables_between_screens.example.py b/my_egeria/my_egeria/DemoCode/Experimental Code/Copying_DataTables_between_screens.example.py deleted file mode 100644 index 7e7fed00..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/Copying_DataTables_between_screens.example.py +++ /dev/null @@ -1,111 +0,0 @@ -from textual.app import App, ComposeResult -from textual.message import Message -from textual.screen import Screen -from textual.widgets import DataTable, Button, Header, Footer - - -# 1. Define the message payload holding keys and values -class TableSyncRequest(Message): - def __init__(self, columns: list[str], rows: list[tuple[str, list]]) -> None: - super().__init__() - self.columns = columns - # List of tuples: [(row_key_string, [cell1, cell2, ...]), ...] - self.rows = rows - - -# 2. Main/Source Screen -class SourceScreen(Screen): - def compose(self) -> ComposeResult: - yield Header() - yield DataTable(id="source_table") - yield Button("Export Data to Screen B", id="btn_export") - yield Footer() - - def on_mount(self) -> None: - table = self.query_one("#source_table", DataTable) - table.add_columns("Item ID", "Qty", "Status") - - # Adding rows with explicit, specific keys - table.add_row("Widget A", "10", "In Stock", key="key_prod_991") - table.add_row("Widget B", "0", "Backorder", key="key_prod_992") - table.add_row("Widget C", "50", "In Stock", key="key_prod_993") - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn_export": - table = self.query_one("#source_table", DataTable) - - # Extract columns - columns = [col.label.plain for col in table.columns.values()] - - # Extract row keys and data simultaneously - rows_with_keys = [] - for row_key in table.rows: - # row_key.value gets the string representation of the RowKey - rows_with_keys.append((row_key.value, table.get_row(row_key))) - - # Bubble up the data message to the main App orchestrator - self.post_message(TableSyncRequest(columns, rows_with_keys)) - - -# 3. Destination Screen -class DestinationScreen(Screen): - def compose(self) -> ComposeResult: - yield Header() - yield DataTable(id="dest_table") - yield Button("Go Back", id="btn_back") - yield Footer() - - def populate_table(self, columns: list[str], rows: list[tuple[str, list]]) -> None: - table = self.query_one("#dest_table", DataTable) - - # Clear data and previous headers completely - table.clear(columns=True) - table.add_columns(*columns) - - # Re-insert every row explicitly mapped to its original key - for key_str, cell_values in rows: - table.add_row(*cell_values, key=key_str) - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn_back": - self.app.pop_screen() - - -# 4. Main App coordinating the screen routing -class MultiScreenSyncApp(App): - def on_mount(self) -> None: - # Register both screens - self.install_screen(SourceScreen(), name="screen_a") - self.install_screen(DestinationScreen(), name="screen_b") - self.push_screen("screen_a") - - # Intercept data message sent by Screen A - def on_table_sync_request(self, message: TableSyncRequest) -> None: - # Get reference to Screen B instance - screen_b = self.get_screen("screen_b") - - # Push data directly into Screen B's method - screen_b.populate_table(message.columns, message.rows) - - # Switch views - self.push_screen("screen_b") - self.notify("Table copied with keys intact!") - - -if __name__ == "__main__": - MultiScreenSyncApp().run() - - """Key Design Patterns for Keys and Screens - - row_key.value Extraction: Inside Textual, row identifiers are tracking objects (RowKey). - Always call .value during extraction to grab the raw underlying identifier string. - When passed into table.add_row(..., key=key_str) on the new screen, Textual seamlessly - rebuilds a valid matching RowKey tracking reference. - - table.rows.items() Iteration: Iterating directly through .items() yields the RowKey instance - as the dictionary key and the row metadata array as the value. - This ensures you never mismatch a key to a dataset. - - Pre-population on Inactive Screens: Textual lets you pull a screen instance by name using - self.get_screen("screen_name") even if it isn't currently active on the terminal display. - You can inject data into its widgets safely before running self.push_screen().""" \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/Examople_Initiate_GAP.java b/my_egeria/my_egeria/DemoCode/Experimental Code/Examople_Initiate_GAP.java deleted file mode 100644 index 10b8e4f3..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/Examople_Initiate_GAP.java +++ /dev/null @@ -1,33 +0,0 @@ -### -# @name initiateGovernanceActionProcess (Create Subscription) -# Using the named governance action process as a template, initiate a chain of engine actions. - -POST {{baseURL}}/servers/{{viewServer}}/api/open-metadata/automated-curation/governance-action-processes/initiate -Authorization: Bearer {{token}} -Content-Type: application/json - -{ - "processQualifiedName": "GovernanceActionProcess::Valid Value Sets List::Create Subscription::EVALUATION-SUBSCRIPTION" -} - -### -# Full request body for initiate governance action process -# { -# "class" : "InitiateGovernanceActionProcessRequestBody", -# "processQualifiedName": "", -# "requestParameters": { -# "parameterName1" : "parameterValue1", -# "parameterName2" : "parameterValue2" -# }, -# "requestSourceGUIDs": [ "" ], -# "actionTargets": [ -# { -# "class" : "NewActionTarget", -# "actionTargetName" : "", -# "actionTargetGUID" : "" -# } ], -# "startDate": "", -# "originatorServiceName": "", -# "originatorEngineName": "" -# } -#general \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/Exmaple_of_returning_data_in_a_product.java b/my_egeria/my_egeria/DemoCode/Experimental Code/Exmaple_of_returning_data_in_a_product.java deleted file mode 100644 index 4aac0b72..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/Exmaple_of_returning_data_in_a_product.java +++ /dev/null @@ -1,22 +0,0 @@ -### -# @name get Exceptions -# Retrieve the element, extract the guid and then request the data set report. -# -POST {{baseURL}}/servers/{{viewServer}}/api/open-metadata/asset-maker/assets/by-name -Authorization: Bearer {{token}} -Content-Type: application/json - -{ - "class" : "FilterRequestBody", - "metadataElementTypeName": "TabularDataSet", - "filter" : "Data set for Exceptions", - "startFrom" : 0, - "pageSize": 10 -} - -> {% client.global.set("exceptionsDataSetGUID", response.body.elements[0].elementHeader.guid); %} - -### - -GET {{baseURL}}/servers/{{viewServer}}/api/open-metadata/data-engineer/tabular-data-sets/{{exceptionsDataSetGUID}}/report?startFromRow=0&maxRowCount=5000 -Authorization: Bearer {{token}} \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment1.py b/my_egeria/my_egeria/DemoCode/Experimental Code/experiment1.py deleted file mode 100644 index 0c83487f..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment1.py +++ /dev/null @@ -1,290 +0,0 @@ -""" python - - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a utility function for my_egeria. - - This file is being used to test some display layout options - This is a test file to explore different ways to display Egeria data - Author: Peter Coldicott, November 2025 - -""" - -import os - -from pydantic import ValidationError -from pyegeria.base_report_formats import report_spec_list, get_report_format_description, select_report_spec -from pyegeria.format_set_executor import exec_report_spec -from textual import on -from textual.app import App -from textual.containers import Container -from textual.widgets import Static, Button, DataTable, Header, Footer - -class MyApp(App): - CSS_PATH = "experiment1.tcss" - - BINDINGS = [ - ("q", "quit", "Quit"), - ("s", "show report", "Show"), ] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - self.selected_report_spec: str = "" - self.items: list = [] - - def compose(self): - yield Header() - yield Container( - Static("Start of report specification list:", id="one_start"), - DataTable(id="spec_datatable"), - Static("End of report specification list:", id="one_end"), - id="container1", classes="box") - yield Container( - Static("Report Specification Details", classes="box", id="two_start"), - DataTable(id="spec_extracted_datatable"), - Static("End of Report Specification Details", classes="box", id="two_end"), - id="container2", classes="box") - yield Container( - Static("Report Specification Output)", classes="box", id="three_start"), - DataTable(id="spec_output_datatable"), - Static("End of Report Specification Output)", classes="box", id="three_end"), - id="container3", classes="box") - yield Container( - Button("Quit", id="quit"), - id = "action_row" - ) - yield Footer() - - def on_mount(self): - self.spec_datatable = self.query_one("#spec_datatable", DataTable) - self.spec_datatable.clear(columns=True) - self.spec_datatable.add_columns("Name", "Description", "") - self.spec_datatable.zebra_stripes = True - self.spec_datatable.cursor_type = "row" - num_columns = len(self.spec_datatable.columns) - self.log(f"Number of columns: {num_columns}") - self.heading = "Report Specs" - self.subheading = "Select a report spec to execute:" - self.description = "A list of report specifications, click on one to see its attributes\ns key to run" - self.report_spec_list: list = report_spec_list() - self.log(f"report_spec_list: {self.report_spec_list}, type: {type(self.report_spec_list)}") - for spec in self.report_spec_list: - if not isinstance(spec, str): - spec = str(spec) - self.log(f"spec: {spec}") - desc = get_report_format_description(spec) - if not isinstance(desc, str): - desc = str(desc) - self.log(f"desc: {desc}") - self.spec_datatable.add_row(spec, desc) - self.spec_datatable.refresh() - - @on(DataTable.RowSelected, "#spec_datatable") - async def handle_spec_table_row_selected(self, message: DataTable.RowSelected): - self.log(f"Row Selected, Processing selection") - self.selected_report_spec = message.row_key - self.log(f"selected_report_spec: {self.selected_report_spec}") - self.selected_row_data = self.spec_datatable.get_row(message.row_key) - self.log(f"selected_row: {self.selected_row_data}") - self.selected_name = str(self.selected_row_data[0] or "") - self.log(f"selected_name: {self.selected_name}") - await self.get_named_report_spec_details(self.selected_name) - await self.execute_selected_report_spec(self.selected_name) - - async def get_named_report_spec_details(self, name): - """Get the details of a named report spec and render as a flat table. - - Simplified approach: - - Normalize the incoming spec to a dict. - - Recursively walk dicts/lists with a small helper that yields rows. - - Use a path convention where the first segment is shown in "Attribute", - the remaining path (if any) is shown in "Value", and the leaf value in - "Extended Values". This keeps the UI compact while preserving structure. - - """ - - self.spec_name = name - self.log(f"get_named_report_spec_details: {self.spec_name}") - selected_report_spec = select_report_spec(self.spec_name, output_type="DICT") - self.log(f"selected_report_spec: {selected_report_spec}, type: {type(selected_report_spec)}") - - # Normalize the shape of the returned spec → always a dict - if isinstance(selected_report_spec, dict) and "data" in selected_report_spec: - self.extracted_report_spec: dict = selected_report_spec.get("data") or {} - elif isinstance(selected_report_spec, dict): - self.extracted_report_spec: dict = selected_report_spec or {} - else: - error_text = f"Unknown shape: {selected_report_spec}" - self.extracted_report_spec = {"Error": error_text} - - self.log(f"extracted_report_spec: {self.extracted_report_spec}, type: {type(self.extracted_report_spec)}") - response_data = self.extracted_report_spec - # Access the datatable used to display report specification attributes - self.spec_attribute_datatable = self.query_one("#spec_extracted_datatable", DataTable) - # Ensure columns exist only once - self.spec_attribute_datatable.clear(columns=True) - self.spec_attribute_datatable.add_columns("Attribute", "Value", "Extended Values") - self.spec_attribute_datatable.border = True - self.spec_attribute_datatable.zebra_stripes = True - self.spec_attribute_datatable.cursor_type = "row" - self.spec_attribute_datatable.refresh() - - - if not response_data or response_data == None or response_data == "": - response_data = {"NoData": "No data found for selected item"} - if isinstance(response_data, list): - for list_item in response_data: - response_data: dict = list_item - self.log(f"list_item: {list_item}") - if isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.log(f"else: key {key} value: {value}") - self.spec_attribute_datatable.add_row(key, value) - continue - if isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.spec_attribute_datatable.add_row(key, value) - continue - - self.spec_attribute_datatable.refresh() - mount_point = self.query_one("#container2", Container) - await mount_point.mount(self.spec_attribute_datatable, before="#two_end") - return - - async def execute_selected_report_spec(self, selected_name: str = ""): - """ execute the selected report spec """ - self.log(f"Executing report spec: {self.selected_name}") - try: - reponse = exec_report_spec(format_set_name=selected_name, - output_format="DICT", - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password) - self.log(f"Return from exec_report_spec:") - self.log(f"reponse: {reponse}") - except (ValidationError) as e: - self.log(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - self.log(f"Exception: {e}") - reponse = {"error": f"Exception: {e}"} - await self.display_response(reponse) - - async def display_response(self, response): - """ Display the response from executing the selected report spec""" - self.response = response - # create and/or clear datatable for displaying data - self.spec_output_datatable = self.query_one("#spec_output_datatable", DataTable) - self.spec_output_datatable.clear(columns=True) - self.log(f"self.spec_output_datatable : {self.spec_output_datatable} has been created") - # self.spec_output_datatable.id = "spec_output_datatable" - self.log(f"self.spec_output_datatable has been assigned an id : {self.spec_output_datatable.id} ") - self.spec_output_datatable.add_columns("Key", "Value", "Description") - self.log(f"self.spec_output_datatable has been assigned columns {self.spec_output_datatable.columns}") - # extract data payload from response["data"]. Then populate variables for display - if isinstance(self.response, dict) and "data" in self.response: - self.response_data = self.response.get("data") - elif isinstance(self.response, dict): - self.response_data = self.response - elif isinstance(self.response, list): - self.response_data = self.response - else: - response_data = [{"NoData": "No data found for selected item", "Data Content": self.response}] - - if isinstance(self.response_data, list): - for list_item in self.response_data: - response_dict = list_item if isinstance(list_item, dict) else {"Value": list_item} - self.log(f"list_item: {list_item}") - for key, value in response_dict.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_output_datatable.add_row(str(vkey), str(vvalue), "") - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_output_datatable.add_row(str(key), str(v), "") - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_output_datatable.add_row(key_str, value_str, "") - else: - self.log(f"else: key {key} value: {value}") - self.spec_output_datatable.add_row(str(key), str(value), "") - - elif isinstance(self.response_data, dict): - for key, value in self.response_data.items(): - self.log(f"key: {key}, value: {value}") - if key == "kind": - continue - elif isinstance(value, dict): - for vkey, vvalue in value.items(): - self.spec_output_datatable.add_row(str(vkey), str(vvalue), "") - elif isinstance(value, list): - for v in value: - self.spec_output_datatable.add_row(str(key), str(v), "") - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_output_datatable.add_row(key_str, value_str, "") - else: - self.spec_output_datatable.add_row(str(key), str(value), "") - - # Always refresh after populating - self.spec_output_datatable.refresh() - mount_point = self.query_one("#container3", Container) - await mount_point.mount(self.spec_output_datatable, before="#three_end") - return - - @on(Button.Pressed, "#quit") - def handle_quit(self, event: Button.Pressed) -> None: - self.action_quit() - - def action_quit(self): - self.exit() - -if __name__ == "__main__": - app = MyApp() - app.run() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment1.tcss b/my_egeria/my_egeria/DemoCode/Experimental Code/experiment1.tcss deleted file mode 100644 index d77bb704..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment1.tcss +++ /dev/null @@ -1,134 +0,0 @@ -/* *********************************************************************************************** */ -/* PDX-License-Identifier: Apache-2.0 */ -/* Copyright Contributors to the ODPi Egeria project. */ -/* */ -/* This file provides a set of report specification related functions for my_egeria. */ -/* */ -/* Unless you are running Microsoft's Studio Product with the Textual Extension installed */ -/* the file will show multiple errors, for example I use the PyCharm IDE for whoich there are */ -/* no extensions for Textual, regarless of the errors the file will run correctly with Textual. */ -/* *********************************************************************************************** */ - -Screen { - layout: grid; - grid-size: 6 3; - grid-columns: 1fr 1fr; -} -Button { - width: auto; - height: auto; - border: solid white; -} -Static { - width: 50%; - height: auto; - /*margin: 1 1;*/ - /*padding-bottom: 1;*/ -} -Label { - width: 50%; - height: auto; - padding-bottom: 1; -} -Input { - width: 50%; - height: auto; - /* margin: 1 1;*/ - padding-bottom: 1; -} -.spaced { - margin-bottom: 1; /* Adds 1 cell of space below the button */ - } -Text {} -.box { - height: 100%; - width: 100%; - border: solid green; -} -/*Horizontal {*/ -/* height: 1fr;*/ -/* width: 100%;*/ -/* padding: 1;*/ -/*}*/ -#container1 { - column-span: 2; - width: 100%; - height: 1fr; - padding: 1; -} -#container2 { - column-span: 2; - width: 100%; - height: 1fr; - padding: 1; -} -#container3 { - column-span: 4; - row-span: 2; - width: 100%; - height: 1fr; - padding: 1; -} -#container4 { - column-span:2; - row-span: 3; - width: 100%; - height: 1fr; - padding: 1; -} -#one_start { - width: 100%; - height: auto; -} -#two_start { - width: 100%; - height: auto; -} -#three_start { - width: 100%; - height: auto; -} -#four_start { - width: 100%; - height: auto; -} -#one_end { - width: 100%; - height: auto; -} -#two_end { - width: 100%; - height: auto; -} -#three_end { - width: 100%; - height: auto; -} -#four_end { - width: 100%; - height: auto; -} -#spec_datatable { - width: 100%; - height: auto; -} -#spec_extracted_datatable { - width: 100%; - height: auto; -} -#specoutput_datatable { - width: 100%; - height: auto; -} -#spec_datagrid { - width: 100% -} -#input_fields { - width: 100%; - height: auto; - padding-bottom: 1; -} -#comment1 { - width: 100%; - height: auto; -} \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2-tree-rotated.py b/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2-tree-rotated.py deleted file mode 100644 index f85fde44..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2-tree-rotated.py +++ /dev/null @@ -1,573 +0,0 @@ -""" python - - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a utility function for my_egeria. - - This file is being used to test some display layout options - This is a test file to explore different ways to display Egeria data - Author: Peter Coldicott, November 2025 - - This version is to test having the user input required parameters to run - customized reports - -""" -from __future__ import annotations -from pyegeria import config -import os -import re -from typing import Any -from loguru import logger - -from commands.cat.run_report import list_generic -from pydantic import ValidationError -from pyegeria.base_report_formats import report_spec_list, select_report_spec -from pyegeria.format_set_executor import exec_report_spec -from textual import on -from textual.app import App -from textual.containers import Container, Horizontal, HorizontalScroll, ScrollableContainer -from textual.widgets import Static, Button, DataTable, Header, Footer, Input, Tree, Label - - -class RunSpec(App): - CSS_PATH = "experiment1.tcss" - - BINDINGS = [ - ("q", "quit", "Quit"), - ("ctl+e", "expand", "Expand Twisties"), - ("r", "prepare_report", "Run"), - ] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - self.selected_report_spec: str = "" - self.items: list = [] - self.created_inputs: list[Input] = [] - self.inputs_tracker: dict[str, list] = {} - self.snode_label: str = "" - # Holds rotated data as column -> list of row values - self.rotated_table: dict[str, list[str]] = {} - - def compose(self): - self.title = "My_Egeria" - self.sub_title = "Report Specification Details" - yield Header() - yield Container( - Static("Start of report specification list:", id="one_start"), - Tree(label="Report Specs", id="spec_tree", classes="trees"), - Static("End of report specification list:", id="one_end"), - id="container1", classes="box") - yield Container( - Static("Report Specification Details", classes="box", id="two_start"), - DataTable(id="spec_extracted_datatable"), - Static("End of Report Specification Details", classes="box", id="two_end"), - id="container2", classes="box") - yield ScrollableContainer( - Static("Report Specs Input Fields", classes="box", id="four_start"), - Container(id="input_fields"), - Static("End of Report Specs Input Fields", classes="box", id="four_end"), - id="container4", classes="box") - yield Container( - Static("Report Specification Output)", classes="box", id="three_start"), - DataTable(id="spec_output_datatable"), - Static("End of Report Specification Output)", classes="box", id="three_end"), - id="container3", classes="box") - yield Footer() - - def on_mount(self): - self.spec_tree = self.query_one("#spec_tree", Tree) - self.spec_tree.clear() - root_data = "A categorized list of PyEgeria report specifications" - self.spec_tree.root.expand() - self.spec_tree.root.label = "Report Specs" - self.spec_tree.root.data = root_data - tree_root = self.spec_tree.root - self.family: str = "" - self.heading = "Report Specs" - self.subheading = "Select a report spec to execute:" - self.description = "A list of report specifications, click on one to see its attributes\ns key to run" - - try: - self.report_spec_list: list = report_spec_list(show_family=True, sort_by_family=True, return_kind="dicts") - logger.debug(f"report_spec_list: {self.report_spec_list}, type: {type(self.report_spec_list)}") - except Exception as e: - logger.debug(f"Error getting report_spec_list: {e}") - import traceback - logger.debug(traceback.format_exc()) - self.report_spec_list = [] - - for spec in self.report_spec_list: - if spec.get("family") != self.family: - self.family = spec.get("family") - family_node = tree_root.add(self.family, data="family_name") - family_node.expand() - family_node.add(spec.get("name"), data=spec.get("description")) - continue - else: - family_node.add(spec.get("name"), data=spec.get("description")) - continue - tree_root.collapse_all() - self.spec_tree.refresh() - - @on(Tree.NodeSelected, "#spec_tree") - async def handle_spec_tree_node_selected(self, message: Tree.NodeSelected): - logger.debug(f"Node Selected, Processing selection") - self.selected_report_spec = message.node - logger.debug(f"selected_report_spec: {self.selected_report_spec}") - snode = message.node - logger.debug(f"snode: {snode}", type(snode)) - if snode.data: - snode_label = str(snode.label) - logger.debug(f"snode_label: {snode_label}", type(snode_label)) - else: - snode_label = str(snode.label) - logger.debug(f"snode_label: {snode_label}", type(snode_label)) - # address input fields container so we can clear it from prior selections - inputs_container = self.query_one("#input_fields", Container) - # Clear previous rows - try: - self.created_inputs: list[Input] = [] - # Remove all existing children synchronously - for child in list(inputs_container.children): - await child.remove() - except Exception as e: - logger.debug(f"Exception clearing inputs container: {e} possibly first time through") - self.snode_label = snode_label - await self.get_named_report_spec_details(snode_label) - - if "DICT" or "FORM" in self.inputs_tracker: - outlabel = Label("Output Format: DICT to display report on screen, FORM to write to file", id="comment1") - await inputs_container.mount(outlabel) - - async def get_named_report_spec_details(self, name): - """Get the details of a named report spec and render as a flat table. """ - - self.spec_name = name - logger.debug(f"get_named_report_spec_details: {self.spec_name}") - selected_report_spec = select_report_spec(self.spec_name, output_type="DICT") - logger.debug(f"selected_report_spec: {selected_report_spec}, type: {type(selected_report_spec)}") - if selected_report_spec == None: - selected_report_spec = {"NoDetails": "No details found for selected report spec"} - # Normalize the shape of the returned spec → always a dict - if isinstance(selected_report_spec, dict) and "data" in selected_report_spec: - self.extracted_report_spec: dict = selected_report_spec.get("data") or {} - elif isinstance(selected_report_spec, dict): - self.extracted_report_spec: dict = selected_report_spec or {} - else: - error_text = f"Unknown shape: {selected_report_spec}" - self.extracted_report_spec = {"Error": error_text} - - logger.debug(f"extracted_report_spec: {self.extracted_report_spec}, type: {type(self.extracted_report_spec)}") - response_data = self.extracted_report_spec - # Access the datatable used to display report specification attributes - self.spec_attribute_datatable = self.query_one("#spec_extracted_datatable", DataTable) - # Ensure columns exist only once - self.spec_attribute_datatable.clear(columns=True) - self.spec_attribute_datatable.add_columns("Attribute", "Value", "Extended Values") - self.spec_attribute_datatable.border = True - self.spec_attribute_datatable.zebra_stripes = True - self.spec_attribute_datatable.cursor_type = "row" - self.spec_attribute_datatable.refresh() - - if not response_data: - response_data = {"NoData": "No data found for selected item"} - if isinstance(response_data, list): - for list_item in response_data: - response_data: dict = list_item - logger.debug(f"list_item: {list_item}") - if isinstance(response_data, dict): - for key, value in response_data.items(): - logger.debug(f"key: {key}, value: {value}") - # check for input capable report spec parms - if key == "required_params" or key == "optional_params" or key == "spec_params" or key == "types": - logger.debug(f"Executing populate_input_fields for key: {key}") - await self.populate_input_fields(key, value) - logger.debug(f"populate_input_fields completed for key: {key}") - logger.debug(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - if vkey == "required_params" or vkey == "optional_params" or vkey == "spec_params" or vkey == "types": - logger.debug(f"Executing populate_input_fields for key: {vkey}") - await self.populate_input_fields(vkey, vvalue) - logger.debug(f"populate_input_fields completed for key: {vkey}") - logger.debug(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - logger.debug(f"v: {v}") - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - logger.debug(f"key_str: {key_str}, value_str: {value_str}") - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - elif isinstance(value, dict): - for vkey, vvalue in value.items(): - logger.debug(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - else: - logger.debug(f"else: key {key} value: {value}") - self.spec_attribute_datatable.add_row(key, value) - continue - elif isinstance(response_data, dict): - for key, value in response_data.items(): - logger.debug(f"key: {key}, value: {value}") - if key == "required_params" or key == "optional_params" or key == "spec_params" or key == "types": - logger.debug(f"Executing populate_input_fields for key: {key}") - await self.populate_input_fields(key, value) - logger.debug(f"populate_input_fields completed for key: {key}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - if vkey == "required_params" or vkey == "optional_params" or vkey == "spec_params" or vkey == "types": - logger.debug(f"Executing populate_input_fields for key: {vkey}") - await self.populate_input_fields(vkey, vvalue) - logger.debug(f"populate_input_fields completed for key: {vkey}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.spec_attribute_datatable.add_row(key, value) - continue - - self.spec_attribute_datatable.refresh() - mount_point = self.query_one("#container2", Container) - await mount_point.mount(self.spec_attribute_datatable, before="#two_end") - return - - async def execute_selected_report_spec(self, selected_name: str = "", additional_parameters: dict = None): - """ execute the selected report spec """ - self.selected_name = selected_name - self.additional_parameters = additional_parameters - output_form = "DICT" - if len(self.additional_parameters) > 0: - logger.debug(f"additional_parameters: {self.additional_parameters}") - for key, value in self.additional_parameters.items(): - if value == "True" or value == "False": - self.additional_parameters[key] = bool(value) - else: - logger.debug(f"No additional parameters provided") - self.additional_parameters = {} - if self.additional_parameters != None: - if "inp_types" in self.additional_parameters: - output_form = self.additional_parameters["inp_types"] - - logger.debug(f"Executing report spec: {self.selected_name}") - try: - reponse = exec_report_spec(format_set_name=selected_name, - output_format=output_form, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password, - params=self.additional_parameters - ) - logger.debug(f"Return from exec_report_spec:") - logger.debug(f"reponse: {reponse}") - except (ValidationError) as e: - logger.debug(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - logger.debug(f"Exception: {e}") - import traceback - logger.debug(traceback.format_exc()) - reponse = {"error": f"Exception: {e}"} - await self.display_response(reponse) - - async def display_response(self, response): - """ Display the response from executing the selected report spec""" - self.response = response - # create and/or clear datatable for displaying data - self.spec_output_datatable = self.query_one("#spec_output_datatable", DataTable) - self.spec_output_datatable.clear(columns=True) - logger.debug(f"self.spec_output_datatable : {self.spec_output_datatable} has been created") - logger.debug(f"self.spec_output_datatable has been assigned an id : {self.spec_output_datatable.id} ") - # extract data payload from response["data"]. Then populate variables for display - if isinstance(self.response, dict) and "data" in self.response: - self.response_data = self.response.get("data") - elif isinstance(self.response, dict): - self.response_data = self.response - elif isinstance(self.response, list): - self.response_data = self.response - else: - self.response_data = [{"NoData": "No data found for selected item", "Data Content": self.response}] - - self.rotated_table.clear() - if isinstance(self.response_data, list): - for list_item in self.response_data: - response_dict = list_item if isinstance(list_item, dict) else {"Value": list_item} - logger.debug(f"list_item: {list_item}") - for key, value in response_dict.items(): - logger.debug(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - logger.debug(f"vkey: {vkey}, vvalue: {vvalue}") - self.rotated_table.setdefault(str(vkey), []).append(vvalue) - elif isinstance(value, list): - for v in value: - logger.debug(f"v: {v}") - self.rotated_table.setdefault(str(v), []).append(" ") - elif key == "kind" and value == "empty": - key_str: str = "NoData" - message: str = "For That Asset Type in this repository" - logger.debug(f"key_str: {key_str}, value_str: {message}") - # Present a single consolidated message row under NoData - self.rotated_table[key_str] = [message] - else: - logger.debug(f"else: key {key} value: {value}") - self.rotated_table.setdefault(str(key), []).append(value) - elif isinstance(self.response_data, dict): - for key, value in self.response_data.items(): - logger.debug(f"key: {key}, value: {value}") - if key == "kind": - continue - elif isinstance(value, dict): - for vkey, vvalue in value.items(): - self.rotated_table.setdefault(str(vkey), []).append(vvalue) - elif isinstance(value, list): - for v in value: - self.rotated_table.setdefault(str(v), []).append(" ") - elif key == "kind" and value == "empty": - key_str = "NoData" - message = "For That Asset Type in this repository" - logger.debug(f"key_str: {key_str}, value_str: {message}") - # Present a single consolidated message row under NoData - self.rotated_table[key_str] = [message] - else: - self.rotated_table.setdefault(str(key), []).append(value) - # now the rotated table is loaded with data, create the datatable - keys = list(self.rotated_table.keys()) - for col_key in keys: - self.spec_output_datatable.add_column(str(col_key)) - max_rows = max((len(self.rotated_table[k]) for k in keys), default=0) - for row_idx in range(max_rows): - row = [str(self.rotated_table[k][row_idx]) if row_idx < len(self.rotated_table[k]) else "" for k in keys] - self.spec_output_datatable.add_row(*row) - - # Always refresh after populating - self.spec_output_datatable.refresh() - mount_point = self.query_one("#container3", Container) - await mount_point.mount(self.spec_output_datatable, before="#three_end") - return - - async def populate_input_fields(self, key, value): - """Create interactive input rows for required/optional params.""" - self.key = key - self.input = value - inp_value: Any = None - inputs_container = self.query_one("#input_fields", Container) - - def safe_id(s: str) -> str: - return re.sub(r"[^0-9a-zA-Z_-]", "_", s) - - self.created_inputs: list[Input] = [] - # reset tracker for this render; we'll repopulate with the current inputs - self.inputs_tracker.clear() - - # Build rows according to incoming shape - if isinstance(self.input, dict): - items = list(self.input.items()) - for ikey, ivalue in items: - row = HorizontalScroll(id=f"input_row_{safe_id(str(ikey))}") - await inputs_container.mount(row) - await row.mount(Label(f"{ikey}:")) - logger.debug(f"Processing input field: {ikey}") - if ikey == "search_string": - inp_value = "*" - elif ikey == "page_size": - inp_value = '0' - elif ikey == "start_from": - inp_value = '0' - elif ikey == "starts_with": - inp_value = "True" - elif ikey == "ends_with": - inp_value = "False" - elif ikey == "ignore_case": - inp_value = "False" - elif ikey == "output_format": - inp_value = "DICT" - else: - inp_value = "" - inp = Input(value=inp_value, placeholder=str(ivalue), id=f"inp_{safe_id(str(ikey))}", classes="spaced") - await row.mount(inp) - self.created_inputs.append(inp) - # Track by input id so events can update values reliably - self.inputs_tracker[inp.id] = [str(ikey), ""] - elif isinstance(self.input, list): - # Special-case for types: just display info text - if key == "types": - row = Horizontal(id=f"input_row_{safe_id(str(key))}") - await inputs_container.mount(row) - await row.mount(Label("Output Format:")) - await row.mount(Input(value="DICT", placeholder="DICT", id=f"inp_{safe_id(str(key))}", classes="spaced")) - # informational only; no input to track - else: - for list_item in self.input: - label = str(list_item) - row = Horizontal(id=f"input_row_{safe_id(label)}") - await inputs_container.mount(row) - await row.mount(Label(f"{label}:")) - logger.debug(f"Processing input field: {label}") - if label == "search_string": - inp_value = "*" - elif label == "page_size": - inp_value = '0' - elif label == "start_from": - inp_value = '0' - elif label == "starts_with": - inp_value = "True" - elif label == "ends_with": - inp_value = "False" - elif label == "ignore_case": - inp_value = "False" - elif label == "output_format": - inp_value = "DICT" - else: - inp_value = "" - if isinstance(inp_value, list): - inp_value = ", ".join(inp_value) - logger.debug(f"inp_value: {inp_value}") - inp = Input(value=inp_value, placeholder="", id=f"inp_{safe_id(label)}") - await row.mount(inp) - self.created_inputs.append(inp) - # Track with a reasonable parameter name for list entries - self.inputs_tracker[inp.id] = [label, ""] - else: - # Unknown shape: show a message - row = Horizontal(id=f"input_row_{safe_id(str(key))}") - await inputs_container.mount(row) - await row.mount(Label(f"Error, Unknown Shape for {key}: {value}")) - - # Give focus to the first input if any - if self.created_inputs: - self.created_inputs[0].focus() - logger.debug(f"inputs_tracker: {self.inputs_tracker}") - - # Refresh the container - inputs_container.refresh() - return - - @on(Input.Changed, "#input_fields Input") - async def handle_input_changed(self, event: Input.Changed) -> None: - """Update the tracker whenever an input changes. - - We key the tracker by the actual Input id and store [param_name, value]. - """ - input_id = event.input.id - value = event.value or "" - # Initialize if missing (defensive) - if input_id not in self.inputs_tracker: - # Default param name falls back to the visible label-less id - self.inputs_tracker[input_id] = [input_id, value] - else: - self.inputs_tracker[input_id][1] = value - logger.debug(f"Changed: {input_id} -> {value}") - return - - @on(Button.Pressed, "#quit") - def handle_quit(self, event: Button.Pressed) -> None: - self.exit(200) - - def action_quit(self): - self.exit(200) - - def action_expand_spec_attribute_table(self): - self.spec_tree.root.expand_all() - return - - async def action_prepare_report(self): - # Build additional parameters from current inputs - self.new_additional_parameters = {} - self.additional_parameters = {} - if self.inputs_tracker: - logger.debug(f"self.inputs_tracker: {self.inputs_tracker}") - for _, (parm_name, parm_value) in self.inputs_tracker.items(): - self.additional_parameters[parm_name] = parm_value - logger.debug(f"additional_parameters: {self.additional_parameters}") - # run report when input fields completed - # iterate and strip the inp_ from the front of the key values if present - for key, value in self.additional_parameters.items(): - new_key:str = key.removeprefix("inp_") - self.new_additional_parameters.update({new_key: value}) - self.additional_parameters.clear() - # change the types key to output_format if present fior inclusion in parameters dict - value = self.new_additional_parameters.get("types") - if value: - self.new_additional_parameters.update({"output_format": value}) - else: - self.new_additional_parameters.update({"output_format": "DICT"}) - self.log (f"new_additional_parameters: {self.new_additional_parameters}") - self.additional_parameters = self.new_additional_parameters - logger.debug(f"additional_parameters trimmed: {self.additional_parameters}") - # Retrieve the output_format value - output_form = self.additional_parameters.get("output_format", "FORM") - logger.debug(f"output_format: {output_form}") - # if output_format is FORM, we write the report output to a file - # if not then we execute the report spec and display the output on the screen - if output_form == "FORM": - await self.report_to_file(self.snode_label, self.additional_parameters) - else: - await self.execute_selected_report_spec(self.snode_label, self.additional_parameters) - return - - async def report_to_file(self, selected_name: str = "", additional_parameters: dict = None): - """ execute the selected report spec """ - # self.selected_name = selected_name - # self.additional_parameters = additional_parameters - my_additional_parameters: dict = additional_parameters - output_form = "FORM" - if len(my_additional_parameters) > 0: - logger.debug(f"additional_parameters: {my_additional_parameters}") - if "output_format" in my_additional_parameters: - output_form = my_additional_parameters.get("output_format") - del my_additional_parameters["output_format"] - else: - output_form = "FORM" - else: - logger.debug(f"No additional parameters provided") - my_additional_parameters = {} - - logger.debug(f"Executing report spec: {selected_name}") - try: - reponse = list_generic(report_spec=selected_name, - output_format=output_form, - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password, - params=my_additional_parameters, - write_file=True - ) - logger.debug(f"Return from exec_report_spec:") - logger.debug(f"reponse: {reponse}") - except (ValidationError) as e: - logger.debug(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - logger.debug(f"Exception: {e}") - reponse = {"error": f"Exception: {e}"} - await self.display_response(reponse) - -def start_exp2(): - app = RunSpec() - app.run() - -if __name__ == "__main__": - start_exp2() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2-tree.py b/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2-tree.py deleted file mode 100644 index 20e41aff..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2-tree.py +++ /dev/null @@ -1,435 +0,0 @@ -""" python - - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a utility function for my_egeria. - - This file is being used to test some display layout options - This is a test file to explore different ways to display Egeria data - Author: Peter Coldicott, November 2025 - - This version is to test having the user input required parameters to run - customized reports - -""" - -import os -import re -from typing import Any - -from pydantic import ValidationError -from pyegeria.base_report_formats import report_spec_list, get_report_format_description, select_report_spec -from pyegeria.format_set_executor import exec_report_spec -from textual import on -from textual.app import App -from textual.containers import Container, Horizontal, HorizontalScroll -from textual.widgets import Static, Button, DataTable, Header, Footer, Input, Tree, Label - - -class MyApp(App): - CSS_PATH = "experiment1.tcss" - - BINDINGS = [ - ("q", "quit", "Quit"), - ("s", "show_report", "Show"), - ("r", "run_report", "Run"), - ] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - self.selected_report_spec: str = "" - self.items: list = [] - self.created_inputs: list[Input] = [] - self.inputs_tracker: dict[str, list] = {} - self.snode_label: str = "" - - def compose(self): - yield Header() - yield Container( - Static("Start of report specification list:", id="one_start"), - Tree(label="Report Specs",id="spec_tree", classes="trees"), - Static("End of report specification list:", id="one_end"), - id="container1", classes="box") - yield Container( - Static("Report Specification Details", classes="box", id="two_start"), - DataTable(id="spec_extracted_datatable"), - Static("End of Report Specification Details", classes="box", id="two_end"), - id="container2", classes="box") - yield Container( - Static("Report Specs Input Fields", classes="box", id="four_start"), - Container(id="input_fields"), - Static("End of Report Specs Input Fields", classes="box", id="four_end"), - id="container4", classes="box") - yield Container( - Static("Report Specification Output)", classes="box", id="three_start"), - DataTable(id="spec_output_datatable"), - Static("End of Report Specification Output)", classes="box", id="three_end"), - id="container3", classes="box") - yield Footer() - - def on_mount(self): - self.spec_tree = self.query_one("#spec_tree", Tree) - self.spec_tree.clear() - root_data = "A categorized list of PyEgeria report specifications" - self.spec_tree.root.expand() - self.spec_tree.root.label = "Report Specs" - self.spec_tree.root.data = root_data - tree_root = self.spec_tree.root - self.family: str = "" - self.heading = "Report Specs" - self.subheading = "Select a report spec to execute:" - self.description = "A list of report specifications, click on one to see its attributes\ns key to run" - self.report_spec_list: list = report_spec_list(show_family=True, sort_by_family=True, return_kind="dicts") - self.log(f"report_spec_list: {self.report_spec_list}, type: {type(self.report_spec_list)}") - for spec in self.report_spec_list: - if spec.get("family") != self.family: - self.family = spec.get("family") - family_node = tree_root.add(self.family, data="family_name") - family_node.expand() - family_node.add(spec.get("name"), data=spec.get("description")) - continue - else: - family_node.add(spec.get("name"), data=spec.get("description")) - continue - self.spec_tree.refresh() - - @on(Tree.NodeSelected, "#spec_tree") - async def handle_spec_tree_node_selected(self, message: Tree.NodeSelected): - self.log(f"Node Selected, Processing selection") - self.selected_report_spec = message.node - self.log(f"selected_report_spec: {self.selected_report_spec}") - snode = message.node - self.log(f"snode: {snode}", type(snode)) - if snode.data: - snode_label = str(snode.label) - self.log(f"snode_label: {snode_label}", type(snode_label)) - else: - snode_label = str(snode.label) - self.log(f"snode_label: {snode_label}", type(snode_label)) - # address input fields container so we can clear it from prior selections - inputs_container = self.query_one("#input_fields", Container) - # Clear previous rows - try: - self.created_inputs: list[Input] = [] - # Remove all existing children synchronously - for child in list(inputs_container.children): - await child.remove() - except Exception as e: - self.log(f"Exception clearing inputs container: {e} possibly first time through") - self.snode_label = snode_label - await self.get_named_report_spec_details(snode_label) - # await self.execute_selected_report_spec(snode_label) - - async def get_named_report_spec_details(self, name): - """Get the details of a named report spec and render as a flat table. - - Simplified approach: - - Normalize the incoming spec to a dict. - - Recursively walk dicts/lists with a small helper that yields rows. - - Use a path convention where the first segment is shown in "Attribute", - the remaining path (if any) is shown in "Value", and the leaf value in - "Extended Values". This keeps the UI compact while preserving structure. - - """ - - self.spec_name = name - self.log(f"get_named_report_spec_details: {self.spec_name}") - selected_report_spec = select_report_spec(self.spec_name, output_type="DICT") - self.log(f"selected_report_spec: {selected_report_spec}, type: {type(selected_report_spec)}") - if selected_report_spec == None: - selected_report_spec = {"NoDetails": "No details found for selected report spec"} - # Normalize the shape of the returned spec → always a dict - if isinstance(selected_report_spec, dict) and "data" in selected_report_spec: - self.extracted_report_spec: dict = selected_report_spec.get("data") or {} - elif isinstance(selected_report_spec, dict): - self.extracted_report_spec: dict = selected_report_spec or {} - else: - error_text = f"Unknown shape: {selected_report_spec}" - self.extracted_report_spec = {"Error": error_text} - - self.log(f"extracted_report_spec: {self.extracted_report_spec}, type: {type(self.extracted_report_spec)}") - response_data = self.extracted_report_spec - # Access the datatable used to display report specification attributes - self.spec_attribute_datatable = self.query_one("#spec_extracted_datatable", DataTable) - # Ensure columns exist only once - self.spec_attribute_datatable.clear(columns=True) - self.spec_attribute_datatable.add_columns("Attribute", "Value", "Extended Values") - self.spec_attribute_datatable.border = True - self.spec_attribute_datatable.zebra_stripes = True - self.spec_attribute_datatable.cursor_type = "row" - self.spec_attribute_datatable.refresh() - - - if not response_data or response_data == None or response_data == "": - response_data = {"NoData": "No data found for selected item"} - if isinstance(response_data, list): - for list_item in response_data: - response_data: dict = list_item - self.log(f"list_item: {list_item}") - if isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - # check for input capable report spec parms - if key == "required_params" or key == "optional_params" or key == "spec_params" or key == "types": - self.log(f"Executing populate_input_fields for key: {key}") - await self.populate_input_fields(key, value) - self.log(f"populate_input_fields completed for key: {key}") - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - if vkey == "required_params" or vkey == "optional_params" or vkey == "spec_params" or vkey == "types": - self.log(f"Executing populate_input_fields for key: {vkey}") - await self.populate_input_fields(vkey, vvalue) - self.log(f"populate_input_fields completed for key: {vkey}") - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.log(f"else: key {key} value: {value}") - self.spec_attribute_datatable.add_row(key, value) - continue - elif isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - if key == "required_params" or key == "optional_params" or key == "spec_params" or key == "types": - self.log(f"Executing populate_input_fields for key: {key}") - await self.populate_input_fields(key, value) - self.log(f"populate_input_fields completed for key: {key}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - if vkey == "required_params" or vkey == "optional_params" or vkey == "spec_params" or vkey == "types": - self.log(f"Executing populate_input_fields for key: {vkey}") - await self.populate_input_fields(vkey, vvalue) - self.log(f"populate_input_fields completed for key: {vkey}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.spec_attribute_datatable.add_row(key, value) - continue - - self.spec_attribute_datatable.refresh() - mount_point = self.query_one("#container2", Container) - await mount_point.mount(self.spec_attribute_datatable, before="#two_end") - return - - async def execute_selected_report_spec(self, selected_name: str = "", additional_parameters: dict = None): - """ execute the selected report spec """ - self.selected_name = selected_name - self.additional_parameters = additional_parameters - if len(self.additional_parameters) > 0: - self.log(f"additional_parameters: {self.additional_parameters}") - else: - self.log(f"No additional parameters provided") - self.additional_parameters = {} - - self.log(f"Executing report spec: {self.selected_name}") - try: - reponse = exec_report_spec(format_set_name=selected_name, - output_format="DICT", - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password, - params=self.additional_parameters, - ) - self.log(f"Return from exec_report_spec:") - self.log(f"reponse: {reponse}") - except (ValidationError) as e: - self.log(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - self.log(f"Exception: {e}") - reponse = {"error": f"Exception: {e}"} - await self.display_response(reponse) - - async def display_response(self, response): - """ Display the response from executing the selected report spec""" - self.response = response - # create and/or clear datatable for displaying data - self.spec_output_datatable = self.query_one("#spec_output_datatable", DataTable) - self.spec_output_datatable.clear(columns=True) - self.log(f"self.spec_output_datatable : {self.spec_output_datatable} has been created") - # self.spec_output_datatable.id = "spec_output_datatable" - self.log(f"self.spec_output_datatable has been assigned an id : {self.spec_output_datatable.id} ") - self.spec_output_datatable.add_columns("Key", "Value", "Description") - self.log(f"self.spec_output_datatable has been assigned columns {self.spec_output_datatable.columns}") - # extract data payload from response["data"]. Then populate variables for display - if isinstance(self.response, dict) and "data" in self.response: - self.response_data = self.response.get("data") - elif isinstance(self.response, dict): - self.response_data = self.response - elif isinstance(self.response, list): - self.response_data = self.response - else: - response_data = [{"NoData": "No data found for selected item", "Data Content": self.response}] - - if isinstance(self.response_data, list): - for list_item in self.response_data: - response_dict = list_item if isinstance(list_item, dict) else {"Value": list_item} - self.log(f"list_item: {list_item}") - for key, value in response_dict.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_output_datatable.add_row(str(vkey), str(vvalue), "") - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_output_datatable.add_row(str(key), str(v), "") - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_output_datatable.add_row(key_str, value_str, "") - else: - self.log(f"else: key {key} value: {value}") - self.spec_output_datatable.add_row(str(key), str(value), "") - - elif isinstance(self.response_data, dict): - for key, value in self.response_data.items(): - self.log(f"key: {key}, value: {value}") - if key == "kind": - continue - elif isinstance(value, dict): - for vkey, vvalue in value.items(): - self.spec_output_datatable.add_row(str(vkey), str(vvalue), "") - elif isinstance(value, list): - for v in value: - self.spec_output_datatable.add_row(str(key), str(v), "") - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_output_datatable.add_row(key_str, value_str, "") - else: - self.spec_output_datatable.add_row(str(key), str(value), "") - - # Always refresh after populating - self.spec_output_datatable.refresh() - mount_point = self.query_one("#container3", Container) - await mount_point.mount(self.spec_output_datatable, before="#three_end") - return - - async def populate_input_fields(self, key, value): - """Create interactive input rows for required/optional params.""" - self.key = key - self.input = value - inputs_container = self.query_one("#input_fields", Container) - - def safe_id(s: str) -> str: - return re.sub(r"[^0-9a-zA-Z_-]", "_", s) - - self.created_inputs: list[Input] = [] - # reset tracker for this render; we'll repopulate with the current inputs - self.inputs_tracker.clear() - - # Build rows according to incoming shape - - if isinstance(self.input, dict): - items = list(self.input.items()) - for ikey, ivalue in items: - row = HorizontalScroll(id=f"input_row_{safe_id(str(ikey))}") - await inputs_container.mount(row) - await row.mount(Label(f"{ikey}:")) - inp = Input(value="", placeholder=str(ivalue), id=f"inp_{safe_id(str(ikey))}", classes="spaced") - await row.mount(inp) - self.created_inputs.append(inp) - # Track by input id so events can update values reliably - self.inputs_tracker[inp.id] = [str(ikey), ""] - elif isinstance(self.input, list): - # Special-case for types: just display info text - if key == "types": - row = Horizontal(id=f"input_row_{safe_id(str(key))}") - await inputs_container.mount(row) - await row.mount(Label("DICT:")) - await row.mount(Label(" DICT is required to run the report in this program")) - # informational only; no input to track - else: - for list_item in self.input: - label = str(list_item) - row = Horizontal(id=f"input_row_{safe_id(label)}") - await inputs_container.mount(row) - await row.mount(Label(f"{label}:")) - inp = Input(value="", placeholder="", id=f"inp_{safe_id(label)}") - await row.mount(inp) - self.created_inputs.append(inp) - # Track with a reasonable parameter name for list entries - self.inputs_tracker[inp.id] = [label, ""] - else: - # Unknown shape: show a message - row = Horizontal(id=f"input_row_{safe_id(str(key))}") - await inputs_container.mount(row) - await row.mount(Label(f"Error, Unknown Shape for {key}: {value}")) - - # Give focus to the first input if any - if self.created_inputs: - self.created_inputs[0].focus() - - # Refresh the container - inputs_container.refresh() - return - - @on(Input.Changed, "#input_fields Input") - async def handle_input_changed(self, event: Input.Changed) -> None: - """Update the tracker whenever an input changes. - - We key the tracker by the actual Input id and store [param_name, value]. - """ - input_id = event.input.id - value = event.value or "" - # Initialize if missing (defensive) - if input_id not in self.inputs_tracker: - # Default param name falls back to the visible label-less id - self.inputs_tracker[input_id] = [input_id, value] - else: - self.inputs_tracker[input_id][1] = value - self.log(f"Changed: {input_id} -> {value}") - return - - @on(Button.Pressed, "#quit") - def handle_quit(self, event: Button.Pressed) -> None: - self.exit(200) - - def action_quit(self): - self.exit(200) - - async def action_run_report(self): - # Build additional parameters from current inputs - self.additional_parameters = {} - if self.inputs_tracker: - for _, (parm_name, parm_value) in self.inputs_tracker.items(): - self.additional_parameters[parm_name] = parm_value - self.log(f"additional_parameters: {self.additional_parameters}") - # run report when input fields completed - await self.execute_selected_report_spec(self.snode_label, self.additional_parameters) - return - -if __name__ == "__main__": - app = MyApp() - app.run() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2.py b/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2.py deleted file mode 100644 index b256cbc1..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment2.py +++ /dev/null @@ -1,331 +0,0 @@ -""" python - - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a utility function for my_egeria. - - This file is being used to test some display layout options - This is a test file to explore different ways to display Egeria data - Author: Peter Coldicott, November 2025 - - This version is to test having the user input required parameters to run - customized reports - -""" - -import os - -from pydantic import ValidationError -from pyegeria.base_report_formats import report_spec_list, get_report_format_description, select_report_spec -from pyegeria.format_set_executor import exec_report_spec -from textual import on -from textual.app import App -from textual.containers import Container, Horizontal -from textual.widgets import Static, Button, DataTable, Header, Footer, Placeholder, Input - - -class MyApp(App): - CSS_PATH = "experiment1.tcss" - - BINDINGS = [ - ("q", "quit", "Quit"), - ("s", "show report", "Show"), ] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - self.selected_report_spec: str = "" - self.items: list = [] - - def compose(self): - yield Header() - yield Container( - Static("Start of report specification list:", id="one_start"), - DataTable(id="spec_datatable"), - Static("End of report specification list:", id="one_end"), - id="container1", classes="box") - yield Container( - Static("Report Specification Details", classes="box", id="two_start"), - DataTable(id="spec_extracted_datatable"), - Static("End of Report Specification Details", classes="box", id="two_end"), - id="container2", classes="box") - yield Container( - Static("Report Specification Output)", classes="box", id="three_start"), - DataTable(id="spec_output_datatable"), - Static("End of Report Specification Output)", classes="box", id="three_end"), - id="container3", classes="box") - yield Container( - Static("Report Specs Input Fields", classes="box", id="four_start"), - Placeholder(id="input_fields"), - Static("End of Report Specs Input Fields", classes="box", id="four_end"), - id="container4", classes="box" - ) - yield Container( - Button("Quit", variant="primary", id="quit"), - id = "action_row" - ) - yield Footer() - - def on_mount(self): - self.spec_datatable = self.query_one("#spec_datatable", DataTable) - self.spec_datatable.clear(columns=True) - self.spec_datatable.add_columns("Family", "Name", "Description", "") - self.spec_datatable.zebra_stripes = True - self.spec_datatable.cursor_type = "row" - self.family: str = "" - self.heading = "Report Specs" - self.subheading = "Select a report spec to execute:" - self.description = "A list of report specifications, click on one to see its attributes\ns key to run" - self.report_spec_list: list = report_spec_list(show_family=True, sort_by_family=True, return_kind="dicts") - self.log(f"report_spec_list: {self.report_spec_list}, type: {type(self.report_spec_list)}") - for spec in self.report_spec_list: - if spec.get("family") != self.family: - self.family = spec.get("family") - self.spec_datatable.add_row(self.family, spec.get("name"), spec.get("description")) - continue - else: - self.spec_datatable.add_row("", spec.get("name"), spec.get("description")) - continue - self.spec_datatable.refresh() - - @on(DataTable.RowSelected, "#spec_datatable") - async def handle_spec_table_row_selected(self, message: DataTable.RowSelected): - self.log(f"Row Selected, Processing selection") - self.selected_report_spec = message.row_key - self.log(f"selected_report_spec: {self.selected_report_spec}") - self.selected_row_data = self.spec_datatable.get_row(message.row_key) - self.log(f"selected_row: {self.selected_row_data}") - self.selected_name = str(self.selected_row_data[1] or "") - self.log(f"selected_name: {self.selected_name}") - await self.get_named_report_spec_details(self.selected_name) - await self.execute_selected_report_spec(self.selected_name) - - async def get_named_report_spec_details(self, name): - """Get the details of a named report spec and render as a flat table. - - Simplified approach: - - Normalize the incoming spec to a dict. - - Recursively walk dicts/lists with a small helper that yields rows. - - Use a path convention where the first segment is shown in "Attribute", - the remaining path (if any) is shown in "Value", and the leaf value in - "Extended Values". This keeps the UI compact while preserving structure. - - """ - - self.spec_name = name - self.log(f"get_named_report_spec_details: {self.spec_name}") - selected_report_spec = select_report_spec(self.spec_name, output_type="DICT") - self.log(f"selected_report_spec: {selected_report_spec}, type: {type(selected_report_spec)}") - - # Normalize the shape of the returned spec → always a dict - if isinstance(selected_report_spec, dict) and "data" in selected_report_spec: - self.extracted_report_spec: dict = selected_report_spec.get("data") or {} - elif isinstance(selected_report_spec, dict): - self.extracted_report_spec: dict = selected_report_spec or {} - else: - error_text = f"Unknown shape: {selected_report_spec}" - self.extracted_report_spec = {"Error": error_text} - - self.log(f"extracted_report_spec: {self.extracted_report_spec}, type: {type(self.extracted_report_spec)}") - response_data = self.extracted_report_spec - # Access the datatable used to display report specification attributes - self.spec_attribute_datatable = self.query_one("#spec_extracted_datatable", DataTable) - # Ensure columns exist only once - self.spec_attribute_datatable.clear(columns=True) - self.spec_attribute_datatable.add_columns("Attribute", "Value", "Extended Values") - self.spec_attribute_datatable.border = True - self.spec_attribute_datatable.zebra_stripes = True - self.spec_attribute_datatable.cursor_type = "row" - self.spec_attribute_datatable.refresh() - - - if not response_data or response_data == None or response_data == "": - response_data = {"NoData": "No data found for selected item"} - if isinstance(response_data, list): - for list_item in response_data: - response_data: dict = list_item - self.log(f"list_item: {list_item}") - if isinstance(response_data, dict): - for key, value in response_data.items(): - # check for input capable report spec parms - if key == "required_params" or key == "optional_params" or key == "spec_params" or key == "types": - self.populate_input_fields(key, value) - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.log(f"else: key {key} value: {value}") - self.spec_attribute_datatable.add_row(key, value) - continue - if isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.spec_attribute_datatable.add_row(key, value) - continue - - self.spec_attribute_datatable.refresh() - mount_point = self.query_one("#container2", Container) - await mount_point.mount(self.spec_attribute_datatable, before="#two_end") - return - - async def execute_selected_report_spec(self, selected_name: str = ""): - """ execute the selected report spec """ - self.log(f"Executing report spec: {self.selected_name}") - try: - reponse = exec_report_spec(format_set_name=selected_name, - output_format="DICT", - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password) - self.log(f"Return from exec_report_spec:") - self.log(f"reponse: {reponse}") - except (ValidationError) as e: - self.log(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - self.log(f"Exception: {e}") - reponse = {"error": f"Exception: {e}"} - await self.display_response(reponse) - - async def display_response(self, response): - """ Display the response from executing the selected report spec""" - self.response = response - # create and/or clear datatable for displaying data - self.spec_output_datatable = self.query_one("#spec_output_datatable", DataTable) - self.spec_output_datatable.clear(columns=True) - self.log(f"self.spec_output_datatable : {self.spec_output_datatable} has been created") - # self.spec_output_datatable.id = "spec_output_datatable" - self.log(f"self.spec_output_datatable has been assigned an id : {self.spec_output_datatable.id} ") - self.spec_output_datatable.add_columns("Key", "Value", "Description") - self.log(f"self.spec_output_datatable has been assigned columns {self.spec_output_datatable.columns}") - # extract data payload from response["data"]. Then populate variables for display - if isinstance(self.response, dict) and "data" in self.response: - self.response_data = self.response.get("data") - elif isinstance(self.response, dict): - self.response_data = self.response - elif isinstance(self.response, list): - self.response_data = self.response - else: - response_data = [{"NoData": "No data found for selected item", "Data Content": self.response}] - - if isinstance(self.response_data, list): - for list_item in self.response_data: - response_dict = list_item if isinstance(list_item, dict) else {"Value": list_item} - self.log(f"list_item: {list_item}") - for key, value in response_dict.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_output_datatable.add_row(str(vkey), str(vvalue), "") - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_output_datatable.add_row(str(key), str(v), "") - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_output_datatable.add_row(key_str, value_str, "") - else: - self.log(f"else: key {key} value: {value}") - self.spec_output_datatable.add_row(str(key), str(value), "") - - elif isinstance(self.response_data, dict): - for key, value in self.response_data.items(): - self.log(f"key: {key}, value: {value}") - if key == "kind": - continue - elif isinstance(value, dict): - for vkey, vvalue in value.items(): - self.spec_output_datatable.add_row(str(vkey), str(vvalue), "") - elif isinstance(value, list): - for v in value: - self.spec_output_datatable.add_row(str(key), str(v), "") - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_output_datatable.add_row(key_str, value_str, "") - else: - self.spec_output_datatable.add_row(str(key), str(value), "") - - # Always refresh after populating - self.spec_output_datatable.refresh() - mount_point = self.query_one("#container3", Container) - await mount_point.mount(self.spec_output_datatable, before="#three_end") - return - - def populate_input_fields(self, key, value): - """ Take input (optional or required) parameters and populate the input fields table""" - self.key = key - self.input = value - if isinstance(self.input, dict): - for key, value in self.input.items(): - self.log(f"key: {key}, value: {value}") - new_input = Horizontal( - Static(f"{key}+{str(value)}"), - Input("", classes="input_fields"), - id="input_field"+str(key)) - mount_point = self.query_one("#container4", Container) - mount_point.mount(new_input, before="#four_end") - elif isinstance(self.input, list): - for list_item in self.input: - #check for type field, if so, handle specifically - if key == "types": - new_input = Horizontal( - Input(placeholder=f"{list_item}"), - id="input_field"+str(key)) - else: - new_input = Horizontal(Static(f"{list_item}"), - Input("", classes="input_fields"), - id="input_field"+str(list_item)) - mount_point = self.query_one("#container4", Container) - mount_point.mount(new_input, before="#four_end") - else: - pass - return - - @on(Button.Pressed, "#quit") - def handle_quit(self, event: Button.Pressed) -> None: - self.action_quit() - - def action_quit(self): - self.exit() - -if __name__ == "__main__": - app = MyApp() - app.run() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment3.py b/my_egeria/my_egeria/DemoCode/Experimental Code/experiment3.py deleted file mode 100644 index 8b061890..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/experiment3.py +++ /dev/null @@ -1,293 +0,0 @@ -""" python - - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides a utility function for my_egeria. - - This file is being used to test some display layout options - This is a test file to explore different ways to display Egeria data - Author: Peter Coldicott, November 2025 - - This file is starting to test the Tech_type process, initially just displaying, and - later allowing for inputting required parameters and executing the process flow - -""" - -import os - -from pydantic import ValidationError -from pyegeria.base_report_formats import report_spec_list, get_report_format_description, select_report_spec -from pyegeria.format_set_executor import exec_report_spec -from textual import on -from textual.app import App -from textual.containers import Container -from textual.widgets import Static, Button, DataTable, Header, Footer - -class MyApp(App): - CSS_PATH = "experiment1.tcss" - - BINDINGS = [ - ("q", "quit", "Quit"), - ("s", "show report", "Show"), ] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.Egeria_config = ["https://127.0.0.1:9443", "qs-view-server", "erinoverview", "secret"] - self.platform_url = self.Egeria_config[0] - self.view_server = self.Egeria_config[1] - self.user = self.Egeria_config[2] - self.password = self.Egeria_config[3] - self.selected_report_spec: str = "" - self.items: list = [] - - def compose(self): - yield Header() - yield Container( - Static("Start of report specification list:", id="one_start"), - DataTable(id="spec_datatable"), - Static("End of report specification list:", id="one_end"), - id="container1", classes="box") - yield Container( - Static("Report Specification Details", classes="box", id="two_start"), - DataTable(id="spec_extracted_datatable"), - Static("End of Report Specification Details", classes="box", id="two_end"), - id="container2", classes="box") - yield Container( - Static("Report Specification Output)", classes="box", id="three_start"), - DataTable(id="spec_output_datatable"), - Static("End of Report Specification Output)", classes="box", id="three_end"), - id="container3", classes="box") - yield Container( - Button("Quit", id="quit"), - id = "action_row" - ) - yield Footer() - - def on_mount(self): - self.spec_datatable = self.query_one("#spec_datatable", DataTable) - self.spec_datatable.clear(columns=True) - self.spec_datatable.add_columns("Name", "Description", "") - self.spec_datatable.zebra_stripes = True - self.spec_datatable.cursor_type = "row" - num_columns = len(self.spec_datatable.columns) - self.log(f"Number of columns: {num_columns}") - self.heading = "Report Specs" - self.subheading = "Select a report spec to execute:" - self.description = "A list of report specifications, click on one to see its attributes\ns key to run" - self.report_spec_list: list = report_spec_list() - self.log(f"report_spec_list: {self.report_spec_list}, type: {type(self.report_spec_list)}") - for spec in self.report_spec_list: - if not isinstance(spec, str): - spec = str(spec) - self.log(f"spec: {spec}") - desc = get_report_format_description(spec) - if not isinstance(desc, str): - desc = str(desc) - self.log(f"desc: {desc}") - self.spec_datatable.add_row(spec, desc) - self.spec_datatable.refresh() - - @on(DataTable.RowSelected, "#spec_datatable") - async def handle_spec_table_row_selected(self, message: DataTable.RowSelected): - self.log(f"Row Selected, Processing selection") - self.selected_report_spec = message.row_key - self.log(f"selected_report_spec: {self.selected_report_spec}") - self.selected_row_data = self.spec_datatable.get_row(message.row_key) - self.log(f"selected_row: {self.selected_row_data}") - self.selected_name = str(self.selected_row_data[0] or "") - self.log(f"selected_name: {self.selected_name}") - await self.get_named_report_spec_details(self.selected_name) - await self.execute_selected_report_spec(self.selected_name) - - async def get_named_report_spec_details(self, name): - """Get the details of a named report spec and render as a flat table. - - Simplified approach: - - Normalize the incoming spec to a dict. - - Recursively walk dicts/lists with a small helper that yields rows. - - Use a path convention where the first segment is shown in "Attribute", - the remaining path (if any) is shown in "Value", and the leaf value in - "Extended Values". This keeps the UI compact while preserving structure. - - """ - - self.spec_name = name - self.log(f"get_named_report_spec_details: {self.spec_name}") - selected_report_spec = select_report_spec(self.spec_name, output_type="DICT") - self.log(f"selected_report_spec: {selected_report_spec}, type: {type(selected_report_spec)}") - - # Normalize the shape of the returned spec → always a dict - if isinstance(selected_report_spec, dict) and "data" in selected_report_spec: - self.extracted_report_spec: dict = selected_report_spec.get("data") or {} - elif isinstance(selected_report_spec, dict): - self.extracted_report_spec: dict = selected_report_spec or {} - else: - error_text = f"Unknown shape: {selected_report_spec}" - self.extracted_report_spec = {"Error": error_text} - - self.log(f"extracted_report_spec: {self.extracted_report_spec}, type: {type(self.extracted_report_spec)}") - response_data = self.extracted_report_spec - # Access the datatable used to display report specification attributes - self.spec_attribute_datatable = self.query_one("#spec_extracted_datatable", DataTable) - # Ensure columns exist only once - self.spec_attribute_datatable.clear(columns=True) - self.spec_attribute_datatable.add_columns("Attribute", "Value", "Extended Values") - self.spec_attribute_datatable.border = True - self.spec_attribute_datatable.zebra_stripes = True - self.spec_attribute_datatable.cursor_type = "row" - self.spec_attribute_datatable.refresh() - - - if not response_data or response_data == None or response_data == "": - response_data = {"NoData": "No data found for selected item"} - if isinstance(response_data, list): - for list_item in response_data: - response_data: dict = list_item - self.log(f"list_item: {list_item}") - if isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.log(f"else: key {key} value: {value}") - self.spec_attribute_datatable.add_row(key, value) - continue - if isinstance(response_data, dict): - for key, value in response_data.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.spec_attribute_datatable.add_row(vkey, vvalue) - continue - elif isinstance(value, list): - for v in value: - self.spec_attribute_datatable.add_row(key, v) - continue - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_attribute_datatable.add_row(key_str, value_str) - continue - else: - self.spec_attribute_datatable.add_row(key, value) - continue - - self.spec_attribute_datatable.refresh() - mount_point = self.query_one("#container2", Container) - await mount_point.mount(self.spec_attribute_datatable, before="#two_end") - return - - async def execute_selected_report_spec(self, selected_name: str = ""): - """ execute the selected report spec """ - self.log(f"Executing report spec: {self.selected_name}") - try: - reponse = exec_report_spec(format_set_name=selected_name, - output_format="DICT", - view_server=self.view_server, - view_url=self.platform_url, - user=self.user, - user_pass=self.password) - self.log(f"Return from exec_report_spec:") - self.log(f"reponse: {reponse}") - except (ValidationError) as e: - self.log(f"ValidationError: {e}") - reponse = {"error": f"ValidationError: {e}"} - except Exception as e: - self.log(f"Exception: {e}") - reponse = {"error": f"Exception: {e}"} - await self.display_response(reponse) - - async def display_response(self, response): - """ Display the response from executing the selected report spec""" - self.response = response - # create and/or clear datatable for displaying data - self.spec_output_datatable = self.query_one("#spec_output_datatable", DataTable) - self.spec_output_datatable.clear(columns=True) - self.log(f"self.spec_output_datatable : {self.spec_output_datatable} has been created") - # self.spec_output_datatable.id = "spec_output_datatable" - self.log(f"self.spec_output_datatable has been assigned an id : {self.spec_output_datatable.id} ") - self.spec_output_datatable.add_columns("Key", "Value", "Description") - self.log(f"self.spec_output_datatable has been assigned columns {self.spec_output_datatable.columns}") - # extract data payload from response["data"]. Then populate variables for display - if isinstance(self.response, dict) and "data" in self.response: - self.response_data = self.response.get("data") - elif isinstance(self.response, dict): - self.response_data = self.response - elif isinstance(self.response, list): - self.response_data = self.response - else: - response_data = [{"NoData": "No data found for selected item", "Data Content": self.response}] - - if isinstance(self.response_data, list): - for list_item in self.response_data: - response_dict = list_item if isinstance(list_item, dict) else {"Value": list_item} - self.log(f"list_item: {list_item}") - for key, value in response_dict.items(): - self.log(f"key: {key}, value: {value}") - if isinstance(value, dict): - for vkey, vvalue in value.items(): - self.log(f"vkey: {vkey}, vvalue: {vvalue}") - self.spec_output_datatable.add_row(str(vkey), str(vvalue), "") - elif isinstance(value, list): - for v in value: - self.log(f"v: {v}") - self.spec_output_datatable.add_row(str(key), str(v), "") - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.log(f"key_str: {key_str}, value_str: {value_str}") - self.spec_output_datatable.add_row(key_str, value_str, "") - else: - self.log(f"else: key {key} value: {value}") - self.spec_output_datatable.add_row(str(key), str(value), "") - - elif isinstance(self.response_data, dict): - for key, value in self.response_data.items(): - self.log(f"key: {key}, value: {value}") - if key == "kind": - continue - elif isinstance(value, dict): - for vkey, vvalue in value.items(): - self.spec_output_datatable.add_row(str(vkey), str(vvalue), "") - elif isinstance(value, list): - for v in value: - self.spec_output_datatable.add_row(str(key), str(v), "") - elif key == "kind" and value == "empty": - key_str = "No Data" - value_str = "For That Asset Type in this repository" - self.spec_output_datatable.add_row(key_str, value_str, "") - else: - self.spec_output_datatable.add_row(str(key), str(value), "") - - # Always refresh after populating - self.spec_output_datatable.refresh() - mount_point = self.query_one("#container3", Container) - await mount_point.mount(self.spec_output_datatable, before="#three_end") - return - - @on(Button.Pressed, "#quit") - def handle_quit(self, event: Button.Pressed) -> None: - self.action_quit() - - def action_quit(self): - self.exit() - -if __name__ == "__main__": - app = MyApp() - app.run() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py b/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py deleted file mode 100644 index 2fe435aa..00000000 --- a/my_egeria/my_egeria/DemoCode/Experimental Code/my_network.py +++ /dev/null @@ -1,584 +0,0 @@ -""" - PDX-License-Identifier: Apache-2.0 - Copyright Contributors to the ODPi Egeria project. - - This file provides the main entry point and core lifecycle for the My Profile Textual App. -""" - -import sys -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -from textual import on -from textual.app import App, ComposeResult -from textual.binding import Binding -from textual.containers import ScrollableContainer, Grid -from textual.css.query import NoMatches -from textual.screen import ModalScreen -from textual.widgets import Header, Footer, Static, Placeholder, Input, DataTable, Button, Label - -# Add the project root to sys.path to allow running this script from any directory -root_path = Path(__file__).resolve().parents[4] -if str(root_path) not in sys.path: - sys.path.append(str(root_path)) - -current_dir = Path(__file__).resolve().parent -if str(current_dir) not in sys.path: - sys.path.append(str(current_dir)) - -import os -os.environ.setdefault("PYEGERIA_CONFIG_DIRECTORY", str(root_path / "config")) - -from pyegeria import ( - load_app_config, - settings, - MyProfile, - PyegeriaException, - print_basic_exception, - exec_report_spec, Egeria, -) - -class ConfirmRemoveScreen(ModalScreen[bool]): - """Modal dialog asking the user to confirm removing a peer from their network. - Dismisses with True if the user confirms, False otherwise.""" - - DEFAULT_CSS = """ - ConfirmRemoveScreen { - align: center middle; - } - #confirm_dialog { - grid-size: 2; - grid-gutter: 1 2; - grid-rows: 1fr 3; - padding: 0 1; - width: 60; - height: 11; - border: thick $background 80%; - background: $surface; - } - #confirm_question { - column-span: 2; - height: 1fr; - width: 1fr; - content-align: center middle; - } - #confirm_dialog Button { - width: 100%; - } - """ - - BINDINGS = [("escape", "cancel", "Cancel")] - - def __init__(self, question: str) -> None: - super().__init__() - self.question = question - - def compose(self) -> ComposeResult: - yield Grid( - Label(self.question, id="confirm_question"), - Button("Remove", variant="error", id="confirm_remove"), - Button("Cancel", variant="primary", id="confirm_cancel"), - id="confirm_dialog", - ) - - @on(Button.Pressed, "#confirm_remove") - def handle_confirm(self, event: Button.Pressed) -> None: - self.dismiss(True) - - @on(Button.Pressed, "#confirm_cancel") - def handle_cancel(self, event: Button.Pressed) -> None: - self.dismiss(False) - - def action_cancel(self) -> None: - self.dismiss(False) - - -class MyNetworkApp(App): - """My Network App. - - Finds user ids based on a search parameter to allow the current user to add the user(s) - found to their private "peer network" in their profile in Egeria. - """ - - BINDINGS = [ - ("q", "quit", "Quit"), - ("r", "refresh", "Refresh Data"), - Binding("ctrl+f", "feedback", "Feedback", priority=True), - Binding("f3", "view_feedback_log", "Feedback Log", priority=True), - ] - - # CSS_PATH = "my_profile.tcss" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.title = "My_Network" - self.sub_title = "Egeria network of contacts for the current user" - self.description = "Display the user's network of (Egeria) contacts." - - app_config = settings.Environment - self.log(f"Application Config: {app_config}") - app_user = settings.User_Profile - self.log(f"User Profile: {app_user}") - self.user_name = app_user.user_name or "tessatube" - self.user_password = app_user.user_pwd or "secret" - self.view_server = app_config.egeria_view_server or "qs-view-server" - self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.log(f"Platform URL: {self.platform_url}") - self.log(f"View Server: {self.view_server}") - self.log(f"User: {self.user_name}") - self.log(f"User PWD: {self.user_password}") - self.users_found: DataTable = DataTable(id="users_found") - self.my_network: DataTable = DataTable(id="my_network") - self.my_user_contact_details: DataTable = DataTable(id="my_user_contact_details") - self.my_user_communities: DataTable = DataTable(id="my_user_communities") - self.nickname = "" - self.description = "" - self.selected_network_member = None - self.peer_qname = None, - self.peer_dname = None, - self.peer_fname = None, - self.peer_pronouns = None, - self.peer_guid = None, - self.peer_description = None - # GUID of the peer whose details are currently displayed (get_peer_data resets self.peer_guid) - self.details_peer_guid = None - - def on_mount(self) -> None: - """Perform actions when the application is mounted.""" - self.my_network.add_columns("Qualified Name", "Display Name", "Full Name", "Pronouns", "GUID", "Description") - self.my_network.cursor_type="row" - self.my_network.zebra_stripes=True - error_flag = 0 - try: - eclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) - token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) - self.my_profile=eclient.get_my_profile(output_format="DICT") - self.log(f"User profile retrieved successfully, {self.my_profile}") - except PyegeriaException as e: - self.log(f"Error: {e}") - self.notify(f"Error, unable to retrieve user profile: {e}", severity="error", timeout=20) - error_flag = 1 - finally: - if eclient: - eclient.close_session() - if error_flag == 1: - # Use the error_flag to see if there was an error and if so exit - self.exit(400) - if isinstance(self.my_profile, list): - self.user_GUID = self.my_profile[0].get("GUID") or None - else: - self.notify(f"User profile is not a list: {self.my_profile}", severity="error", timeout=20) - self.exit(400) - self.get_peer_data() - return - - - def get_peer_data(self): - # clear out DataTable - self.my_network.clear() - # Get existing peer relationship list - try: - eclient = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) - token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - open_metadata_type_name = "Referenceable" - relationship_type = "Peer" - body = { - "class": "ResultsRequestBody", - "metadataElementTypeName": open_metadata_type_name, - "includeOnlyRelationships": ["ContactThrough"] - } - peers = eclient.get_related_elements(element_guid=self.user_GUID, - relationship_type=relationship_type, - body=body) - self.log(f"Peers: {peers}") - except PyegeriaException as e: - self.log(f"Error creating peer relationship: {e}") - self.notify(f"Error creating peer relationship: {e}", timeout=15, severity="error") - self.exit(400) - finally: - if eclient: - eclient.close_session() - if peers != "No elements found": - for peer in peers: - self.log(f"Peer: {peer}") - peer_properties = peer.get("properties", None) - self.log(f"Peer Properties: {peer_properties}") - peer_related_by = peer.get("relatedBy", None) - self.log(f"Peer Related By: {peer_related_by}") - peer_relationship_properties = peer.get("relationshipProperties", None) - self.log(f"Peer Relationship Properties: {peer_relationship_properties}") - if peer_properties is not None: - if peer_properties.get("class") == "PersonProperties" and peer_properties.get("typeName") == "Person": - self.peer_qname = peer_properties.get("qualifiedName") - self.peer_dname = peer_properties.get("displayName") - self.peer_fname = peer_properties.get("fullName") - self.peer_pronouns = peer_properties.get("pronouns") - # the peer's own GUID is in its elementHeader - relatedBy describes the Peer relationship - self.peer_guid = (peer.get("elementHeader") or {}).get("guid") - if peer_relationship_properties is not None: - self.peer_description = peer_relationship_properties.get("description") - self.my_network.add_row(self.peer_qname, - self.peer_dname, - self.peer_fname, - self.peer_pronouns, - self.peer_guid, - self.peer_description) - self.my_network.cursor_type = "row" - # reset variables - self.peer_qname = None - self.peer_dname = None - self.peer_fname = None - self.peer_pronouns = None - self.peer_guid = None - self.peer_description = None - continue - continue - else: - self.notify(f"No peers found for user: {self.user_name}", timeout=15, severity="error") - self.my_network.refresh() - container=self.query_one("#users_network_container", ScrollableContainer) - container.remove_children("#users_network_placeholder") - container.mount(self.my_network) - try: - self.query_one("#user_details1", Button) - except NoMatches: - self.log("Button 1 not found") - try: self.query_one("#user_details2", Button) - except NoMatches: - self.log("Button 2 not found") - container.mount(Button(label="User Details", id="user_details1", variant="primary")) - try: - self.query_one("#remove_peer_button", Button) - except NoMatches: - container.mount(Button(label="Remove from Network", id="remove_peer_button", variant="error")) - # container.refresh() - # self.my_network.refresh() - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - yield Static(self.description) - yield ScrollableContainer( - Placeholder(label="Placeholder for contacts list", id="users_network_placeholder"), - id="users_network_container") - yield ScrollableContainer( - Static("Search for user, enter the (partial) name to search for and press return:"), - Input(placeholder="Name", id="user_search_input"), - id="users_input_container") - yield ScrollableContainer(id="user_details_container") - yield Footer() - - @on(Input.Submitted, "#user_search_input") - def handle_input_submitted(self, event: Input.Submitted) -> None: - """Handle the submission of the user search input.""" - self.log(f"User search input submitted: {event.value}") - user_found = self.search_users(str(event.value)) - self.users_found.clear(columns=True) - self.users_found.add_columns("Id", "GUID", "Qualified Name") - self.users_found.cursor_type = "row" - self.users_found.zebra_stripes=True - self.notify(f"Found {len(user_found)} users with name: {event.value}", severity="information", timeout=20) - if len(user_found) == 0: - self.notify(f"No users found with name: {event.value}", severity="warning", timeout=20) - return - if isinstance(user_found, str): - self.notify(f"Error searching for users: {user_found}", severity="error", timeout=20) - return - if isinstance(user_found, list): - for user in user_found: - self.users_found.add_row(user["Display Name"], user["GUID"], user["Qualified Name"]) - self.query_one("#users_network_container", ScrollableContainer).remove_children() - self.query_one("#users_network_container", ScrollableContainer).mount(self.users_found, - Static("Please select a row to add to your network")) - elif isinstance(user_found, dict): - self.users_found.add_row(user_found["User ID"], user_found["GUID"], user_found["Qualified Name"]) - self.query_one("#users_network_container", ScrollableContainer).mount(self.users_found, - Static( - "Please select a row to add to your network")) - else: - self.log(f"Unexpected response type: {type(user_found)}") - self.notify(f"Unexpected response type: {type(user_found)}") - self.users_found.refresh() - self.query_one("#users_input_container", ScrollableContainer).remove_children() - - def search_users(self, user_name: str) -> Any: - self.log(f"Searching for users with name: {user_name}") - try: - eclient=Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) - token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) - user_found=eclient.find_actor_profiles( - search_string=user_name, - ignore_case=True, - output_format="DICT", - report_spec="User-Identities", - query_depth=0, - page_size=10, - ) - self.log(f"Found {user_found} users with name: {user_name}") - except PyegeriaException as e: - self.log(f"Error searching for users: {e}") - user_found="Error" - self.notify(f"Error searching for users: {e}", severity="error", timeout=20) - finally: - if eclient: - eclient.close_session() - return user_found - - @on(DataTable.RowSelected, "#users_found") - def handle_users_found_row_selected(self, event: DataTable.RowSelected) -> None: - self.log(f"User selected: {event.row_key}") - self.row_selected = event.row_key - row_data = self.users_found.get_row(self.row_selected) - self.log(f"Selected row data: {row_data}") - self.query_one("#users_input_container", ScrollableContainer).remove_children() - self.selected_user_id = row_data[0] - self.selected_GUID = row_data[1] - self.selected_qualified_name = row_data[2] - self.log(f"Selected user ID: {self.selected_user_id}") - self.log(f"Selected GUID: {self.selected_GUID}") - self.log(f"Selected qualified name: {self.selected_qualified_name}") - self.query_one("#users_input_container", ScrollableContainer).mount( - Static("Selected ID for new network contact is:"), - Static(f"{self.selected_user_id, " ", self.selected_qualified_name}"), - Static("Please provide a NickName to Store this contact under:"), - Input(placeholder="Nickname", id="nickname_input"), - Static("Optionally provide a description/comment about this relationship"), - Input(placeholder="Description", id="description_input"), - Button("Link Peer", id="link_peer_button", variant="primary") - ) - - @on(Input.Changed, "#nickname_input") - def handle_nickname_input(self, event: Input.Changed): - """Handle the submission of the user nickname input.""" - self.log(f"User nickname input submitted: {event.value}") - self.nickname = event.value - - @on(Input.Changed, "#description_input") - def handle_description_input(self, event: Input.Changed): - """Handle the submission of the user description input.""" - self.log(f"User description input submitted: {event.value}") - self.description = event.value - - @on(Button.Pressed, "#link_peer_button") - def handle_link_peer_button(self, event: Button.Pressed): - """Handle the submission of the link peer button. - Since there is no validity check and both NickName and Description are optional - we link the peer person without further upfront validation """ - #add link to peer - self.log(f"Owner GUID: {self.user_GUID} Peer selected GUID: {self.selected_GUID}") - now = datetime.now(timezone.utc).isoformat(timespec="milliseconds") - body = { - "class": "NewRelationshipRequestBody", - "properties": { - "class": "PeerProperties", - "peer_relationship_name": self.nickname, - "description": self.description, - "effective_from" : None, - "effective_to": None - } - } - self.log(f"Linking peer person: body {body} Owner GUID: {self.user_GUID} and Peer selected GUID: {self.selected_GUID}") - try: - eclient = Egeria(self.view_server, - self.platform_url, - self.user_name, - self.user_password) - token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - eclient.link_peer_person(self.user_GUID, - self.selected_GUID, - body - ) - self.notify(f"Peer person linked successfully") - except PyegeriaException as e: - self.log(f"Error linking peer person: {e}") - self.notify(f"Error linking peer person: {e}") - finally: - # clear input fields - self.query_one("#nickname_input", Input).clear() - self.query_one("#description_input", Input).clear() - # reset display to allow another search - self.query_one("#users_input_container", ScrollableContainer).remove_children() - self.query_one("#users_network_container", ScrollableContainer).remove_children() - self.query_one("#users_input_container").mount( - Static("Search for user, enter the (partial) name to search for and press return:"), - Input(placeholder="Name", id="user_search_input") - ) - self.query_one("#users_network_container").mount( - Static("Peer person network"), - self.my_network - ) - # update the data table to display the updated list of peers - # (get_peer_data also mounts the User Details button if it isn't already present) - self.get_peer_data() - return - - @on(DataTable.RowHighlighted, "#my_network") - def handle_network_member_highlighted(self,event: DataTable.RowHighlighted): - self.log(f"Row highlighted: {event.row_key}") - self.selected_network_member = event.row_key - return - - @on(DataTable.RowSelected, "#my_network") - def handle_network_member_selected(self, event: DataTable.RowSelected): - self.log(f"Row selected: {event.row_key}") - self.selected_network_member = event.row_key - return - - @on(Button.Pressed, "#user_details1") - def handle_user_details_1_button(self, event: Button.Pressed): - if self.selected_network_member: - selected_row_data = self.my_network.get_row(self.selected_network_member) - self.log(f"Selected row data: {selected_row_data}") - self.show_user_details(selected_row_data) - return - else: - self.notify(f"Please select a network member to view details before using the Button.", severity="warning", timeout=10) - return - - @on(Button.Pressed, "#remove_peer_button") - def handle_remove_peer_button(self, event: Button.Pressed): - """Remove the Peer relationship between the current user and the highlighted network member.""" - if not self.selected_network_member: - self.notify(f"Please select a network member to remove before using the Button.", severity="warning", timeout=10) - return - selected_row_data = self.my_network.get_row(self.selected_network_member) - self.log(f"Selected row data for removal: {selected_row_data}") - peer_dname = selected_row_data[1] - peer_guid = selected_row_data[4] - if not peer_guid: - self.notify(f"No GUID found for network member {peer_dname}", severity="error", timeout=10) - return - - def confirm_removal(confirmed: bool | None) -> None: - if confirmed: - self.remove_peer(peer_guid, peer_dname) - else: - self.notify(f"{peer_dname} was not removed from your network", severity="information", timeout=5) - - self.push_screen(ConfirmRemoveScreen(f"Remove {peer_dname} from your network?"), confirm_removal) - return - - def remove_peer(self, peer_guid: str, peer_dname: str) -> None: - """Unlink the peer from the current user's profile and refresh the network display.""" - body = { - "class": "DeleteRelationshipRequestBody" - } - self.log(f"Unlinking peer person: body {body} Owner GUID: {self.user_GUID} and Peer GUID: {peer_guid}") - eclient = None - try: - eclient = Egeria(self.view_server, - self.platform_url, - self.user_name, - self.user_password) - token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) - # same GUID order as link_peer_person: owner first, peer second - eclient.unlink_peer_person(self.user_GUID, - peer_guid, - body - ) - self.notify(f"{peer_dname} removed from your network") - except PyegeriaException as e: - self.log(f"Error unlinking peer person: {e}") - self.notify(f"Error removing {peer_dname} from your network: {e}", severity="error", timeout=15) - return - finally: - if eclient: - eclient.close_session() - # the removed row's key is no longer valid, so clear the selection before reloading the peers - self.selected_network_member = None - # hide the details panel if it is showing the peer that was just removed - if self.details_peer_guid == peer_guid: - self.query_one("#user_details_container", ScrollableContainer).display = False - self.details_peer_guid = None - self.get_peer_data() - return - - def show_user_details(self, selected_row_data): - """ Show both contact details and community membership details for the selected network member.""" - # keep the peer's details separate from self.user_GUID, which is the logged-in user's own profile GUID - self.log(f"Selected row data: {selected_row_data}") - self.peer_qname = selected_row_data[0] - self.peer_dname = selected_row_data[1] - self.peer_fname = selected_row_data[2] - self.peer_pronouns = selected_row_data[3] - self.peer_guid = selected_row_data[4] - self.peer_description = selected_row_data[5] - self.log(f"Peer details: GUID={self.peer_guid}, qname={self.peer_qname}, full_name={self.peer_fname}, display_name={self.peer_dname}, pronouns={self.peer_pronouns}, description={self.peer_description}") - if not self.peer_guid: - self.notify(f"No GUID found for network member {self.peer_dname}", severity="error", timeout=10) - return - #clear and set up the datatables - self.my_user_contact_details.clear(columns=True) - self.my_user_contact_details.add_columns("Name", "Method Type", "Contact Type", "Service", "Value", "GUID") - self.my_user_contact_details.cursor_type = "row" - self.my_user_contact_details.zebra_stripes=True - self.my_user_communities.clear(columns=True) - self.my_user_communities.add_columns("Name", "Assignment Type", "Description", "GUID") - self.my_user_communities.cursor_type = "row" - self.my_user_communities.zebra_stripes=True - # retrieve the peer's profile and extract contact methods and communities using the - # My-User report spec (whose detail specs are My-User-Contact-Detail / My-User-Communities-Detail) - eclient = None - try: - eclient=Egeria( - self.view_server, - self.platform_url, - self.user_name, - self.user_password) - token=eclient.create_egeria_bearer_token(self.user_name, self.user_password) - peer_profile = eclient.get_actor_profile_by_guid(self.peer_guid, output_format="JSON") - peer_details = eclient._generate_my_profile_output(peer_profile, output_format="DICT", - report_spec="My-User") - self.log(f"Retrieved peer profile details: {peer_details}") - except Exception as e: - self.notify(f"Error retrieving details for {self.peer_dname}: {str(e)}", severity="error", timeout=10) - return - finally: - if eclient: - eclient.close_session() - if isinstance(peer_details, list) and peer_details: - peer_details = peer_details[0] - if not isinstance(peer_details, dict): - self.notify(f"No profile details found for {self.peer_dname}", severity="warning", timeout=10) - return - self.user_contact_details = peer_details.get("Contact Methods") or [] - self.user_communities = peer_details.get("Communities") or [] - self.log(f"User details: {self.user_contact_details}") - self.log(f"User communities: {self.user_communities}") - for contact in self.user_contact_details: - self.my_user_contact_details.add_row(contact.get("name"), - contact.get("contactMethodType"), - contact.get("contactType"), - contact.get("contactMethodService"), - contact.get("contactMethodValue"), - contact.get("guid")) - for community in self.user_communities: - self.my_user_communities.add_row(community.get("name"), - community.get("assignmentType"), - community.get("description"), - community.get("guid")) - # display the tables, mounting them the first time through - container = self.query_one("#user_details_container", ScrollableContainer) - container.display = True - self.details_peer_guid = self.peer_guid - try: - container.query_one("#user_details_heading", Static).update(f"Details for {self.peer_dname}") - except NoMatches: - container.mount(Static(f"Details for {self.peer_dname}", id="user_details_heading"), - Static("Contact Methods:"), - self.my_user_contact_details, - Static("Communities:"), - self.my_user_communities) - if not self.user_contact_details: - self.notify(f"No contact methods found for {self.peer_dname}", severity="information", timeout=10) - if not self.user_communities: - self.notify(f"No community memberships found for {self.peer_dname}", severity="information", timeout=10) - return - -def main() -> None: - """Entry point for the my_profile console script.""" - MyNetworkApp().run() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py index 2cce1be9..66931b86 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py @@ -345,7 +345,7 @@ class AddProjectScreen(BaseAddScreen): ("project_start_date", "Start Date (YYYY-MM-DD)", False), ("project_end_date", "Planned End Date (YYYY-MM-DD)", False), ] - LINK_LABEL = "Add yourself to the project team?" + LINK_LABEL = "To not add the project team to your profile click on the switch to move it to the off position" def validate(self, values: dict[str, str]) -> str | None: kind = values["project_classification"] @@ -384,7 +384,7 @@ class AddRoleScreen(BaseAddScreen): ("role_name", "Name of Role", True), ("role_description", "Description of Role", True), ] - LINK_LABEL = "Appoint yourself to this role?" + LINK_LABEL = "To not appoint yourself to this role click on the switch to turn it off" def create_element(self, client: Egeria, values: dict[str, str]) -> str: name = values["role_name"] @@ -417,7 +417,7 @@ class AddTeamScreen(BaseAddScreen): ("team_name", "Name of Team", True), ("team_description", "Description of Team", True), ] - LINK_LABEL = "Join this team as a member?" + LINK_LABEL = "to not join this team click on the switch to turn it off" def create_element(self, client: Egeria, values: dict[str, str]) -> str: name = values["team_name"] @@ -486,7 +486,7 @@ class AddUserIdentityScreen(BaseAddScreen): ("user_identity_display_name", "Display Name", False), ("user_identity_distinguished_name", "Distinguished Name", False), ] - LINK_LABEL = "Link this User Identity to your profile?" + LINK_LABEL = " To not link this User Identity to your profile click on the switch to turn it off" def create_element(self, client: Egeria, values: dict[str, str]) -> str: user_id = values["user_identity_user_id"] diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md b/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md index 080c92e5..fcdbce60 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md @@ -1,6 +1,6 @@ # My Profile Application Return Codes Reference -This document provides a comprehensive catalog of all return codes and screen dismiss codes used across the `my_profile_app.py` application and its supporting handlers and screens in `my_egeria/my_egeria/DemoCode/My_Profile/`. +This document provides a comprehensive catalog of all return codes and screen dismiss codes used across the `my_egeria.py` application and its supporting handlers and screens in `my_egeria/my_egeria/DemoCode/My_Profile/`. In accordance with application conventions: - **200 Series**: Successful operations, normal navigation, user selections, and action triggers. @@ -12,7 +12,7 @@ In accordance with application conventions: | Return Code | Name / Type | Description | Source Components | |---|---|---|---| -| **`200`** | `SUCCESS_OK` / `DEFAULT_EXIT` | Standard success code. Used for successful operations, standard modal dismissal, backing out of dialogs, cancelling inputs without error, or quitting cleanly. | `my_profile_app.py`, `MainScreen.py`, `StatusScreen.py`, `CreateProfileScreen.py`, `EditElementsScreens.py`, `AddToElementsScreens.py`, `TechnologyTypeScreens.py`, `tech_types_handler.py`, `shop_for_data_handler.py`, `team_roles_handler.py`, `SelectionOverviewScreen.py`, `GenericDataViewScreen.py`, `MyBookMarksScreen.py`, `MyTeamScreen.py`, `SearchForTermScreen.py`, `ShowCommentScreen.py` | +| **`200`** | `SUCCESS_OK` / `DEFAULT_EXIT` | Standard success code. Used for successful operations, standard modal dismissal, backing out of dialogs, cancelling inputs without error, or quitting cleanly. | `my_egeria.py`, `MainScreen.py`, `StatusScreen.py`, `CreateProfileScreen.py`, `EditElementsScreens.py`, `AddToElementsScreens.py`, `TechnologyTypeScreens.py`, `tech_types_handler.py`, `shop_for_data_handler.py`, `team_roles_handler.py`, `SelectionOverviewScreen.py`, `GenericDataViewScreen.py`, `MyBookMarksScreen.py`, `MyTeamScreen.py`, `SearchForTermScreen.py`, `ShowCommentScreen.py` | | **`201`** | `ALT_NAVIGATION` / `NO_MATCH` | Alternative navigation or fallback action code. In search/team screens, indicates no matching terms/roles found and triggers transition to data shopping or alternate view. In tech type selection, represents secondary navigation action. | `SearchForTermScreen.py`, `team_roles_handler.py`, `TechnologyTypeScreens.py`, `MyTeamScreen.py`, `ShopForDataScreen.py` | | **`210`** | `QUIT_TO_MAIN` | Exit/Quit action code from data shopping / data viewing screens to dismiss the view and return directly to the main menu screen. | `ShopForDataScreen.py`, `GenericDataViewScreen.py`, `SelectionOverviewScreen.py`, `shop_for_data_handler.py` | | **`211`** | `SUBSCRIBE_ACTION` | Action code indicating the user has requested to create a digital subscription for the selected data source, catalog product, or data element. | `ShopForDataScreen.py`, `GenericDataViewScreen.py`, `SelectionOverviewScreen.py`, `shop_for_data_handler.py` | @@ -43,11 +43,11 @@ In accordance with application conventions: | **`418`** | `TECH_TYPE_TEMPLATE_CALLBACK_INVALID` / `SHOP_DATA_PAYLOAD_INVALID` | Invalid payload or missing required arguments received in technology type templates/processes callback; or invalid data payload returned from Shop for Data screen. | `tech_types_handler.py`, `shop_for_data_handler.py` | | **`419`** | `TECH_TYPE_PLACEHOLDER_NOT_DICT` | Technology type placeholder input data is not a dictionary structure as expected. | `tech_types_handler.py` | | **`420`** | `TECH_TYPE_CREATE_ELEMENT_FAIL` / `SHOP_DATA_RETRIEVAL_ERROR` | Exception raised during creation of metadata element from template via Automated Curation; or error retrieving details for glossary, catalog, or dictionary data elements. | `tech_types_handler.py`, `shop_for_data_handler.py` | -| **`421`** | `APP_INITIALIZATION_FAIL` / `DOMAIN_DATA_RETRIEVAL_ERROR` | Failure to initialize main profile app / retrieve current user profile details; or error retrieving business domain details in Shop for Data. | `my_profile_app.py`, `shop_for_data_handler.py` | +| **`421`** | `APP_INITIALIZATION_FAIL` / `DOMAIN_DATA_RETRIEVAL_ERROR` | Failure to initialize main profile app / retrieve current user profile details; or error retrieving business domain details in Shop for Data. | `my_egeria.py`, `shop_for_data_handler.py` | | **`422`** | `DATA_SPECIFICATION_RETRIEVAL_ERROR` | Error retrieving data specification details from Egeria in Shop for Data handler. | `shop_for_data_handler.py` | | **`423`** | `BUSINESS_DOMAIN_SPEC_ERROR` | Error executing BusinessCapabilities report specification in Shop for Data handler. | `shop_for_data_handler.py` | | **`429`** | `SAMPLE_DATA_RETRIEVAL_ERROR` | Failed to retrieve or parse sample data from Egeria Asset Catalog / Data Engine in Shop for Data handler. | `shop_for_data_handler.py` | -| **`430`** | `INCOMPATIBLE_ENVIRONMENT` | App started under a Python whose pydantic is older than pyegeria requires, so request bodies would reach Egeria with their parameters silently missing. The app exits at startup with a message naming the interpreter and how to run from the project venv instead. | `my_profile_app.py`, `profile_utils.py` | +| **`430`** | `INCOMPATIBLE_ENVIRONMENT` | App started under a Python whose pydantic is older than pyegeria requires, so request bodies would reach Egeria with their parameters silently missing. The app exits at startup with a message naming the interpreter and how to run from the project venv instead. | `my_egeria.py`, `profile_utils.py` | | **`440`** | `TEAM_ROLES_OR_GLOSSARY_ERROR` | Error communicating with Egeria or retrieving glossary term details. (A failed team members lookup is now shown as an error notification and does not exit the app.) | `team_roles_handler.py` | --- diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md index d84ec809..93d0c567 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md @@ -2,7 +2,7 @@ **Working title:** "Meeting Egeria Through My Profile — Callie Quartile, Data Scientist at Coco Pharmaceuticals — Video Blog" **Target length:** ~4 minutes (≈550 spoken words) -**Format:** In-character video blog set at Coco Pharmaceuticals, in the same style as the Jules Keeper series. Talking head / voice-over, cutting to screen capture of `my_profile_app.py` running in a terminal. +**Format:** In-character video blog set at Coco Pharmaceuticals, in the same style as the Jules Keeper series. Talking head / voice-over, cutting to screen capture of `my_egeria.py` running in a terminal. **Presenter persona:** Callie Quartile, data scientist at Coco Pharmaceuticals (swap in another Coco persona if you prefer; see the notes at the end). --- diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/catalog_api_calls.md b/my_egeria/my_egeria/DemoCode/My_Profile/catalog_api_calls.md index dbcca4d1..f1d31461 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/catalog_api_calls.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/catalog_api_calls.md @@ -1,6 +1,6 @@ # Catalog (Shop for Data) — pyegeria API Call Reference -This document maps each UI function in the **Catalogs / Shop for Data** flow of `my_profile_app.py` +This document maps each UI function in the **Catalogs / Shop for Data** flow of `my_egeria.py` to the underlying pyegeria API calls it makes. --- diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py b/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py similarity index 100% rename from my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py rename to my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss index 896f028d..e49aeef0 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss @@ -298,7 +298,10 @@ Input { width: auto; border: solid white; } -#add_input_container .link_row Switch--slider { - color: chartreuse; - background: cornsilk; +#add_input_container .link_row Switch > .switch--slider { + color: grey; /* off knob */ + background: cornsilk; /* track */ +} +#add_input_container .link_row Switch.-on > .switch--slider { + color: chartreuse; /* on knob */ } diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/profile_utils.py b/my_egeria/my_egeria/DemoCode/My_Profile/profile_utils.py index 2443c13b..9fdfe28c 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/profile_utils.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/profile_utils.py @@ -30,7 +30,7 @@ def check_request_serialization() -> str | None: return (f"This Python ({sys.executable}) has pydantic {pydantic.VERSION}, but pyegeria needs " f"pydantic >= {MIN_PYDANTIC_VERSION} - requests to Egeria would lose their parameters.\n" "Run the app from the project venv instead: `source .venv/bin/activate` in the egeria-python " - "folder (then `textual run my_profile_app.py --dev`), or `uv run python my_profile_app.py`.") + "folder (then `textual run my_egeria.py --dev`), or `uv run python my_egeria.py`.") def truncate_at_sequence(data: Any, target: str = "specificationMermaidGraph") -> tuple[Any, bool]: From c1bff75865786460e85f11ed52de92337d0d9d33 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:47:23 -0500 Subject: [PATCH 19/19] Use the my_egeria name for the My Profile app module, manual and tests Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- docs/My-Egeria-Doc.md | 8 +++---- docs/README.md | 2 +- docs/developer_guide_pyegeria_textual.md | 4 ++-- ...file_app_manual.md => my_egeria_manual.md} | 2 +- .../developer_guide/capture_screenshots.py | 2 +- my_egeria/EGERIA_WORKSPACES_INTEGRATION.md | 2 +- my_egeria/README.md | 2 +- .../DemoCode/My_Profile/SplashScreen.py | 4 ++-- .../DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md | 2 +- my_egeria/my_egeria/serve.py | 2 +- pyproject.toml | 2 +- tests/micro-tests/my_profile/README.md | 2 +- tests/micro-tests/my_profile/conftest.py | 5 +++++ .../my_profile/test_key_bindings.py | 4 ++-- ...st_my_profile_app.py => test_my_egeria.py} | 22 +++++++++---------- 15 files changed, 35 insertions(+), 30 deletions(-) rename docs/{my_profile_app_manual.md => my_egeria_manual.md} (99%) rename tests/micro-tests/my_profile/{test_my_profile_app.py => test_my_egeria.py} (95%) diff --git a/docs/My-Egeria-Doc.md b/docs/My-Egeria-Doc.md index d4a0dffa..bf1f2ec6 100644 --- a/docs/My-Egeria-Doc.md +++ b/docs/My-Egeria-Doc.md @@ -3,7 +3,7 @@ # My Profile Application: Reference Guide -This guide explains how the **My Profile Application** (`my_egeria.py`) is put together: its screens, the handler mixins behind them, the Egeria calls each flow makes, and the codes screens use to talk to the app. To learn how to *use* the app (configuration, starting it, keys), see the [My Profile App User Manual](my_profile_app_manual.md). +This guide explains how the **My Profile Application** (`my_egeria.py`) is put together: its screens, the handler mixins behind them, the Egeria calls each flow makes, and the codes screens use to talk to the app. To learn how to *use* the app (configuration, starting it, keys), see the [My Profile App User Manual](my_egeria_manual.md). All source files named here are in `my_egeria/my_egeria/DemoCode/My_Profile/`. @@ -597,7 +597,7 @@ Screens tell the app what to do next through their `dismiss(...)` value: | `212` | Sample data | | `250` | Show responses to a comment | -Failures use codes from the `4xx` range, and some of them exit the app. The [user manual](my_profile_app_manual.md#exit-and-return-codes) lists the common ones, and `RETURN_CODES.md` has the full list. +Failures use codes from the `4xx` range, and some of them exit the app. The [user manual](my_egeria_manual.md#exit-and-return-codes) lists the common ones, and `RETURN_CODES.md` has the full list. --- @@ -614,7 +614,7 @@ The suite in `tests/micro-tests/my_profile/` has one module per handler and scre **Running the whole app in a test.** Tests that run `MyProfileApp.run_test()` stub the profile client with `stub_profile_client()`. -**My Network.** `test_network_handler.py` covers `NetworkMixin` and `MyNetworkScreen`, including the remove confirmation; `test_my_profile_app.py` checks the menu routing. The feature has also been tested by hand against a live server. +**My Network.** `test_network_handler.py` covers `NetworkMixin` and `MyNetworkScreen`, including the remove confirmation; `test_my_egeria.py` checks the menu routing. The feature has also been tested by hand against a live server. **Add screens.** `test_screens.py::TestBaseAddScreen` covers the `BaseAddScreen` flow: required-field checks, create, link or no link, and keeping the form after a failed create. @@ -622,7 +622,7 @@ The suite in `tests/micro-tests/my_profile/` has one module per handler and scre ## Known Gaps -The user-visible limitations are listed in the [user manual](my_profile_app_manual.md#known-limitations). Other internal gaps: +The user-visible limitations are listed in the [user manual](my_egeria_manual.md#known-limitations). Other internal gaps: - `get_data_product_catalog_table`, `get_guid_for_qualified_name`, `display_glossary_term_details`, `tech_type_processes_details`, `unpack_egeria_data` and `display_selected_data_specification` are defined but never called. - Bookmarks, My Collections, team joining, subscription links and starting processes have only been tested against fakes; they haven't yet been run against a live Egeria server. diff --git a/docs/README.md b/docs/README.md index cf9c3386..2181dc8c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ contributors who need more depth than the top-level `README.md`/`CLAUDE.md`/ |---|---| | `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, starting the app, change user, every screen and key, comments, feedback, return codes, known limitations, running the tests. | +| `my_egeria_manual.md` | The My Profile App user manual — configuration, starting the app, change user, every screen and key, comments, feedback, return codes, known limitations, running the tests. | | `My-Egeria-Doc.md` | My Profile App reference guide — source layout, mixin structure, routing tables, Egeria calls and report specs per flow, flow and sequence diagrams. | | `output-formats-and-report-specs.md` | How `generate_output()`/report specs work: `FormatSet`/`Format`/`Column`/`ActionParameter` models, analytic functions, chart output formats. | | `reference-data-and-valid-metadata-mechanisms.md` | Reference data (business value lists) vs. valid metadata values (allowed values of Egeria's own properties): the types, pyegeria methods, Dr.Egeria Reference Data and Valid Metadata families, and live-verified behaviour. | diff --git a/docs/developer_guide_pyegeria_textual.md b/docs/developer_guide_pyegeria_textual.md index 2450179c..d65d9d44 100644 --- a/docs/developer_guide_pyegeria_textual.md +++ b/docs/developer_guide_pyegeria_textual.md @@ -517,7 +517,7 @@ data"), subscribe to data products, comment on elements and leave feedback. It i application, so it shows how the patterns from section 6 hold up at scale. > **Source:** for what the app does from a user's point of view, see the -> [My Profile App User Manual](my_profile_app_manual.md). For its architecture, workflows and +> [My Profile App User Manual](my_egeria_manual.md). For its architecture, workflows and > sequence diagrams in more depth, see [Egeria User Guide: My Profile Application](My-Egeria-Doc.md). > This section picks out the patterns most useful to copy. @@ -1032,7 +1032,7 @@ python examples/developer_guide/capture_screenshots.py |---|---| | [pyegeria Programming Guide](user_programming.md) | §3, §5 (summarised) | | [Output Formats and Report Specs](output-formats-and-report-specs.md) | §4 | -| [My Profile App User Manual](my_profile_app_manual.md) | §7 | +| [My Profile App User Manual](my_egeria_manual.md) | §7 | | [Egeria User Guide: My Profile Application](My-Egeria-Doc.md) | §7 | | [My Profile return codes](../my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md) | §7.6 | | [My Profile tests](../tests/micro-tests/my_profile/README.md) | §10 | diff --git a/docs/my_profile_app_manual.md b/docs/my_egeria_manual.md similarity index 99% rename from docs/my_profile_app_manual.md rename to docs/my_egeria_manual.md index fb9ea008..9508cb06 100644 --- a/docs/my_profile_app_manual.md +++ b/docs/my_egeria_manual.md @@ -321,7 +321,7 @@ The full list is in `RETURN_CODES.md` in the app folder. ## Troubleshooting -**The app exits immediately with code 430.** Your environment has an old pydantic, often because a globally installed `textual` was used. Activate the project's `.venv` and run `textual run --dev my_profile_app.py` or `python my_profile_app.py` from there. +**The app exits immediately with code 430.** Your environment has an old pydantic, often because a globally installed `textual` was used. Activate the project's `.venv` and run `textual run --dev my_egeria.py` or `python my_egeria.py` from there. **The app exits with code 402.** The profile could not be retrieved. Check that: - `EGERIA_PLATFORM_URL` is correct and reachable (VPN and firewall included); diff --git a/examples/developer_guide/capture_screenshots.py b/examples/developer_guide/capture_screenshots.py index cb02f2f1..f3cb1b46 100644 --- a/examples/developer_guide/capture_screenshots.py +++ b/examples/developer_guide/capture_screenshots.py @@ -62,7 +62,7 @@ async def capture_examples() -> None: async def capture_my_profile() -> None: - from my_profile_app import MyProfileApp + from my_egeria.DemoCode.My_Profile.my_egeria import MyProfileApp app = MyProfileApp() async with app.run_test(size=SIZE) as pilot: diff --git a/my_egeria/EGERIA_WORKSPACES_INTEGRATION.md b/my_egeria/EGERIA_WORKSPACES_INTEGRATION.md index e45e915a..afc82d59 100644 --- a/my_egeria/EGERIA_WORKSPACES_INTEGRATION.md +++ b/my_egeria/EGERIA_WORKSPACES_INTEGRATION.md @@ -45,7 +45,7 @@ compose startup. For **freshstart** they must be set per logged-in user ## Compose service — quickstart Add one service per Textual app to `egeria-quickstart.yaml`. -`my_profile_app` is the first; additional apps follow the same pattern. +`my_egeria` (the My Profile app) is the first; additional apps follow the same pattern. ```yaml my-profile: diff --git a/my_egeria/README.md b/my_egeria/README.md index 05682765..d2a1e9a4 100644 --- a/my_egeria/README.md +++ b/my_egeria/README.md @@ -20,7 +20,7 @@ This package contains two Textual apps: - **My Profile** (`DemoCode/My_Profile/`) — the actively developed app, and the one the Egeria-Workspaces portal serves: ```bash - uv run my_profile # or: python -m my_egeria.DemoCode.My_Profile.my_profile_app + uv run my_profile # or: python -m my_egeria.DemoCode.My_Profile.my_egeria ``` - **MyEgeria** — the original glossary/collection browser (not under active development; several of its secondary screens are incomplete): diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SplashScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SplashScreen.py index bab79a2d..47c2a973 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SplashScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SplashScreen.py @@ -17,7 +17,7 @@ class SplashScreen(ModalScreen): - """ SplashScreen for my_profile_app """ + """ SplashScreen for my_egeria """ # Escape must dismiss (not pop) the screen, so that MyProfileApp.mainline still runs BINDINGS = [("escape", "continue_to_app", "Continue")] @@ -27,7 +27,7 @@ class SplashScreen(ModalScreen): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.title = "Egeria" - self.subtitle = "Welcome to my_profile_app" + self.subtitle = "Welcome to my_egeria" load_app_config() app_config = settings.Environment app_user = settings.User_Profile diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md index 93d0c567..0324922e 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md @@ -27,7 +27,7 @@ ## 2. Launch and splash screen (0:40 – 1:00) -**[VISUAL]** Terminal. Type `python my_profile_app.py`. The splash screen appears with *"Welcome to My_Profile for Egeria Users, callie!"* and the **Continue to App** / **Change User** buttons. +**[VISUAL]** Terminal. Type `python my_egeria.py`. The splash screen appears with *"Welcome to My_Profile for Egeria Users, callie!"* and the **Continue to App** / **Change User** buttons. **CALLIE:** > My Profile is a terminal app built on pyegeria. While this welcome screen is showing, it's already loading my profile from Egeria in the background, so by the time I press Continue it's ready. diff --git a/my_egeria/my_egeria/serve.py b/my_egeria/my_egeria/serve.py index 9a088cde..c3d3ba59 100644 --- a/my_egeria/my_egeria/serve.py +++ b/my_egeria/my_egeria/serve.py @@ -32,7 +32,7 @@ APPS = { # name: (module run with `python -m`, port env var, default port, public URL env var) "My Egeria": ("my_egeria.main", "MY_EGERIA_PORT", "8021", "MY_EGERIA_PUBLIC_URL"), - "My Profile": ("my_egeria.DemoCode.My_Profile.my_profile_app", "MY_PROFILE_PORT", "8020", "MY_PROFILE_PUBLIC_URL"), + "My Profile": ("my_egeria.DemoCode.My_Profile.my_egeria", "MY_PROFILE_PORT", "8020", "MY_PROFILE_PUBLIC_URL"), } diff --git a/pyproject.toml b/pyproject.toml index 25084e85..7d584825 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,7 +138,7 @@ dependencies = [ # my_egeria TUI apps — terminal mode my_egeria = "my_egeria.main:main" - my_profile = "my_egeria.DemoCode.My_Profile.my_profile_app:main" + my_profile = "my_egeria.DemoCode.My_Profile.my_egeria:main" # my_egeria TUI apps — local browser mode (textual-serve); env: MY_EGERIA_HOST, MY_EGERIA_PORT, MY_PROFILE_PORT serve_my_egeria = "my_egeria.serve:serve_my_egeria" serve_my_profile = "my_egeria.serve:serve_my_profile" diff --git a/tests/micro-tests/my_profile/README.md b/tests/micro-tests/my_profile/README.md index 0f4c4ae8..58326f4e 100644 --- a/tests/micro-tests/my_profile/README.md +++ b/tests/micro-tests/my_profile/README.md @@ -68,7 +68,7 @@ under test was mocked but something *adjacent* was not (entering client regardless of what the test itself patched). Those tests passed on a machine with a server running and behaved differently on one without. -`stub_profile_client()` in `test_my_profile_app.py` is the helper for that +`stub_profile_client()` in `test_my_egeria.py` is the helper for that specific case: any test that enters `app.run_test()` should call it. ## How the switch works diff --git a/tests/micro-tests/my_profile/conftest.py b/tests/micro-tests/my_profile/conftest.py index ccc80207..154c8649 100644 --- a/tests/micro-tests/my_profile/conftest.py +++ b/tests/micro-tests/my_profile/conftest.py @@ -25,6 +25,11 @@ if str(here) not in sys.path: sys.path.insert(0, str(here)) +# Import the my_egeria package before My_Profile goes on sys.path: the app module is +# My_Profile/my_egeria.py, and once that folder is on the path a first `import my_egeria` +# finds it ahead of the (editable-installed) package the tests import the app through. +import my_egeria # noqa: E402,F401 + profile_path = root_path / "my_egeria" / "my_egeria" / "DemoCode" / "My_Profile" if str(profile_path) not in sys.path: sys.path.insert(0, str(profile_path)) diff --git a/tests/micro-tests/my_profile/test_key_bindings.py b/tests/micro-tests/my_profile/test_key_bindings.py index d71692e2..a4409248 100644 --- a/tests/micro-tests/my_profile/test_key_bindings.py +++ b/tests/micro-tests/my_profile/test_key_bindings.py @@ -6,7 +6,7 @@ escape back one level (every screen except Main) q the only plain-letter key: Main Menu on sub-screens, Quit on Main. Left off text-entry forms, where it would be typed into the Input instead. - ctrl+* every other action, one meaning per key (see docs/my_profile_app_manual.md) + ctrl+* every other action, one meaning per key (see docs/my_egeria_manual.md) """ import inspect @@ -37,7 +37,7 @@ import TechnologyTypeScreens import UserIdentitiesScreen import ViewSubscriptionsScreen -from my_profile_app import MyProfileApp +from my_egeria.DemoCode.My_Profile.my_egeria import MyProfileApp MODULES = [ AddCommentScreen, AddToElementsScreens, CreateProfileScreen, CreateSubscriptionRequestScreen, diff --git a/tests/micro-tests/my_profile/test_my_profile_app.py b/tests/micro-tests/my_profile/test_my_egeria.py similarity index 95% rename from tests/micro-tests/my_profile/test_my_profile_app.py rename to tests/micro-tests/my_profile/test_my_egeria.py index ce5d9b6d..906c085b 100644 --- a/tests/micro-tests/my_profile/test_my_profile_app.py +++ b/tests/micro-tests/my_profile/test_my_egeria.py @@ -13,7 +13,7 @@ from textual.widgets import Button, OptionList, DataTable from textual.widgets._option_list import Option -from my_profile_app import MyProfileApp +from my_egeria.DemoCode.My_Profile.my_egeria import MyProfileApp from MainScreen import MainScreen from CreateProfileScreen import CreateProfileScreen from SplashScreen import SplashScreen @@ -32,7 +32,7 @@ def stub_profile_client(backend, profile_data, identities, todos): under test is mocked. Live mode leaves the real client in place. """ if backend.live: - return backend.patch("my_profile_app.MyProfile") + return backend.patch("my_egeria.DemoCode.My_Profile.my_egeria.MyProfile") mock_mp = MagicMock() mock_mp.create_egeria_bearer_token.return_value = "token" @@ -45,8 +45,8 @@ def stub_profile_client(backend, profile_data, identities, todos): # The My Collections lookup goes through the Egeria facade, not MyProfile mock_egeria = MagicMock() mock_egeria.find_collections.return_value = SAMPLE_COLLECTIONS - backend.always_fake("my_profile_app.Egeria", returns=mock_egeria) - return backend.always_fake("my_profile_app.MyProfile", returns=mock_mp) + backend.always_fake("my_egeria.DemoCode.My_Profile.my_egeria.Egeria", returns=mock_egeria) + return backend.always_fake("my_egeria.DemoCode.My_Profile.my_egeria.MyProfile", returns=mock_mp) def _collection(guid, name, created_by, qualified_name=None): @@ -125,7 +125,7 @@ async def test_app_on_mount_prompt_create_profile(self, backend): mock_mp = MagicMock() mock_mp.create_egeria_bearer_token.return_value = "token" mock_mp._async_get_my_profile = AsyncMock(return_value=[]) - backend.always_fake("my_profile_app.MyProfile", returns=mock_mp) + backend.always_fake("my_egeria.DemoCode.My_Profile.my_egeria.MyProfile", returns=mock_mp) app = MyProfileApp() async with app.run_test() as pilot: @@ -157,7 +157,7 @@ async def test_splash_escape_still_prompts_create_profile(self, backend): mock_mp = MagicMock() mock_mp.create_egeria_bearer_token.return_value = "token" mock_mp._async_get_my_profile = AsyncMock(return_value=[]) - backend.always_fake("my_profile_app.MyProfile", returns=mock_mp) + backend.always_fake("my_egeria.DemoCode.My_Profile.my_egeria.MyProfile", returns=mock_mp) app = MyProfileApp() async with app.run_test() as pilot: @@ -179,7 +179,7 @@ async def test_app_load_profile_exception_exits_402(self, backend): mock_mp = MagicMock() mock_mp.create_egeria_bearer_token.return_value = "token" mock_mp._async_get_my_profile = AsyncMock(side_effect=PyegeriaException("Server error")) - backend.always_fake("my_profile_app.MyProfile", returns=mock_mp) + backend.always_fake("my_egeria.DemoCode.My_Profile.my_egeria.MyProfile", returns=mock_mp) app = MyProfileApp() app.exit = MagicMock() @@ -332,14 +332,14 @@ def test_roles_row_selected_delegates_to_mixin(self): def test_add_comment_callback_ignores_cancel(self, returned): """Cancel/quit/no-selection returns are not errors and make no Egeria call.""" app = MyProfileApp() - with patch("my_profile_app.Egeria") as mock_egeria: + with patch("my_egeria.DemoCode.My_Profile.my_egeria.Egeria") as mock_egeria: assert app.add_comment_callback(returned) is None mock_egeria.assert_not_called() def test_add_comment_callback_success(self): app = MyProfileApp() app.notify = MagicMock() - with patch("my_profile_app.Egeria") as mock_egeria: + with patch("my_egeria.DemoCode.My_Profile.my_egeria.Egeria") as mock_egeria: client = mock_egeria.return_value client.add_comment_to_element.return_value = "comment-guid-1" assert app.add_comment_callback(["Nice", "question", "elem-guid-1"]) == 200 @@ -432,7 +432,7 @@ async def test_get_data_product_catalog_table_success( backend, sample_profile_data, sample_user_identities, sample_todos_data ) mock_exec = backend.patch( - "my_profile_app.exec_report_spec", + "my_egeria.DemoCode.My_Profile.my_egeria.exec_report_spec", returns={ "kind": "data", "data": [ @@ -466,7 +466,7 @@ async def test_get_data_product_catalog_table_empty( stub_profile_client( backend, sample_profile_data, sample_user_identities, sample_todos_data ) - backend.always_fake("my_profile_app.exec_report_spec", returns={"kind": "empty", "data": []}) + backend.always_fake("my_egeria.DemoCode.My_Profile.my_egeria.exec_report_spec", returns={"kind": "empty", "data": []}) app = MyProfileApp() async with app.run_test(): rc = app.get_data_product_catalog_table()