pyegeria 5.3.9.9.3__py3-none-any.whl → 5.5.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyegeria might be problematic. Click here for more details.
- commands/__init__.py +24 -0
- commands/cat/Dr-Egeria_md-orig.py +2 -2
- commands/cat/__init__.py +1 -17
- commands/cat/collection_actions.py +197 -0
- commands/cat/dr_egeria_command_help.py +372 -0
- commands/cat/dr_egeria_jupyter.py +7 -7
- commands/cat/dr_egeria_md.py +27 -182
- commands/cat/exp_list_glossaries.py +11 -14
- commands/cat/get_asset_graph.py +37 -267
- commands/cat/{get_collection.py → get_collection_tree.py} +10 -18
- commands/cat/get_project_dependencies.py +14 -14
- commands/cat/get_project_structure.py +15 -14
- commands/cat/get_tech_type_elements.py +16 -116
- commands/cat/glossary_actions.py +145 -298
- commands/cat/list_assets.py +3 -11
- commands/cat/list_cert_types.py +17 -63
- commands/cat/list_collections.py +46 -138
- commands/cat/list_deployed_catalogs.py +15 -27
- commands/cat/list_deployed_database_schemas.py +27 -43
- commands/cat/list_deployed_databases.py +16 -31
- commands/cat/list_deployed_servers.py +35 -54
- commands/cat/list_glossaries.py +18 -17
- commands/cat/list_projects.py +10 -12
- commands/cat/list_tech_type_elements.py +21 -37
- commands/cat/list_tech_types.py +13 -25
- commands/cat/list_terms.py +38 -79
- commands/cat/list_todos.py +4 -11
- commands/cat/list_user_ids.py +3 -10
- commands/cat/my_reports.py +559 -0
- commands/cat/run_report.py +394 -0
- commands/cat/run_report_orig.py +528 -0
- commands/cli/egeria.py +222 -247
- commands/cli/egeria_cat.py +68 -81
- commands/cli/egeria_my.py +13 -0
- commands/cli/egeria_ops.py +69 -74
- commands/cli/egeria_tech.py +17 -93
- commands/cli/ops_config.py +3 -6
- commands/{cat/list_categories.py → deprecated/list_data_designer.py} +53 -64
- commands/{cat/list_data_structures.py → deprecated/list_data_structures_full.py} +3 -6
- commands/deprecated/old_get_asset_graph.py +315 -0
- commands/my/__init__.py +0 -2
- commands/my/list_my_profile.py +27 -34
- commands/my/list_my_roles.py +1 -7
- commands/my/monitor_my_todos.py +1 -7
- commands/my/monitor_open_todos.py +6 -7
- commands/my/todo_actions.py +4 -5
- commands/ops/__init__.py +0 -2
- commands/ops/gov_server_actions.py +17 -21
- commands/ops/list_archives.py +17 -38
- commands/ops/list_catalog_targets.py +33 -40
- commands/ops/load_archive.py +35 -26
- commands/ops/{monitor_engine_activity_c.py → monitor_active_engine_activity.py} +51 -82
- commands/ops/{monitor_integ_daemon_status.py → monitor_daemon_status.py} +35 -55
- commands/ops/monitor_engine_activity.py +79 -77
- commands/ops/{monitor_gov_eng_status.py → monitor_engine_status.py} +10 -7
- commands/ops/monitor_platform_status.py +38 -50
- commands/ops/monitor_server_startup.py +6 -11
- commands/ops/monitor_server_status.py +7 -11
- commands/ops/orig_monitor_server_list.py +8 -8
- commands/ops/orig_monitor_server_status.py +1 -5
- commands/ops/refresh_integration_daemon.py +5 -5
- commands/ops/restart_integration_daemon.py +5 -5
- commands/ops/table_integ_daemon_status.py +6 -6
- commands/ops/x_engine_actions.py +7 -7
- commands/tech/__init__.py +0 -2
- commands/tech/{generic_actions.py → element_actions.py} +6 -11
- commands/tech/get_element_info.py +20 -29
- commands/tech/get_guid_info.py +23 -42
- commands/tech/get_tech_details.py +20 -35
- commands/tech/get_tech_type_template.py +28 -39
- commands/tech/list_all_om_type_elements.py +24 -30
- commands/tech/list_all_om_type_elements_x.py +22 -28
- commands/tech/list_all_related_elements.py +19 -28
- commands/tech/list_anchored_elements.py +22 -30
- commands/tech/list_asset_types.py +19 -24
- commands/tech/list_elements_by_classification_by_property_value.py +26 -32
- commands/tech/list_elements_by_property_value.py +19 -25
- commands/tech/list_elements_by_property_value_x.py +20 -28
- commands/tech/list_elements_for_classification.py +28 -41
- commands/tech/list_gov_action_processes.py +16 -27
- commands/tech/list_information_supply_chains.py +22 -30
- commands/tech/list_registered_services.py +14 -26
- commands/tech/list_related_elements_with_prop_value.py +15 -25
- commands/tech/list_related_specification.py +1 -4
- commands/tech/list_relationship_types.py +15 -25
- commands/tech/list_relationships.py +20 -36
- commands/tech/list_solution_blueprints.py +28 -33
- commands/tech/list_solution_components.py +23 -29
- commands/tech/list_solution_roles.py +21 -32
- commands/tech/list_tech_templates.py +51 -54
- commands/tech/list_valid_metadata_values.py +5 -9
- commands/tech/table_tech_templates.py +2 -6
- commands/tech/x_list_related_elements.py +1 -4
- examples/GeoSpatial Products Example.py +524 -0
- examples/Jupyter Notebooks/P-egeria-server-config.ipynb +2137 -0
- examples/Jupyter Notebooks/README.md +2 -0
- examples/Jupyter Notebooks/common/P-environment-check.ipynb +115 -0
- examples/Jupyter Notebooks/common/__init__.py +14 -0
- examples/Jupyter Notebooks/common/common-functions.ipynb +4694 -0
- examples/Jupyter Notebooks/common/environment-check.ipynb +52 -0
- examples/Jupyter Notebooks/common/globals.ipynb +184 -0
- examples/Jupyter Notebooks/common/globals.py +154 -0
- examples/Jupyter Notebooks/common/orig_globals.py +152 -0
- examples/format_sets/all_format_sets.json +910 -0
- examples/format_sets/custom_format_sets.json +268 -0
- examples/format_sets/subset_format_sets.json +187 -0
- examples/format_sets_save_load_example.py +291 -0
- examples/jacquard_data_sets.py +129 -0
- examples/output_formats_example.py +193 -0
- examples/test_jacquard_data_sets.py +54 -0
- examples/test_jacquard_data_sets_scenarios.py +94 -0
- md_processing/__init__.py +90 -0
- md_processing/command_dispatcher.py +33 -0
- md_processing/command_mapping.py +221 -0
- md_processing/data/commands/commands_data_designer.json +537 -0
- md_processing/data/commands/commands_external_reference.json +733 -0
- md_processing/data/commands/commands_feedback.json +155 -0
- md_processing/data/commands/commands_general.json +204 -0
- md_processing/data/commands/commands_glossary.json +218 -0
- md_processing/data/commands/commands_governance.json +3678 -0
- md_processing/data/commands/commands_product_manager.json +865 -0
- md_processing/data/commands/commands_project.json +642 -0
- md_processing/data/commands/commands_solution_architect.json +366 -0
- md_processing/data/commands.json +17568 -0
- md_processing/data/commands_working.json +30641 -0
- md_processing/data/gened_report_specs.py +6584 -0
- md_processing/data/generated_format_sets.json +6533 -0
- md_processing/data/generated_format_sets_old.json +4137 -0
- md_processing/data/generated_format_sets_old.py +45 -0
- md_processing/dr_egeria.py +182 -0
- md_processing/md_commands/__init__.py +3 -0
- md_processing/md_commands/data_designer_commands.py +1276 -0
- md_processing/md_commands/ext_ref_commands.py +530 -0
- md_processing/md_commands/feedback_commands.py +726 -0
- md_processing/md_commands/glossary_commands.py +684 -0
- md_processing/md_commands/governance_officer_commands.py +600 -0
- md_processing/md_commands/product_manager_commands.py +1266 -0
- md_processing/md_commands/project_commands.py +383 -0
- md_processing/md_commands/solution_architect_commands.py +1184 -0
- md_processing/md_commands/view_commands.py +295 -0
- md_processing/md_processing_utils/__init__.py +4 -0
- md_processing/md_processing_utils/common_md_proc_utils.py +1249 -0
- md_processing/md_processing_utils/common_md_utils.py +578 -0
- md_processing/md_processing_utils/determine_width.py +103 -0
- md_processing/md_processing_utils/extraction_utils.py +547 -0
- md_processing/md_processing_utils/gen_report_specs.py +643 -0
- md_processing/md_processing_utils/generate_dr_help.py +193 -0
- md_processing/md_processing_utils/generate_md_cmd_templates.py +144 -0
- md_processing/md_processing_utils/generate_md_templates.py +83 -0
- md_processing/md_processing_utils/md_processing_constants.py +1228 -0
- md_processing/md_processing_utils/message_constants.py +19 -0
- pyegeria/__init__.py +201 -443
- pyegeria/core/__init__.py +40 -0
- pyegeria/core/_base_platform_client.py +574 -0
- pyegeria/core/_base_server_client.py +573 -0
- pyegeria/core/_exceptions.py +457 -0
- pyegeria/core/_globals.py +60 -0
- pyegeria/core/_server_client.py +6073 -0
- pyegeria/core/_validators.py +257 -0
- pyegeria/core/config.py +654 -0
- pyegeria/{create_tech_guid_lists.py → core/create_tech_guid_lists.py} +0 -1
- pyegeria/core/load_config.py +37 -0
- pyegeria/core/logging_configuration.py +207 -0
- pyegeria/core/mcp_adapter.py +144 -0
- pyegeria/core/mcp_server.py +212 -0
- pyegeria/core/utils.py +405 -0
- pyegeria/deprecated/__init__.py +0 -0
- pyegeria/{_client.py → deprecated/_client.py} +62 -24
- pyegeria/{_deprecated_gov_engine.py → deprecated/_deprecated_gov_engine.py} +16 -16
- pyegeria/{classification_manager_omvs.py → deprecated/classification_manager_omvs.py} +1988 -1878
- pyegeria/deprecated/output_formatter_with_machine_keys.py +1127 -0
- pyegeria/{runtime_manager_omvs.py → deprecated/runtime_manager_omvs.py} +216 -229
- pyegeria/{valid_metadata_omvs.py → deprecated/valid_metadata_omvs.py} +93 -93
- pyegeria/{x_action_author_omvs.py → deprecated/x_action_author_omvs.py} +2 -3
- pyegeria/egeria_cat_client.py +25 -51
- pyegeria/egeria_client.py +140 -98
- pyegeria/egeria_config_client.py +48 -24
- pyegeria/egeria_tech_client.py +170 -83
- pyegeria/models/__init__.py +150 -0
- pyegeria/models/collection_models.py +168 -0
- pyegeria/models/models.py +654 -0
- pyegeria/omvs/__init__.py +84 -0
- pyegeria/omvs/action_author.py +342 -0
- pyegeria/omvs/actor_manager.py +5980 -0
- pyegeria/omvs/asset_catalog.py +842 -0
- pyegeria/omvs/asset_maker.py +2736 -0
- pyegeria/omvs/automated_curation.py +4403 -0
- pyegeria/omvs/classification_manager.py +11213 -0
- pyegeria/omvs/collection_manager.py +5780 -0
- pyegeria/omvs/community_matters_omvs.py +468 -0
- pyegeria/{core_omag_server_config.py → omvs/core_omag_server_config.py} +157 -157
- pyegeria/{data_designer_omvs.py → omvs/data_designer.py} +1991 -1691
- pyegeria/omvs/data_discovery.py +869 -0
- pyegeria/omvs/data_engineer.py +372 -0
- pyegeria/omvs/digital_business.py +1133 -0
- pyegeria/omvs/external_links.py +1752 -0
- pyegeria/omvs/feedback_manager.py +834 -0
- pyegeria/{full_omag_server_config.py → omvs/full_omag_server_config.py} +73 -69
- pyegeria/omvs/glossary_manager.py +3231 -0
- pyegeria/omvs/governance_officer.py +3009 -0
- pyegeria/omvs/lineage_linker.py +314 -0
- pyegeria/omvs/location_arena.py +1525 -0
- pyegeria/omvs/metadata_expert.py +668 -0
- pyegeria/omvs/metadata_explorer_omvs.py +2943 -0
- pyegeria/omvs/my_profile.py +1042 -0
- pyegeria/omvs/notification_manager.py +358 -0
- pyegeria/omvs/people_organizer.py +394 -0
- pyegeria/{platform_services.py → omvs/platform_services.py} +113 -193
- pyegeria/omvs/product_manager.py +1825 -0
- pyegeria/omvs/project_manager.py +1907 -0
- pyegeria/omvs/reference_data.py +1140 -0
- pyegeria/omvs/registered_info.py +334 -0
- pyegeria/omvs/runtime_manager.py +2817 -0
- pyegeria/omvs/schema_maker.py +446 -0
- pyegeria/{server_operations.py → omvs/server_operations.py} +27 -26
- pyegeria/omvs/solution_architect.py +6490 -0
- pyegeria/omvs/specification_properties.py +37 -0
- pyegeria/omvs/subject_area.py +1042 -0
- pyegeria/omvs/template_manager_omvs.py +236 -0
- pyegeria/omvs/time_keeper.py +1761 -0
- pyegeria/omvs/valid_metadata.py +3221 -0
- pyegeria/omvs/valid_metadata_lists.py +37 -0
- pyegeria/omvs/valid_type_lists.py +37 -0
- pyegeria/view/__init__.py +28 -0
- pyegeria/view/_output_format_models.py +514 -0
- pyegeria/view/_output_formats.py +14 -0
- pyegeria/view/base_report_formats.py +2719 -0
- pyegeria/view/dr_egeria_reports.py +56 -0
- pyegeria/view/format_set_executor.py +397 -0
- pyegeria/{md_processing_utils.py → view/md_processing_utils.py} +5 -5
- pyegeria/{mermaid_utilities.py → view/mermaid_utilities.py} +2 -154
- pyegeria/view/output_formatter.py +1297 -0
- pyegeria-5.5.3.3.dist-info/METADATA +218 -0
- pyegeria-5.5.3.3.dist-info/RECORD +241 -0
- {pyegeria-5.3.9.9.3.dist-info → pyegeria-5.5.3.3.dist-info}/WHEEL +2 -1
- pyegeria-5.5.3.3.dist-info/entry_points.txt +103 -0
- pyegeria-5.5.3.3.dist-info/top_level.txt +4 -0
- commands/cat/.DS_Store +0 -0
- commands/cat/README.md +0 -16
- commands/cli/txt_custom_v2.tcss +0 -19
- commands/my/README.md +0 -17
- commands/ops/README.md +0 -24
- commands/ops/monitor_asset_events.py +0 -108
- commands/tech/README.md +0 -24
- pyegeria/.DS_Store +0 -0
- pyegeria/README.md +0 -35
- pyegeria/_globals.py +0 -47
- pyegeria/_validators.py +0 -385
- pyegeria/asset_catalog_omvs.py +0 -864
- pyegeria/automated_curation_omvs.py +0 -3765
- pyegeria/collection_manager_omvs.py +0 -2744
- pyegeria/dr.egeria spec.md +0 -9
- pyegeria/egeria_my_client.py +0 -56
- pyegeria/feedback_manager_omvs.py +0 -4573
- pyegeria/glossary_browser_omvs.py +0 -3728
- pyegeria/glossary_manager_omvs.py +0 -2440
- pyegeria/m_test.py +0 -118
- pyegeria/md_processing_helpers.py +0 -58
- pyegeria/md_processing_utils_orig.py +0 -1103
- pyegeria/metadata_explorer_omvs.py +0 -2326
- pyegeria/my_profile_omvs.py +0 -1022
- pyegeria/output_formatter.py +0 -389
- pyegeria/project_manager_omvs.py +0 -1933
- pyegeria/registered_info.py +0 -167
- pyegeria/solution_architect_omvs.py +0 -2156
- pyegeria/template_manager_omvs.py +0 -1414
- pyegeria/utils.py +0 -197
- pyegeria-5.3.9.9.3.dist-info/METADATA +0 -72
- pyegeria-5.3.9.9.3.dist-info/RECORD +0 -143
- pyegeria-5.3.9.9.3.dist-info/entry_points.txt +0 -99
- /pyegeria/{_exceptions.py → deprecated/_exceptions.py} +0 -0
- {pyegeria-5.3.9.9.3.dist-info → pyegeria-5.5.3.3.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
Copyright Contributors to the ODPi Egeria project.
|
|
4
|
+
|
|
5
|
+
Utilities for rendering Egeria elements into various output formats
|
|
6
|
+
such as DICT, LIST, Markdown (including Mermaid), and basic HTML.
|
|
7
|
+
|
|
8
|
+
Exceptions
|
|
9
|
+
----------
|
|
10
|
+
Functions in this module may raise the following exceptions:
|
|
11
|
+
- ValueError: Unsupported or inconsistent output format requests (e.g., LIST/TABLE
|
|
12
|
+
without the required columns); invalid option combinations.
|
|
13
|
+
- KeyError: Missing expected keys from provided elements or report specifications.
|
|
14
|
+
- TypeError: Incorrect input types (e.g., non-list where a list of dicts is required).
|
|
15
|
+
- RuntimeError: Failures in Markdown/HTML conversion or Mermaid rendering helpers.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import copy
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
22
|
+
|
|
23
|
+
from pyegeria.core._globals import MERMAID_GRAPH_TITLES, MERMAID_GRAPHS
|
|
24
|
+
from pyegeria.core.utils import (camel_to_title_case)
|
|
25
|
+
from markdown_it import MarkdownIt
|
|
26
|
+
from rich.console import Console
|
|
27
|
+
from loguru import logger
|
|
28
|
+
|
|
29
|
+
from pyegeria.view.mermaid_utilities import construct_mermaid_web
|
|
30
|
+
from pyegeria.view.base_report_formats import select_report_format, MD_SEPARATOR, get_report_spec_match
|
|
31
|
+
from pyegeria.models import to_camel_case
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
Note on select_report_spec function:
|
|
35
|
+
|
|
36
|
+
This function and related data structures have been moved back to _output_formats.py.
|
|
37
|
+
Please import select_report_spec from pyegeria._output_formats instead of from this module.
|
|
38
|
+
"""
|
|
39
|
+
# Todo - put this back after testing
|
|
40
|
+
# console = Console(width=settings.Environment.console_width)
|
|
41
|
+
console = Console(width=300)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _extract_referenceable_properties(element: dict[str, Any]) -> dict[str, Any]:
|
|
45
|
+
# Get general header attributes
|
|
46
|
+
guid = element.get('elementHeader', {}).get("guid", None)
|
|
47
|
+
if guid is None:
|
|
48
|
+
return {}
|
|
49
|
+
metadata_collection_id = element['elementHeader']['origin'].get("homeMetadataCollectionId", None)
|
|
50
|
+
metadata_collection_name = element['elementHeader']['origin'].get("homeMetadataCollectionName", None)
|
|
51
|
+
origin_category = element['elementHeader'].get("origin_category", None)
|
|
52
|
+
created_by = element['elementHeader']["versions"].get("createdBy", None)
|
|
53
|
+
create_time = element['elementHeader']["versions"].get("createTime", None)
|
|
54
|
+
updated_by = element['elementHeader']["versions"].get("updatedBy", None)
|
|
55
|
+
version = element['elementHeader']["versions"].get("version", None)
|
|
56
|
+
type_name = element['elementHeader']["type"].get("typeName", None)
|
|
57
|
+
classifications = element['elementHeader'].get("classifications", [])
|
|
58
|
+
effective_from = element['elementHeader'].get("effectiveFrom", None)
|
|
59
|
+
effective_to = element['elementHeader'].get("effectiveTo", None)
|
|
60
|
+
status = element['elementHeader'].get("status", None)
|
|
61
|
+
# Get attributes from properties
|
|
62
|
+
properties = element['properties']
|
|
63
|
+
url = properties.get('url', None)
|
|
64
|
+
display_name = properties.get("displayName","")
|
|
65
|
+
description = properties.get("description", "") or ""
|
|
66
|
+
qualified_name = properties.get("qualifiedName", "") or ""
|
|
67
|
+
category = properties.get("category", "") or ""
|
|
68
|
+
version_identifier = properties.get("versionIdentifier", "") or ""
|
|
69
|
+
additional_properties = properties.get("additionalProperties", {}) or {}
|
|
70
|
+
extended_properties = properties.get("extendedProperties", {}) or {}
|
|
71
|
+
contentStatus = properties.get("contentStatus", "")
|
|
72
|
+
activityStatus = properties.get("activityStatus", "")
|
|
73
|
+
deploymentStatus = properties.get("deploymentStatus", "")
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"GUID": guid,
|
|
77
|
+
"metadata_collection_id": metadata_collection_id,
|
|
78
|
+
"metadata_collection_name": metadata_collection_name,
|
|
79
|
+
"origin_category": origin_category,
|
|
80
|
+
"created_by": created_by,
|
|
81
|
+
"create_time": create_time,
|
|
82
|
+
"updated_by": updated_by,
|
|
83
|
+
"version": version,
|
|
84
|
+
"type_name": type_name,
|
|
85
|
+
"classifications": classifications,
|
|
86
|
+
"element_status": status,
|
|
87
|
+
"url": url,
|
|
88
|
+
"display_name": display_name,
|
|
89
|
+
"description": description,
|
|
90
|
+
"qualified_name": qualified_name,
|
|
91
|
+
"category": category,
|
|
92
|
+
"version_identifier": version_identifier,
|
|
93
|
+
"additional_properties": additional_properties,
|
|
94
|
+
"extended_properties": extended_properties,
|
|
95
|
+
"effective_from": effective_from,
|
|
96
|
+
"effective_to": effective_to,
|
|
97
|
+
"content_status": contentStatus,
|
|
98
|
+
"activity_status": activityStatus,
|
|
99
|
+
"deployment_status": deploymentStatus,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def markdown_to_html(markdown_text: str) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Convert markdown text to HTML, with special handling for mermaid code blocks.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
markdown_text: The markdown text to convert
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
HTML string
|
|
116
|
+
"""
|
|
117
|
+
# Initialize markdown-it
|
|
118
|
+
md = MarkdownIt()
|
|
119
|
+
|
|
120
|
+
# Find all mermaid code blocks
|
|
121
|
+
mermaid_blocks = re.findall(r'```mermaid\n(.*?)\n```', markdown_text, re.DOTALL)
|
|
122
|
+
|
|
123
|
+
# Replace each mermaid block with a placeholder
|
|
124
|
+
placeholders = []
|
|
125
|
+
for i, block in enumerate(mermaid_blocks):
|
|
126
|
+
placeholder = f"MERMAID_PLACEHOLDER_{i}"
|
|
127
|
+
markdown_text = markdown_text.replace(f"```mermaid\n{block}\n```", placeholder)
|
|
128
|
+
placeholders.append((placeholder, block))
|
|
129
|
+
|
|
130
|
+
# Convert markdown to HTML
|
|
131
|
+
html_text = md.render(markdown_text)
|
|
132
|
+
|
|
133
|
+
# Replace placeholders with rendered mermaid HTML
|
|
134
|
+
for placeholder, mermaid_block in placeholders:
|
|
135
|
+
mermaid_html = construct_mermaid_web(mermaid_block)
|
|
136
|
+
html_text = html_text.replace(placeholder, mermaid_html)
|
|
137
|
+
|
|
138
|
+
# Add basic HTML structure
|
|
139
|
+
html_text = f"""
|
|
140
|
+
<!DOCTYPE html>
|
|
141
|
+
<html>
|
|
142
|
+
<head>
|
|
143
|
+
<meta charset="UTF-8">
|
|
144
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
145
|
+
<title>Egeria Report</title>
|
|
146
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
147
|
+
<script>
|
|
148
|
+
mermaid.initialize({{ startOnLoad: true }});
|
|
149
|
+
</script>
|
|
150
|
+
<style>
|
|
151
|
+
body {{ font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }}
|
|
152
|
+
h1 {{ color: #2c3e50; }}
|
|
153
|
+
h2 {{ color: #3498db; }}
|
|
154
|
+
pre {{ background-color: #f8f8f8; padding: 10px; border-radius: 5px; overflow-x: auto; }}
|
|
155
|
+
table {{ border-collapse: collapse; width: 100%; margin-bottom: 20px; }}
|
|
156
|
+
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
|
|
157
|
+
th {{ background-color: #f2f2f2; }}
|
|
158
|
+
tr:nth-child(even) {{ background-color: #f9f9f9; }}
|
|
159
|
+
</style>
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
{html_text}
|
|
163
|
+
</body>
|
|
164
|
+
</html>
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
return html_text
|
|
168
|
+
|
|
169
|
+
def make_preamble(obj_type: str, search_string: str, output_format: str = 'MD') -> Tuple[str, Optional[str]]:
|
|
170
|
+
"""
|
|
171
|
+
Creates a preamble string and an elements action based on the given object type, search string,
|
|
172
|
+
and output format.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
obj_type: The type of object being updated or reported on (e.g., "Product", "Category").
|
|
176
|
+
search_string: The search string used to filter objects. Defaults to "All Elements" if None.
|
|
177
|
+
output_format: A format identifier determining the output structure.
|
|
178
|
+
JSON - output standard json
|
|
179
|
+
MD - output standard markdown with no preamble
|
|
180
|
+
FORM - output markdown with a preamble for a form
|
|
181
|
+
REPORT - output markdown with a preamble for a report
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
tuple: A tuple containing:
|
|
185
|
+
- A string representing the formatted update or report preamble.
|
|
186
|
+
- A string or None indicating the action description for the elements,
|
|
187
|
+
depending on the output format.
|
|
188
|
+
"""
|
|
189
|
+
# search_string = search_string if search_string else "All Elements"
|
|
190
|
+
elements_md = ""
|
|
191
|
+
elements_action = "Update " + obj_type
|
|
192
|
+
if output_format == "FORM":
|
|
193
|
+
preamble = f"\n# Update {obj_type} Form - created at {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
|
|
194
|
+
if search_string:
|
|
195
|
+
preamble += f"\t {obj_type} found from the search string: `{search_string}`\n\n"
|
|
196
|
+
return preamble, elements_action
|
|
197
|
+
elif output_format == "REPORT":
|
|
198
|
+
elements_md += (f"# {obj_type} Report - created at {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
|
|
199
|
+
f"\t{obj_type} found from the search string: `{search_string}`\n\n")
|
|
200
|
+
elements_action = None
|
|
201
|
+
return elements_md, elements_action
|
|
202
|
+
else:
|
|
203
|
+
return "\n", elements_action
|
|
204
|
+
|
|
205
|
+
def make_md_attribute(attribute_name: str, attribute_value: str, output_type: str) -> Optional[str]:
|
|
206
|
+
"""
|
|
207
|
+
Create a markdown attribute line for a given attribute name and value.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
attribute_name: The name of the attribute
|
|
211
|
+
attribute_value: The value of the attribute
|
|
212
|
+
output_type: The output format (FORM, MD, REPORT)
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
str: Formatted markdown for the attribute
|
|
216
|
+
"""
|
|
217
|
+
output = ""
|
|
218
|
+
if isinstance(attribute_value,str):
|
|
219
|
+
attribute_value = attribute_value.strip() if attribute_value else ""
|
|
220
|
+
elif isinstance(attribute_value,list) and len(attribute_value) > 0:
|
|
221
|
+
attribute_value = ",\n".join(attribute_value)
|
|
222
|
+
if attribute_name:
|
|
223
|
+
if attribute_name.upper() == "GUID":
|
|
224
|
+
attribute_title = attribute_name.upper()
|
|
225
|
+
else:
|
|
226
|
+
# attribute_title = attribute_name.title()
|
|
227
|
+
attribute_title = camel_to_title_case(attribute_name)
|
|
228
|
+
else:
|
|
229
|
+
attribute_title = ""
|
|
230
|
+
|
|
231
|
+
if output_type in ["FORM", "MD"]:
|
|
232
|
+
if attribute_name.lower() in [ "mermaid", "solutionBlueprintMermaidGraph", "links", "implemented by", "sub_components"]:
|
|
233
|
+
return '\n'
|
|
234
|
+
|
|
235
|
+
output = f"## {attribute_title}\n{attribute_value}\n\n"
|
|
236
|
+
elif output_type in ["REPORT", "MERMAID"]:
|
|
237
|
+
if attribute_title in MERMAID_GRAPH_TITLES + ['Mermaid Graph', 'Mermaid']:
|
|
238
|
+
output = f"## {attribute_title}\n\n```mermaid\n{attribute_value}\n```\n"
|
|
239
|
+
elif attribute_value:
|
|
240
|
+
output = f"## {attribute_title}\n{attribute_value}\n\n"
|
|
241
|
+
return output
|
|
242
|
+
|
|
243
|
+
def format_for_markdown_table(text: str, guid: str = None) -> str:
|
|
244
|
+
"""
|
|
245
|
+
Format text for markdown tables by replacing newlines with spaces and escaping pipe characters.
|
|
246
|
+
No truncation is applied to allow full-length text display regardless of console width.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
text (str): The text to format
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
str: Formatted text safe for markdown tables
|
|
253
|
+
"""
|
|
254
|
+
if not text:
|
|
255
|
+
return ""
|
|
256
|
+
# Replace newlines with spaces and escape pipe characters
|
|
257
|
+
if isinstance(text, list):
|
|
258
|
+
text = "\n".join(text)
|
|
259
|
+
t = text.replace("\n", " ").replace("|", "\\|")
|
|
260
|
+
if '::' in t and guid:
|
|
261
|
+
t = f" [{t}](#{guid}) "
|
|
262
|
+
return t
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def populate_columns_from_properties(element: dict, columns_struct: dict) -> dict:
|
|
266
|
+
"""
|
|
267
|
+
Populate a columns_struct with values from the element's properties.
|
|
268
|
+
|
|
269
|
+
The element can be either:
|
|
270
|
+
- a full element dict that contains a nested 'properties' dict (preferred), or
|
|
271
|
+
- a dict that itself represents the set of properties.
|
|
272
|
+
|
|
273
|
+
In both cases, the effective properties are expected to use camelCase keys.
|
|
274
|
+
The columns_struct is expected to follow the format returned by select_report_spec, where
|
|
275
|
+
columns are located at columns_struct['formats']['columns'] and each column is a dict containing
|
|
276
|
+
at least a 'key' field expressed in snake_case. For each column whose snake_case key corresponds
|
|
277
|
+
to a key in the element properties (after converting to camelCase), this function adds a 'value'
|
|
278
|
+
entry to the column with the matching property's value.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
element: Either a dict with a nested 'properties' dict using camelCase keys, or
|
|
282
|
+
a dict that itself is the set of properties.
|
|
283
|
+
columns_struct: The columns structure whose columns have snake_case 'key' fields.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
The updated columns_struct (the input structure is modified in place and also returned).
|
|
287
|
+
"""
|
|
288
|
+
if not isinstance(columns_struct, dict):
|
|
289
|
+
return columns_struct
|
|
290
|
+
|
|
291
|
+
# Determine the effective properties:
|
|
292
|
+
# 1) Prefer element['properties'] if present and a dict
|
|
293
|
+
# 2) Otherwise, if element itself is a dict, treat it as the properties dict
|
|
294
|
+
# 3) Otherwise, nothing to do
|
|
295
|
+
props: dict | None = None
|
|
296
|
+
if isinstance(element, dict):
|
|
297
|
+
maybe_props = element.get('properties')
|
|
298
|
+
props = maybe_props if isinstance(maybe_props, dict) else element
|
|
299
|
+
if not isinstance(props, dict):
|
|
300
|
+
return columns_struct
|
|
301
|
+
|
|
302
|
+
# Get the attributes list if present
|
|
303
|
+
formats = columns_struct.get('formats') or {}
|
|
304
|
+
columns = formats.get('attributes') if isinstance(formats, dict) else None
|
|
305
|
+
if not isinstance(columns, list):
|
|
306
|
+
return columns_struct
|
|
307
|
+
|
|
308
|
+
# Pre-compute snake_case to camelCase mapping for all columns (cache for performance)
|
|
309
|
+
key_mapping = {}
|
|
310
|
+
for col in columns:
|
|
311
|
+
if isinstance(col, dict):
|
|
312
|
+
key_snake = col.get('key')
|
|
313
|
+
if key_snake:
|
|
314
|
+
key_mapping[key_snake] = to_camel_case(key_snake)
|
|
315
|
+
|
|
316
|
+
# Single pass to populate values
|
|
317
|
+
for col in columns:
|
|
318
|
+
try:
|
|
319
|
+
key_snake = col.get('key') if isinstance(col, dict) else None
|
|
320
|
+
if not key_snake:
|
|
321
|
+
continue
|
|
322
|
+
# Use pre-computed camelCase key
|
|
323
|
+
key_camel = key_mapping.get(key_snake)
|
|
324
|
+
if key_camel and key_camel in props:
|
|
325
|
+
col['value'] = props.get(key_camel)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
# Be resilient; log and continue
|
|
328
|
+
logger.debug(f"populate_columns_from_properties: skipping column due to error: {e}")
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
return columns_struct
|
|
332
|
+
|
|
333
|
+
def populate_feedback(element: dict, columns_struct: dict) -> dict:
|
|
334
|
+
"""
|
|
335
|
+
Populate a columns_struct with values from the element's feedback.
|
|
336
|
+
|
|
337
|
+
The element dict may have feedback relationships. If these are present in the column request, extract them
|
|
338
|
+
and add the values to the columns_struct.
|
|
339
|
+
The columns_struct is expected to follow the format returned by select_report_spec, where
|
|
340
|
+
columns are located at columns_struct['formats']['columns'] and each column is a dict containing
|
|
341
|
+
at least a 'key' field expressed in snake_case. For each column whose snake_case key corresponds
|
|
342
|
+
to a key in the element properties (after converting to camelCase), this function adds a 'value'
|
|
343
|
+
entry to the column with the matching property's value.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
element: The element containing a 'properties' dict with camelCase keys.
|
|
347
|
+
columns_struct: The columns structure whose columns have snake_case 'key' fields.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
The updated columns_struct (the input structure is modified in place and also returned).
|
|
351
|
+
"""
|
|
352
|
+
if not isinstance(columns_struct, dict):
|
|
353
|
+
return columns_struct
|
|
354
|
+
|
|
355
|
+
# Need to think more about what I want to produce
|
|
356
|
+
return columns_struct
|
|
357
|
+
props = {}
|
|
358
|
+
|
|
359
|
+
# Get the attributes list if present
|
|
360
|
+
formats = columns_struct.get('formats') or {}
|
|
361
|
+
columns = formats.get('attributes') if isinstance(formats, dict) else None
|
|
362
|
+
if not isinstance(columns, list):
|
|
363
|
+
return columns_struct
|
|
364
|
+
|
|
365
|
+
for col in columns:
|
|
366
|
+
try:
|
|
367
|
+
key_snake = col.get('key') if isinstance(col, dict) else None
|
|
368
|
+
if not key_snake:
|
|
369
|
+
continue
|
|
370
|
+
# Convert the snake_case key to camelCase to look up in properties
|
|
371
|
+
key_camel = to_camel_case(key_snake)
|
|
372
|
+
if key_camel in props:
|
|
373
|
+
col['value'] = props.get(key_camel)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
# Be resilient; log and continue
|
|
376
|
+
logger.debug(f"populate_columns_from_properties: skipping column due to error: {e}")
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
return columns_struct
|
|
380
|
+
|
|
381
|
+
def get_required_relationships(element: dict, columns_struct: dict) -> dict:
|
|
382
|
+
"""
|
|
383
|
+
Populate relationship-derived column values in columns_struct based on top-level keys in the element.
|
|
384
|
+
|
|
385
|
+
NOTE: This function is now a lightweight wrapper around the optimized logic in populate_common_columns.
|
|
386
|
+
For best performance, use populate_common_columns directly with include_relationships=True.
|
|
387
|
+
|
|
388
|
+
This function inspects the requested columns in columns_struct, converts each column key from
|
|
389
|
+
snake_case to camelCase, and if a matching top-level key exists in the element, parses that value
|
|
390
|
+
(typically lists of relationship beans) into a human-readable value (e.g., a comma-separated list
|
|
391
|
+
of qualified names) and stores it under the column's 'value'. Columns not specified in the
|
|
392
|
+
columns_struct are ignored. Existing non-empty 'value's are left as-is.
|
|
393
|
+
|
|
394
|
+
Example: if a column with key 'member_of_collections' is present, this function will look for the
|
|
395
|
+
top-level key 'memberOfCollections' in the element and derive a value if found.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
element: The element dictionary containing top-level relationship lists (e.g., associatedGlossaries,
|
|
399
|
+
memberOfCollections, collectionMembers).
|
|
400
|
+
columns_struct: The columns structure to augment with derived 'value's.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
The updated columns_struct (modified in place and returned).
|
|
404
|
+
"""
|
|
405
|
+
if not isinstance(columns_struct, dict):
|
|
406
|
+
return columns_struct
|
|
407
|
+
|
|
408
|
+
formats = columns_struct.get('formats') or {}
|
|
409
|
+
columns = formats.get('attributes') if isinstance(formats, dict) else None
|
|
410
|
+
if not isinstance(columns, list):
|
|
411
|
+
return columns_struct
|
|
412
|
+
|
|
413
|
+
# Pre-compute relationship values for efficiency
|
|
414
|
+
relationship_values = {}
|
|
415
|
+
for col in columns:
|
|
416
|
+
if not isinstance(col, dict):
|
|
417
|
+
continue
|
|
418
|
+
key_snake = col.get('key')
|
|
419
|
+
if not key_snake:
|
|
420
|
+
continue
|
|
421
|
+
# If already has a non-empty value, don't overwrite
|
|
422
|
+
if col.get('value') not in (None, ""):
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
key_camel = to_camel_case(key_snake)
|
|
426
|
+
if key_camel not in element:
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
top_val = element.get(key_camel)
|
|
430
|
+
derived_value = ""
|
|
431
|
+
if isinstance(top_val, list):
|
|
432
|
+
names = []
|
|
433
|
+
for item in top_val:
|
|
434
|
+
nm = _extract_name_from_relationship_item(item)
|
|
435
|
+
if nm:
|
|
436
|
+
names.append(nm)
|
|
437
|
+
derived_value = ", ".join(names)
|
|
438
|
+
elif isinstance(top_val, dict):
|
|
439
|
+
nm = _extract_name_from_relationship_item(top_val)
|
|
440
|
+
derived_value = nm or ""
|
|
441
|
+
else:
|
|
442
|
+
derived_value = str(top_val) if top_val is not None else ""
|
|
443
|
+
|
|
444
|
+
if derived_value:
|
|
445
|
+
relationship_values[key_snake] = derived_value
|
|
446
|
+
|
|
447
|
+
# Apply the values
|
|
448
|
+
for col in columns:
|
|
449
|
+
if isinstance(col, dict):
|
|
450
|
+
key = col.get('key')
|
|
451
|
+
if key and key in relationship_values and col.get('value') in (None, ""):
|
|
452
|
+
col['value'] = relationship_values[key]
|
|
453
|
+
|
|
454
|
+
return columns_struct
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def generate_entity_md(elements: List[Dict],
|
|
460
|
+
elements_action: str,
|
|
461
|
+
output_format: str,
|
|
462
|
+
entity_type: str,
|
|
463
|
+
extract_properties_func: Callable,
|
|
464
|
+
get_additional_props_func: Optional[Callable] = None,
|
|
465
|
+
columns_struct: [dict] = None) -> str:
|
|
466
|
+
"""
|
|
467
|
+
Generic method to generate markdown for entities.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
elements (list): List of entity elements
|
|
471
|
+
elements_action (str): Action description for elements
|
|
472
|
+
output_format (str): Output format
|
|
473
|
+
entity_type (str): Type of entity (Glossary, Term, Category, etc.)
|
|
474
|
+
extract_properties_func: Function to extract properties from an element
|
|
475
|
+
get_additional_props_func: Optional function to get additional properties
|
|
476
|
+
columns (list): List of column name structures
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
str: Markdown representation
|
|
480
|
+
"""
|
|
481
|
+
heading = columns_struct.get("heading")
|
|
482
|
+
if heading == "Default Base Attributes":
|
|
483
|
+
elements_md = "## Reporting on Default Base Attributes - Perhaps couldn't find a valid combination of report_spec and output_format?\n\n"
|
|
484
|
+
else:
|
|
485
|
+
elements_md = ""
|
|
486
|
+
base_columns = columns_struct['formats'].get('attributes') if columns_struct else None
|
|
487
|
+
|
|
488
|
+
for element in elements:
|
|
489
|
+
if element is None:
|
|
490
|
+
continue
|
|
491
|
+
guid = element.get('elementHeader', {}).get('guid')
|
|
492
|
+
|
|
493
|
+
# Prefer new behavior: extractor returns an updated columns_struct with values
|
|
494
|
+
returned_struct = None
|
|
495
|
+
if columns_struct is not None:
|
|
496
|
+
try:
|
|
497
|
+
returned_struct = extract_properties_func(element, columns_struct)
|
|
498
|
+
except TypeError:
|
|
499
|
+
# Fallback for legacy extractors without columns_struct parameter
|
|
500
|
+
returned_struct = None
|
|
501
|
+
|
|
502
|
+
# Legacy fallback: get props dict if no columns_struct provided/returned
|
|
503
|
+
props = {}
|
|
504
|
+
if returned_struct is None:
|
|
505
|
+
props = extract_properties_func(element) if callable(extract_properties_func) else {}
|
|
506
|
+
|
|
507
|
+
# Get additional properties if function is provided
|
|
508
|
+
additional_props = {}
|
|
509
|
+
if get_additional_props_func:
|
|
510
|
+
# Use guid if available, else try to get from props
|
|
511
|
+
guid_for_fmt = guid or props.get('GUID')
|
|
512
|
+
additional_props = get_additional_props_func(element, guid_for_fmt, output_format)
|
|
513
|
+
|
|
514
|
+
# Determine display name
|
|
515
|
+
display_name = None
|
|
516
|
+
if returned_struct is not None:
|
|
517
|
+
cols = returned_struct.get('formats', {}).get('attributes', [])
|
|
518
|
+
# Find value from 'display_name' or 'title'
|
|
519
|
+
for col in cols:
|
|
520
|
+
if col.get('key') in ('display_name', 'title', 'keyword'):
|
|
521
|
+
display_name = col.get('value')
|
|
522
|
+
if display_name:
|
|
523
|
+
break
|
|
524
|
+
else:
|
|
525
|
+
display_name = props.get('display_name') or props.get('title') or props.get('keyword')
|
|
526
|
+
|
|
527
|
+
if display_name is None and (keyword:= element.get('properties',{}).get('keyword',None)) is not None:
|
|
528
|
+
display_name = keyword
|
|
529
|
+
elif display_name is None:
|
|
530
|
+
display_name = "NO DISPLAY NAME"
|
|
531
|
+
|
|
532
|
+
# Format header based on output format
|
|
533
|
+
if output_format in ['FORM', 'MD']:
|
|
534
|
+
elements_md += f"# {elements_action}\n\n"
|
|
535
|
+
elements_md += f"## {entity_type} Name \n\n{display_name}\n\n"
|
|
536
|
+
elif output_format == 'REPORT':
|
|
537
|
+
elements_md += f'<a id="{(guid or props.get("GUID") or "No GUID" )}"></a>\n# {entity_type} Name: {display_name}\n\n'
|
|
538
|
+
else:
|
|
539
|
+
elements_md += f"## {entity_type} Name \n\n{display_name}\n\n"
|
|
540
|
+
|
|
541
|
+
# Add attributes based on column spec if available, otherwise, add all (legacy)
|
|
542
|
+
if returned_struct is not None:
|
|
543
|
+
cols = returned_struct.get('formats', {}).get('attributes', [])
|
|
544
|
+
for column in cols:
|
|
545
|
+
name = column.get('name')
|
|
546
|
+
key = column.get('key')
|
|
547
|
+
value = column.get('value')
|
|
548
|
+
if value in (None, "") and key in additional_props:
|
|
549
|
+
value = additional_props[key]
|
|
550
|
+
if column.get('format'):
|
|
551
|
+
value = format_for_markdown_table(value, guid)
|
|
552
|
+
elements_md += make_md_attribute(name, value, output_format)
|
|
553
|
+
if wk := returned_struct.get("annotations", {}).get("wikilinks", None):
|
|
554
|
+
elements_md += ", ".join(wk)
|
|
555
|
+
elif base_columns:
|
|
556
|
+
# If we have columns but extractor didn't return struct, use legacy props lookup
|
|
557
|
+
for column in base_columns:
|
|
558
|
+
key = column['key']
|
|
559
|
+
name = column['name']
|
|
560
|
+
value = ""
|
|
561
|
+
if key in props:
|
|
562
|
+
value = props[key]
|
|
563
|
+
elif key in additional_props:
|
|
564
|
+
value = additional_props[key]
|
|
565
|
+
if column.get('format'):
|
|
566
|
+
value = format_for_markdown_table(value, guid or props.get('GUID'))
|
|
567
|
+
elements_md += make_md_attribute(name, value, output_format)
|
|
568
|
+
if wk := columns_struct.get("annotations", {}).get("wikilinks", None):
|
|
569
|
+
elements_md += ", ".join(wk)
|
|
570
|
+
else:
|
|
571
|
+
# Legacy path without columns: dump all props
|
|
572
|
+
for key, value in props.items():
|
|
573
|
+
if output_format in ['FORM', 'MD', 'DICT'] and key == 'mermaid':
|
|
574
|
+
continue
|
|
575
|
+
if key not in ['properties', 'display_name']:
|
|
576
|
+
if key == 'mermaid' and value == '':
|
|
577
|
+
continue
|
|
578
|
+
elements_md += make_md_attribute(key.replace('_', ' '), value, output_format)
|
|
579
|
+
for key, value in additional_props.items():
|
|
580
|
+
elements_md += make_md_attribute(key.replace('_', ' '), value, output_format)
|
|
581
|
+
|
|
582
|
+
if element != elements[-1]:
|
|
583
|
+
elements_md += MD_SEPARATOR
|
|
584
|
+
|
|
585
|
+
return elements_md
|
|
586
|
+
|
|
587
|
+
def generate_entity_md_table(elements: List[Dict],
|
|
588
|
+
search_string: str,
|
|
589
|
+
entity_type: str,
|
|
590
|
+
extract_properties_func: Callable,
|
|
591
|
+
columns_struct: dict,
|
|
592
|
+
get_additional_props_func: Optional[Callable] = None,
|
|
593
|
+
output_format: str = 'LIST') -> str:
|
|
594
|
+
"""
|
|
595
|
+
Generic method to generate a markdown table for entities.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
elements (list): List of entity elements
|
|
599
|
+
search_string (str): The search string used
|
|
600
|
+
entity_type (str): Type of entity (Glossary, Term, Category, etc.)
|
|
601
|
+
extract_properties_func: Function to extract properties from an element
|
|
602
|
+
columns: List of column definitions, each containing 'name', 'key', and 'format' (optional)
|
|
603
|
+
get_additional_props_func: Optional function to get additional properties
|
|
604
|
+
output_format (str): Output format (FORM, REPORT, LIST, etc.)
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
str: Markdown table
|
|
608
|
+
"""
|
|
609
|
+
# Handle pluralization - if entity_type ends with 'y', use 'ies' instead of 's'
|
|
610
|
+
target_type = columns_struct.get('target_type', entity_type)
|
|
611
|
+
# if target_type.endswith('y'):
|
|
612
|
+
# target_type = target_type.replace('y', 'ies')
|
|
613
|
+
# else:
|
|
614
|
+
# target_type = target_type.replace('s', 's')
|
|
615
|
+
|
|
616
|
+
entity_type_plural = f"{target_type[:-1]}ies" if target_type.endswith('y') else f"{target_type}s"
|
|
617
|
+
# entity_type_plural = target_type
|
|
618
|
+
columns = columns_struct['formats'].get('attributes', [])
|
|
619
|
+
heading = columns_struct.get("heading")
|
|
620
|
+
if heading == "Default Base Attributes":
|
|
621
|
+
elements_md = "## Reporting on Default Base Attributes - Perhaps couldn't find a valid combination of report_spec and output_format?\n\n"
|
|
622
|
+
else:
|
|
623
|
+
elements_md = ""
|
|
624
|
+
|
|
625
|
+
if output_format == "LIST":
|
|
626
|
+
elements_md = f"# {entity_type_plural} Table\n\n"
|
|
627
|
+
elements_md += f"{entity_type_plural} found from the search string: `{search_string}`\n\n"
|
|
628
|
+
|
|
629
|
+
# Add column headers
|
|
630
|
+
header_row = "| "
|
|
631
|
+
separator_row = "|"
|
|
632
|
+
for column in columns:
|
|
633
|
+
header_row += f"{column['name']} | "
|
|
634
|
+
separator_row += "-------------|"
|
|
635
|
+
|
|
636
|
+
elements_md += header_row + "\n"
|
|
637
|
+
elements_md += separator_row + "\n"
|
|
638
|
+
|
|
639
|
+
for element in elements:
|
|
640
|
+
guid = element.get('elementHeader', {}).get('guid', None)
|
|
641
|
+
|
|
642
|
+
# Extractor returns columns_struct with values when possible
|
|
643
|
+
# Use shallow copy and reset only column values for performance
|
|
644
|
+
local_columns_struct = columns_struct.copy()
|
|
645
|
+
if 'formats' in local_columns_struct and 'attributes' in local_columns_struct['formats']:
|
|
646
|
+
# Create new list of columns with reset values
|
|
647
|
+
local_columns_struct['formats'] = local_columns_struct['formats'].copy()
|
|
648
|
+
local_columns_struct['formats']['attributes'] = [
|
|
649
|
+
{**col, 'value': None} if isinstance(col, dict) else col
|
|
650
|
+
for col in local_columns_struct['formats']['attributes']
|
|
651
|
+
]
|
|
652
|
+
try:
|
|
653
|
+
returned_struct = extract_properties_func(element, local_columns_struct)
|
|
654
|
+
except TypeError:
|
|
655
|
+
returned_struct = None
|
|
656
|
+
|
|
657
|
+
# For help mode, bypass extraction
|
|
658
|
+
if output_format == "help":
|
|
659
|
+
returned_struct = {"formats": {"attributes": columns}}
|
|
660
|
+
|
|
661
|
+
# Additional props (if any)
|
|
662
|
+
additional_props = {}
|
|
663
|
+
if get_additional_props_func:
|
|
664
|
+
additional_props = get_additional_props_func(element, guid, output_format)
|
|
665
|
+
|
|
666
|
+
# Build row
|
|
667
|
+
row = "| "
|
|
668
|
+
if returned_struct is not None:
|
|
669
|
+
for column in returned_struct.get('formats', {}).get('attributes', []):
|
|
670
|
+
key = column.get('key')
|
|
671
|
+
value = column.get('value')
|
|
672
|
+
if (value in (None, "")) and key in additional_props:
|
|
673
|
+
value = additional_props[key]
|
|
674
|
+
|
|
675
|
+
if value in (None, ""):
|
|
676
|
+
value = " --- "
|
|
677
|
+
|
|
678
|
+
if column.get('format'):
|
|
679
|
+
value = format_for_markdown_table(value, guid)
|
|
680
|
+
elif isinstance(value, str):
|
|
681
|
+
value = value.replace("\n", " ").replace("|", "\\|")
|
|
682
|
+
|
|
683
|
+
row += f"{value} | "
|
|
684
|
+
else:
|
|
685
|
+
# Legacy fallback: read from props dict
|
|
686
|
+
props = extract_properties_func(element)
|
|
687
|
+
for column in columns:
|
|
688
|
+
key = column['key']
|
|
689
|
+
value = " "
|
|
690
|
+
if key in props:
|
|
691
|
+
value = props[key]
|
|
692
|
+
elif key in additional_props:
|
|
693
|
+
value = additional_props[key]
|
|
694
|
+
|
|
695
|
+
if value in (None, "", " "):
|
|
696
|
+
value = " --- "
|
|
697
|
+
|
|
698
|
+
if column.get('format'):
|
|
699
|
+
value = format_for_markdown_table(value, guid or props.get('GUID'))
|
|
700
|
+
elif isinstance(value, str):
|
|
701
|
+
value = value.replace("\n", " ").replace("|", "\\|")
|
|
702
|
+
|
|
703
|
+
row += f"{value} | "
|
|
704
|
+
|
|
705
|
+
elements_md += row + "\n"
|
|
706
|
+
# if wk := columns_struct.get("annotations",{}).get("wikilinks", None):
|
|
707
|
+
# elements_md += ", ".join(wk)
|
|
708
|
+
return elements_md
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def generate_entity_dict(elements: List[Dict],
|
|
712
|
+
extract_properties_func: Callable,
|
|
713
|
+
get_additional_props_func: Optional[Callable] = None,
|
|
714
|
+
include_keys: Optional[List[str]] = None,
|
|
715
|
+
exclude_keys: Optional[List[str]] = None,
|
|
716
|
+
columns_struct: dict = None,
|
|
717
|
+
output_format: str = 'DICT') -> List[Dict]:
|
|
718
|
+
"""
|
|
719
|
+
Generic method to generate a dictionary representation of entities.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
elements (list): List of entity elements
|
|
723
|
+
extract_properties_func: Function to extract properties from an element
|
|
724
|
+
get_additional_props_func: Optional function to get additional properties
|
|
725
|
+
include_keys: Optional list of keys to include in the result (if None, include all)
|
|
726
|
+
exclude_keys: Optional list of keys to exclude from the result (if None, exclude none)
|
|
727
|
+
columns_struct: Optional dict of columns to include (if None, include all)
|
|
728
|
+
output_format (str): Output format (FORM, REPORT, DICT, etc.)
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
list: List of entity dictionaries
|
|
732
|
+
"""
|
|
733
|
+
result = []
|
|
734
|
+
|
|
735
|
+
#####
|
|
736
|
+
# Add attributes based on column spec if available, otherwise, add all
|
|
737
|
+
import copy
|
|
738
|
+
for element in elements:
|
|
739
|
+
if not isinstance(element, dict):
|
|
740
|
+
continue
|
|
741
|
+
|
|
742
|
+
guid = element.get('elementHeader', {}).get('guid')
|
|
743
|
+
|
|
744
|
+
# Use shallow copy and reset only column values for performance
|
|
745
|
+
local_columns_struct = None
|
|
746
|
+
if columns_struct is not None:
|
|
747
|
+
local_columns_struct = columns_struct.copy()
|
|
748
|
+
if 'formats' in local_columns_struct and 'attributes' in local_columns_struct['formats']:
|
|
749
|
+
# Create new list of columns with reset values
|
|
750
|
+
local_columns_struct['formats'] = local_columns_struct['formats'].copy()
|
|
751
|
+
local_columns_struct['formats']['attributes'] = [
|
|
752
|
+
{**col, 'value': None} if isinstance(col, dict) else col
|
|
753
|
+
for col in local_columns_struct['formats']['attributes']
|
|
754
|
+
]
|
|
755
|
+
|
|
756
|
+
returned_struct = None
|
|
757
|
+
if local_columns_struct is not None:
|
|
758
|
+
try:
|
|
759
|
+
returned_struct = extract_properties_func(element, local_columns_struct)
|
|
760
|
+
except TypeError as e:
|
|
761
|
+
logger.info(f"Error - didn't find extractor?: {e}")
|
|
762
|
+
returned_struct = None
|
|
763
|
+
|
|
764
|
+
# Get additional properties if function is provided
|
|
765
|
+
additional_props = {}
|
|
766
|
+
if get_additional_props_func:
|
|
767
|
+
additional_props = get_additional_props_func(element, guid, output_format)
|
|
768
|
+
|
|
769
|
+
# Create entity dictionary
|
|
770
|
+
entity_dict = {}
|
|
771
|
+
|
|
772
|
+
cols = local_columns_struct['formats'].get('attributes', None) if local_columns_struct else None
|
|
773
|
+
if returned_struct is not None:
|
|
774
|
+
for column in returned_struct.get('formats', {}).get('attributes', []):
|
|
775
|
+
key = column.get('key')
|
|
776
|
+
name = column.get('name')
|
|
777
|
+
value = column.get('value')
|
|
778
|
+
if (value in (None, "")) and key in additional_props:
|
|
779
|
+
value = additional_props[key]
|
|
780
|
+
if column.get('format'):
|
|
781
|
+
value = format_for_markdown_table(value, guid)
|
|
782
|
+
# Avoid overwriting when multiple columns share the same display name in a spec
|
|
783
|
+
dict_key = name
|
|
784
|
+
if dict_key in entity_dict:
|
|
785
|
+
logger.warning(f"DICT key collision for display name '{dict_key}'. Suffixing duplicate to preserve all values.")
|
|
786
|
+
suffix_idx = 1
|
|
787
|
+
tmp_key = f"{dict_key}_{suffix_idx}"
|
|
788
|
+
while tmp_key in entity_dict:
|
|
789
|
+
suffix_idx += 1
|
|
790
|
+
tmp_key = f"{dict_key}_{suffix_idx}"
|
|
791
|
+
dict_key = tmp_key
|
|
792
|
+
entity_dict[dict_key] = value
|
|
793
|
+
elif cols:
|
|
794
|
+
for column in cols:
|
|
795
|
+
key = column['key']
|
|
796
|
+
name = column['name']
|
|
797
|
+
value = ""
|
|
798
|
+
props = extract_properties_func(element, columns_struct)
|
|
799
|
+
if key in props:
|
|
800
|
+
value = props[key]
|
|
801
|
+
elif key in additional_props:
|
|
802
|
+
value = additional_props[key]
|
|
803
|
+
if column.get('format', None):
|
|
804
|
+
value = format_for_markdown_table(value, guid or props.get('GUID'))
|
|
805
|
+
dict_key = name
|
|
806
|
+
if dict_key in entity_dict:
|
|
807
|
+
logger.warning(f"DICT key collision for display name '{dict_key}'. Suffixing duplicate to preserve all values.")
|
|
808
|
+
suffix_idx = 1
|
|
809
|
+
tmp_key = f"{dict_key}_{suffix_idx}"
|
|
810
|
+
while tmp_key in entity_dict:
|
|
811
|
+
suffix_idx += 1
|
|
812
|
+
tmp_key = f"{dict_key}_{suffix_idx}"
|
|
813
|
+
dict_key = tmp_key
|
|
814
|
+
entity_dict[dict_key] = value
|
|
815
|
+
else:
|
|
816
|
+
props = extract_properties_func(element, local_columns_struct)
|
|
817
|
+
# Add properties based on include/exclude lists
|
|
818
|
+
for key, value in props.items():
|
|
819
|
+
if key not in ['properties', 'mermaid']: # Skip the raw properties object
|
|
820
|
+
if (include_keys is None or key in include_keys) and (
|
|
821
|
+
exclude_keys is None or key not in exclude_keys):
|
|
822
|
+
entity_dict[key] = value
|
|
823
|
+
|
|
824
|
+
# Add additional properties
|
|
825
|
+
for key, value in additional_props.items():
|
|
826
|
+
if (include_keys is None or key in include_keys) and (exclude_keys is None or key not in exclude_keys):
|
|
827
|
+
entity_dict[key] = value
|
|
828
|
+
|
|
829
|
+
result.append(entity_dict)
|
|
830
|
+
#####
|
|
831
|
+
# for element in elements:
|
|
832
|
+
# if element is None:
|
|
833
|
+
# continue
|
|
834
|
+
# props = extract_properties_func(element)
|
|
835
|
+
#
|
|
836
|
+
# # Get additional properties if function is provided
|
|
837
|
+
# additional_props = {}
|
|
838
|
+
# if get_additional_props_func:
|
|
839
|
+
# additional_props = get_additional_props_func(element,props['GUID'], output_format)
|
|
840
|
+
#
|
|
841
|
+
# # Create entity dictionary
|
|
842
|
+
# entity_dict = {}
|
|
843
|
+
#
|
|
844
|
+
# # Add properties based on include/exclude lists
|
|
845
|
+
# for key, value in props.items():
|
|
846
|
+
# if key not in [ 'properties', 'mermaid']: # Skip the raw properties object
|
|
847
|
+
# if (include_keys is None or key in include_keys) and (
|
|
848
|
+
# exclude_keys is None or key not in exclude_keys):
|
|
849
|
+
# entity_dict[key] = value
|
|
850
|
+
#
|
|
851
|
+
# # Add additional properties
|
|
852
|
+
# for key, value in additional_props.items():
|
|
853
|
+
# if (include_keys is None or key in include_keys) and (exclude_keys is None or key not in exclude_keys):
|
|
854
|
+
# entity_dict[key] = value
|
|
855
|
+
#
|
|
856
|
+
# result.append(entity_dict)
|
|
857
|
+
|
|
858
|
+
return result
|
|
859
|
+
|
|
860
|
+
def resolve_output_formats(entity_type: str,
|
|
861
|
+
output_format: str,
|
|
862
|
+
report_spec: Optional[Union[str, dict]] = None,
|
|
863
|
+
default_label: Optional[str] = None,
|
|
864
|
+
**kwargs) -> Optional[dict]:
|
|
865
|
+
"""
|
|
866
|
+
Resolve a report format structure given an entity type, the desired output format
|
|
867
|
+
(e.g., DICT, LIST, MD, REPORT, FORM), and either a label (str) or a dict of format sets.
|
|
868
|
+
|
|
869
|
+
Notes:
|
|
870
|
+
- Preferred naming is report_spec (formerly output_format); compatibility for the
|
|
871
|
+
old 'output_format_spec' terminology has been removed.
|
|
872
|
+
|
|
873
|
+
Selection order:
|
|
874
|
+
- If report_spec is a str: select by label.
|
|
875
|
+
- If report_spec is a dict: use get_report_spec_match to pick a matching format.
|
|
876
|
+
- Else: try selecting by entity_type or default_label.
|
|
877
|
+
- Fallback: select "Default".
|
|
878
|
+
"""
|
|
879
|
+
from pyegeria.view.base_report_formats import get_report_spec_match
|
|
880
|
+
|
|
881
|
+
if report_spec is None and isinstance(kwargs, dict):
|
|
882
|
+
if 'report_spec' in kwargs:
|
|
883
|
+
report_spec = kwargs.get('report_spec')
|
|
884
|
+
elif 'report_format' in kwargs:
|
|
885
|
+
# Still accept 'report_format' as a synonym for report_spec
|
|
886
|
+
report_spec = kwargs.get('report_format')
|
|
887
|
+
|
|
888
|
+
if isinstance(report_spec, str):
|
|
889
|
+
return select_report_format(report_spec, output_format)
|
|
890
|
+
if isinstance(report_spec, dict):
|
|
891
|
+
return get_report_spec_match(report_spec, output_format)
|
|
892
|
+
|
|
893
|
+
label = default_label or entity_type
|
|
894
|
+
fmt = select_report_format(label, output_format)
|
|
895
|
+
if fmt is None:
|
|
896
|
+
fmt = select_report_format("Default", output_format)
|
|
897
|
+
return fmt
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def overlay_additional_values(columns_struct: dict, extra: Optional[dict]) -> dict:
|
|
901
|
+
"""
|
|
902
|
+
Overlay extra values into columns_struct only where the column's value is empty or missing.
|
|
903
|
+
Returns the modified columns_struct.
|
|
904
|
+
"""
|
|
905
|
+
if not isinstance(columns_struct, dict) or not extra:
|
|
906
|
+
return columns_struct
|
|
907
|
+
columns = columns_struct.get('formats', {}).get('attributes')
|
|
908
|
+
if not isinstance(columns, list):
|
|
909
|
+
return columns_struct
|
|
910
|
+
for col in columns:
|
|
911
|
+
if not isinstance(col, dict):
|
|
912
|
+
continue
|
|
913
|
+
key = col.get('key')
|
|
914
|
+
if not key:
|
|
915
|
+
continue
|
|
916
|
+
if col.get('value') in (None, "") and key in extra:
|
|
917
|
+
col['value'] = extra[key]
|
|
918
|
+
return columns_struct
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def populate_common_columns(
|
|
922
|
+
element: dict,
|
|
923
|
+
columns_struct: dict,
|
|
924
|
+
*,
|
|
925
|
+
include_header: bool = True,
|
|
926
|
+
include_relationships: bool = True,
|
|
927
|
+
include_subject_area: bool = True,
|
|
928
|
+
mermaid_source_key: str = 'mermaidGraph',
|
|
929
|
+
mermaid_dest_key: str = 'mermaid'
|
|
930
|
+
) -> dict:
|
|
931
|
+
"""
|
|
932
|
+
Populate the common columns in columns_struct based on a standard Egeria element shape.
|
|
933
|
+
|
|
934
|
+
Steps:
|
|
935
|
+
- Populate from element.properties (camelCase mapped from snake_case keys)
|
|
936
|
+
- Optionally overlay header-derived values (GUID, type_name, times, etc.)
|
|
937
|
+
- Optionally populate relationship-based columns via get_required_relationships
|
|
938
|
+
- Optionally populate subject_area from element.elementHeader.subjectArea.classificationProperties.subjectAreaName
|
|
939
|
+
- If a column with key == mermaid_dest_key is present, set it from mermaid_source_key
|
|
940
|
+
- Do not overwrite non-empty values already set
|
|
941
|
+
"""
|
|
942
|
+
# Pre-compute values that will be reused
|
|
943
|
+
props = element.get('properties', {}) if isinstance(element, dict) else {}
|
|
944
|
+
header_props = _extract_referenceable_properties(element) if include_header else {}
|
|
945
|
+
guid = header_props.get('GUID') if include_header else None
|
|
946
|
+
|
|
947
|
+
# Pre-compute subject area value
|
|
948
|
+
subject_area_val = ""
|
|
949
|
+
if include_subject_area:
|
|
950
|
+
try:
|
|
951
|
+
subject_area = element.get('elementHeader', {}).get('subjectArea') or ""
|
|
952
|
+
if isinstance(subject_area, dict):
|
|
953
|
+
subject_area_val = subject_area.get('classificationProperties', {}).get('subjectAreaName', '')
|
|
954
|
+
except Exception as e:
|
|
955
|
+
logger.debug(f"populate_common_columns: subject_area handling error: {e}")
|
|
956
|
+
|
|
957
|
+
# Pre-compute mermaid value
|
|
958
|
+
mermaid_val = element.get(mermaid_source_key, '') or ''
|
|
959
|
+
|
|
960
|
+
# Pre-compute relationship values for efficiency
|
|
961
|
+
relationship_values = {}
|
|
962
|
+
if include_relationships:
|
|
963
|
+
formats = columns_struct.get('formats') or {}
|
|
964
|
+
columns = formats.get('attributes') if isinstance(formats, dict) else None
|
|
965
|
+
if isinstance(columns, list):
|
|
966
|
+
for col in columns:
|
|
967
|
+
if not isinstance(col, dict):
|
|
968
|
+
continue
|
|
969
|
+
key_snake = col.get('key')
|
|
970
|
+
if not key_snake:
|
|
971
|
+
continue
|
|
972
|
+
key_camel = to_camel_case(key_snake)
|
|
973
|
+
if key_camel in element:
|
|
974
|
+
top_val = element.get(key_camel)
|
|
975
|
+
derived_value = ""
|
|
976
|
+
if isinstance(top_val, list):
|
|
977
|
+
names = []
|
|
978
|
+
for item in top_val:
|
|
979
|
+
nm = _extract_name_from_relationship_item(item)
|
|
980
|
+
if nm:
|
|
981
|
+
names.append(nm)
|
|
982
|
+
derived_value = ", ".join(names)
|
|
983
|
+
elif isinstance(top_val, dict):
|
|
984
|
+
nm = _extract_name_from_relationship_item(top_val)
|
|
985
|
+
derived_value = nm or ""
|
|
986
|
+
else:
|
|
987
|
+
derived_value = str(top_val) if top_val is not None else ""
|
|
988
|
+
if derived_value:
|
|
989
|
+
relationship_values[key_snake] = derived_value
|
|
990
|
+
|
|
991
|
+
# Single pass through columns - consolidate all operations
|
|
992
|
+
col_data = columns_struct
|
|
993
|
+
columns_list = col_data.get('formats', {}).get('attributes', [])
|
|
994
|
+
|
|
995
|
+
for column in columns_list:
|
|
996
|
+
if not isinstance(column, dict):
|
|
997
|
+
continue
|
|
998
|
+
|
|
999
|
+
key = column.get('key')
|
|
1000
|
+
if not key:
|
|
1001
|
+
continue
|
|
1002
|
+
|
|
1003
|
+
# Skip if already has a value
|
|
1004
|
+
if column.get('value') not in (None, ""):
|
|
1005
|
+
continue
|
|
1006
|
+
|
|
1007
|
+
# 1) Try properties (camelCase conversion)
|
|
1008
|
+
key_camel = to_camel_case(key)
|
|
1009
|
+
if key_camel in props:
|
|
1010
|
+
column['value'] = props.get(key_camel)
|
|
1011
|
+
continue
|
|
1012
|
+
|
|
1013
|
+
# 2) Try header properties
|
|
1014
|
+
if include_header:
|
|
1015
|
+
if key in header_props:
|
|
1016
|
+
column['value'] = header_props.get(key)
|
|
1017
|
+
continue
|
|
1018
|
+
elif isinstance(key, str) and key.lower() == 'guid':
|
|
1019
|
+
column['value'] = guid
|
|
1020
|
+
continue
|
|
1021
|
+
|
|
1022
|
+
# 3) Try relationship values
|
|
1023
|
+
if include_relationships and key in relationship_values:
|
|
1024
|
+
column['value'] = relationship_values[key]
|
|
1025
|
+
continue
|
|
1026
|
+
|
|
1027
|
+
# 4) Try subject area
|
|
1028
|
+
if include_subject_area and key == 'subject_area':
|
|
1029
|
+
column['value'] = subject_area_val
|
|
1030
|
+
continue
|
|
1031
|
+
|
|
1032
|
+
# 5) Try mermaid
|
|
1033
|
+
if key == mermaid_dest_key:
|
|
1034
|
+
column['value'] = mermaid_val
|
|
1035
|
+
continue
|
|
1036
|
+
if key in MERMAID_GRAPHS and mermaid_dest_key not in MERMAID_GRAPHS:
|
|
1037
|
+
column['value'] = element.get(key, '')
|
|
1038
|
+
continue
|
|
1039
|
+
|
|
1040
|
+
# Get any Feedback Values that have been requested (kept separate as it may have side effects)
|
|
1041
|
+
col_data = populate_feedback(element, col_data)
|
|
1042
|
+
|
|
1043
|
+
return col_data
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def _extract_name_from_relationship_item(item: Any) -> Optional[str]:
|
|
1047
|
+
"""Best-effort extraction of a display/qualified name from a relationship item."""
|
|
1048
|
+
try:
|
|
1049
|
+
if isinstance(item, dict):
|
|
1050
|
+
# Common pattern: item['relatedElement']['properties']['qualifiedName']
|
|
1051
|
+
related = item.get('relatedElement') or item.get('related_element')
|
|
1052
|
+
if isinstance(related, dict):
|
|
1053
|
+
props = related.get('properties') or {}
|
|
1054
|
+
name = (
|
|
1055
|
+
props.get('qualifiedName')
|
|
1056
|
+
or props.get('displayName')
|
|
1057
|
+
or props.get('name')
|
|
1058
|
+
)
|
|
1059
|
+
if name:
|
|
1060
|
+
return name
|
|
1061
|
+
# Sometimes the properties may be at the top level of the item
|
|
1062
|
+
name = (
|
|
1063
|
+
item.get('qualifiedName')
|
|
1064
|
+
or item.get('displayName')
|
|
1065
|
+
or item.get('name')
|
|
1066
|
+
)
|
|
1067
|
+
if name:
|
|
1068
|
+
return name
|
|
1069
|
+
elif isinstance(item, str):
|
|
1070
|
+
return item
|
|
1071
|
+
except Exception as e:
|
|
1072
|
+
logger.debug(f"_extract_name_from_relationship_item: error extracting name: {e}")
|
|
1073
|
+
return None
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def extract_mermaid_only(elements: Union[Dict, List[Dict]]) -> Union[str, List[str]]:
|
|
1077
|
+
"""
|
|
1078
|
+
Extract mermaid graph data from elements.
|
|
1079
|
+
|
|
1080
|
+
Args:
|
|
1081
|
+
elements: Dictionary or list of dictionaries containing element data
|
|
1082
|
+
|
|
1083
|
+
Returns:
|
|
1084
|
+
String or list of strings containing mermaid graph data
|
|
1085
|
+
"""
|
|
1086
|
+
if isinstance(elements, dict):
|
|
1087
|
+
mer = elements.get('mermaidGraph', None)
|
|
1088
|
+
if mer:
|
|
1089
|
+
return f"\n```mermaid\n{mer}\n```"
|
|
1090
|
+
else:
|
|
1091
|
+
return "---"
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
result = []
|
|
1095
|
+
for element in elements:
|
|
1096
|
+
mer = element.get('mermaidGraph', "---")
|
|
1097
|
+
mer_out = f"\n\n```mermaid\n{mer}\n```" if mer else "---"
|
|
1098
|
+
result.append(mer_out)
|
|
1099
|
+
return result
|
|
1100
|
+
|
|
1101
|
+
def extract_basic_dict(elements: Union[Dict, List[Dict]]) -> Union[Dict, List[Dict]]:
|
|
1102
|
+
"""
|
|
1103
|
+
Extract basic dictionary data from elements.
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
elements: Dictionary or list of dictionaries containing element data
|
|
1107
|
+
|
|
1108
|
+
Returns:
|
|
1109
|
+
Dictionary or list of dictionaries with extracted data
|
|
1110
|
+
"""
|
|
1111
|
+
if isinstance(elements, dict):
|
|
1112
|
+
body = {'guid': elements['elementHeader']['guid']}
|
|
1113
|
+
for key in elements['properties']:
|
|
1114
|
+
body[key] = elements['properties'][key]
|
|
1115
|
+
|
|
1116
|
+
# Add classifications if present
|
|
1117
|
+
classifications = elements['elementHeader'].get('classifications', [])
|
|
1118
|
+
if classifications:
|
|
1119
|
+
classification_names = "["
|
|
1120
|
+
for classification in classifications:
|
|
1121
|
+
if len(classification_names) > 1:
|
|
1122
|
+
classification_names += ", "
|
|
1123
|
+
classification_names += f"{classification['classificationName']}"
|
|
1124
|
+
body['classification_names'] = classification_names + ']'
|
|
1125
|
+
|
|
1126
|
+
return body
|
|
1127
|
+
|
|
1128
|
+
result = []
|
|
1129
|
+
for element in elements:
|
|
1130
|
+
if element is None:
|
|
1131
|
+
continue
|
|
1132
|
+
body = {'guid': element['elementHeader']['guid']}
|
|
1133
|
+
for key in element['properties']:
|
|
1134
|
+
body[key] = element['properties'][key]
|
|
1135
|
+
|
|
1136
|
+
# Add classifications if present
|
|
1137
|
+
classifications = element['elementHeader'].get('classifications', [])
|
|
1138
|
+
if classifications:
|
|
1139
|
+
classification_names = "["
|
|
1140
|
+
for classification in classifications:
|
|
1141
|
+
if len(classification_names) > 1:
|
|
1142
|
+
classification_names += ", "
|
|
1143
|
+
classification_names += f"{classification['classificationName']}"
|
|
1144
|
+
body['classifications'] = classification_names + ']'
|
|
1145
|
+
|
|
1146
|
+
result.append(body)
|
|
1147
|
+
return result
|
|
1148
|
+
|
|
1149
|
+
def _extract_default_properties(self, element: dict, columns_struct: dict) -> dict:
|
|
1150
|
+
props = element.get('properties', {}) or {}
|
|
1151
|
+
normalized = {
|
|
1152
|
+
'properties': props,
|
|
1153
|
+
'elementHeader': element.get('elementHeader', {}),
|
|
1154
|
+
}
|
|
1155
|
+
# Common population pipeline
|
|
1156
|
+
col_data = populate_common_columns(element, columns_struct)
|
|
1157
|
+
columns_list = col_data.get('formats', {}).get('attributes', [])
|
|
1158
|
+
|
|
1159
|
+
return col_data
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _generate_default_output(self, elements: dict | list[dict], search_string: str,
|
|
1163
|
+
element_type_name: str | None,
|
|
1164
|
+
output_format: str = 'DICT',
|
|
1165
|
+
report_format: dict | str | None = None,
|
|
1166
|
+
**kwargs) -> str | list[dict]:
|
|
1167
|
+
entity_type = 'Referenceable' if element_type_name is None else element_type_name
|
|
1168
|
+
# Backward compatibility: accept legacy kwarg
|
|
1169
|
+
if report_format is None and isinstance(kwargs, dict) and 'report_spec' in kwargs:
|
|
1170
|
+
report_format = kwargs.get('report_spec')
|
|
1171
|
+
if report_format:
|
|
1172
|
+
if isinstance(report_format, str):
|
|
1173
|
+
output_formats = select_report_format(report_format, output_format)
|
|
1174
|
+
elif isinstance(report_format, dict):
|
|
1175
|
+
output_formats = get_report_spec_match(report_format, output_format)
|
|
1176
|
+
else:
|
|
1177
|
+
output_formats = None
|
|
1178
|
+
else:
|
|
1179
|
+
output_formats = select_report_format(entity_type, output_format)
|
|
1180
|
+
if output_formats is None:
|
|
1181
|
+
output_formats = select_report_format('Default', output_format)
|
|
1182
|
+
return generate_output(
|
|
1183
|
+
elements=elements,
|
|
1184
|
+
search_string=search_string,
|
|
1185
|
+
entity_type=entity_type,
|
|
1186
|
+
output_format=output_format,
|
|
1187
|
+
extract_properties_func=_extract_default_properties,
|
|
1188
|
+
get_additional_props_func=None,
|
|
1189
|
+
columns_struct=output_formats,
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def generate_output(elements: Union[Dict, List[Dict]],
|
|
1194
|
+
search_string: str,
|
|
1195
|
+
entity_type: str,
|
|
1196
|
+
output_format: str,
|
|
1197
|
+
extract_properties_func: Callable,
|
|
1198
|
+
get_additional_props_func: Optional[Callable] = None,
|
|
1199
|
+
columns_struct: dict = None) -> Union[str, list[dict]]:
|
|
1200
|
+
"""
|
|
1201
|
+
Generate output in the specified format for the given elements.
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
elements: Dictionary or list of dictionaries containing element data
|
|
1205
|
+
search_string: The search string used to find the elements
|
|
1206
|
+
entity_type: The type of entity (e.g., "Glossary", "Term", "Category")
|
|
1207
|
+
output_format: The desired output format (MD, FORM, REPORT, LIST, DICT, MERMAID, HTML)
|
|
1208
|
+
extract_properties_func: Function to extract properties from an element
|
|
1209
|
+
get_additional_props_func: Optional function to get additional properties
|
|
1210
|
+
columns: Optional list of column definitions for table output
|
|
1211
|
+
|
|
1212
|
+
Returns:
|
|
1213
|
+
Formatted output as string or list of dictionaries
|
|
1214
|
+
"""
|
|
1215
|
+
columns = columns_struct['formats'].get('attributes',None) if columns_struct else None
|
|
1216
|
+
if not columns:
|
|
1217
|
+
columns_struct = select_report_format("Default",output_format)
|
|
1218
|
+
if columns_struct:
|
|
1219
|
+
columns = columns_struct.get('formats', {}).get('attributes', None)
|
|
1220
|
+
|
|
1221
|
+
target_type = columns_struct.get('target_type', entity_type) if columns_struct else entity_type
|
|
1222
|
+
if target_type is None:
|
|
1223
|
+
target_type = entity_type
|
|
1224
|
+
|
|
1225
|
+
# Ensure elements is a list
|
|
1226
|
+
if isinstance(elements, dict):
|
|
1227
|
+
elements = [elements]
|
|
1228
|
+
|
|
1229
|
+
# Handle empty search string
|
|
1230
|
+
if search_string is None or search_string == '':
|
|
1231
|
+
search_string = "All"
|
|
1232
|
+
|
|
1233
|
+
# Set the output format to DICT to return values to table display
|
|
1234
|
+
# if output_format == "TABLE":
|
|
1235
|
+
# output_format = "DICT"
|
|
1236
|
+
|
|
1237
|
+
# Generate output based on format
|
|
1238
|
+
if output_format == 'MERMAID':
|
|
1239
|
+
return extract_mermaid_only(elements)
|
|
1240
|
+
|
|
1241
|
+
elif output_format == 'HTML':
|
|
1242
|
+
# First generate the REPORT format output
|
|
1243
|
+
report_output = generate_output(
|
|
1244
|
+
elements=elements,
|
|
1245
|
+
search_string=search_string,
|
|
1246
|
+
entity_type=entity_type,
|
|
1247
|
+
output_format="REPORT",
|
|
1248
|
+
extract_properties_func=extract_properties_func,
|
|
1249
|
+
get_additional_props_func=get_additional_props_func,
|
|
1250
|
+
columns_struct=columns_struct
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
# Convert the markdown to HTML
|
|
1254
|
+
return markdown_to_html(report_output)
|
|
1255
|
+
|
|
1256
|
+
elif output_format in ['DICT','TABLE']:
|
|
1257
|
+
return generate_entity_dict(
|
|
1258
|
+
elements=elements,
|
|
1259
|
+
extract_properties_func=extract_properties_func,
|
|
1260
|
+
get_additional_props_func=get_additional_props_func,
|
|
1261
|
+
exclude_keys=['properties'],
|
|
1262
|
+
columns_struct=columns_struct,
|
|
1263
|
+
output_format=output_format
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
elif output_format == 'LIST':
|
|
1267
|
+
if columns is None:
|
|
1268
|
+
raise ValueError("Columns must be provided for LIST output format")
|
|
1269
|
+
|
|
1270
|
+
return generate_entity_md_table(
|
|
1271
|
+
elements=elements,
|
|
1272
|
+
search_string=search_string,
|
|
1273
|
+
entity_type=entity_type,
|
|
1274
|
+
extract_properties_func=extract_properties_func,
|
|
1275
|
+
columns_struct=columns_struct,
|
|
1276
|
+
get_additional_props_func=get_additional_props_func,
|
|
1277
|
+
output_format=output_format
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
else: # MD, FORM, REPORT
|
|
1281
|
+
elements_md, elements_action = make_preamble(
|
|
1282
|
+
obj_type=target_type,
|
|
1283
|
+
search_string=search_string,
|
|
1284
|
+
output_format=output_format
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1287
|
+
elements_md += generate_entity_md(
|
|
1288
|
+
elements=elements,
|
|
1289
|
+
elements_action=elements_action,
|
|
1290
|
+
output_format=output_format,
|
|
1291
|
+
entity_type=target_type,
|
|
1292
|
+
extract_properties_func=extract_properties_func,
|
|
1293
|
+
get_additional_props_func=get_additional_props_func,
|
|
1294
|
+
columns_struct = columns_struct
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
return elements_md
|