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.

Files changed (272) hide show
  1. commands/__init__.py +24 -0
  2. commands/cat/Dr-Egeria_md-orig.py +2 -2
  3. commands/cat/__init__.py +1 -17
  4. commands/cat/collection_actions.py +197 -0
  5. commands/cat/dr_egeria_command_help.py +372 -0
  6. commands/cat/dr_egeria_jupyter.py +7 -7
  7. commands/cat/dr_egeria_md.py +27 -182
  8. commands/cat/exp_list_glossaries.py +11 -14
  9. commands/cat/get_asset_graph.py +37 -267
  10. commands/cat/{get_collection.py → get_collection_tree.py} +10 -18
  11. commands/cat/get_project_dependencies.py +14 -14
  12. commands/cat/get_project_structure.py +15 -14
  13. commands/cat/get_tech_type_elements.py +16 -116
  14. commands/cat/glossary_actions.py +145 -298
  15. commands/cat/list_assets.py +3 -11
  16. commands/cat/list_cert_types.py +17 -63
  17. commands/cat/list_collections.py +46 -138
  18. commands/cat/list_deployed_catalogs.py +15 -27
  19. commands/cat/list_deployed_database_schemas.py +27 -43
  20. commands/cat/list_deployed_databases.py +16 -31
  21. commands/cat/list_deployed_servers.py +35 -54
  22. commands/cat/list_glossaries.py +18 -17
  23. commands/cat/list_projects.py +10 -12
  24. commands/cat/list_tech_type_elements.py +21 -37
  25. commands/cat/list_tech_types.py +13 -25
  26. commands/cat/list_terms.py +38 -79
  27. commands/cat/list_todos.py +4 -11
  28. commands/cat/list_user_ids.py +3 -10
  29. commands/cat/my_reports.py +559 -0
  30. commands/cat/run_report.py +394 -0
  31. commands/cat/run_report_orig.py +528 -0
  32. commands/cli/egeria.py +222 -247
  33. commands/cli/egeria_cat.py +68 -81
  34. commands/cli/egeria_my.py +13 -0
  35. commands/cli/egeria_ops.py +69 -74
  36. commands/cli/egeria_tech.py +17 -93
  37. commands/cli/ops_config.py +3 -6
  38. commands/{cat/list_categories.py → deprecated/list_data_designer.py} +53 -64
  39. commands/{cat/list_data_structures.py → deprecated/list_data_structures_full.py} +3 -6
  40. commands/deprecated/old_get_asset_graph.py +315 -0
  41. commands/my/__init__.py +0 -2
  42. commands/my/list_my_profile.py +27 -34
  43. commands/my/list_my_roles.py +1 -7
  44. commands/my/monitor_my_todos.py +1 -7
  45. commands/my/monitor_open_todos.py +6 -7
  46. commands/my/todo_actions.py +4 -5
  47. commands/ops/__init__.py +0 -2
  48. commands/ops/gov_server_actions.py +17 -21
  49. commands/ops/list_archives.py +17 -38
  50. commands/ops/list_catalog_targets.py +33 -40
  51. commands/ops/load_archive.py +35 -26
  52. commands/ops/{monitor_engine_activity_c.py → monitor_active_engine_activity.py} +51 -82
  53. commands/ops/{monitor_integ_daemon_status.py → monitor_daemon_status.py} +35 -55
  54. commands/ops/monitor_engine_activity.py +79 -77
  55. commands/ops/{monitor_gov_eng_status.py → monitor_engine_status.py} +10 -7
  56. commands/ops/monitor_platform_status.py +38 -50
  57. commands/ops/monitor_server_startup.py +6 -11
  58. commands/ops/monitor_server_status.py +7 -11
  59. commands/ops/orig_monitor_server_list.py +8 -8
  60. commands/ops/orig_monitor_server_status.py +1 -5
  61. commands/ops/refresh_integration_daemon.py +5 -5
  62. commands/ops/restart_integration_daemon.py +5 -5
  63. commands/ops/table_integ_daemon_status.py +6 -6
  64. commands/ops/x_engine_actions.py +7 -7
  65. commands/tech/__init__.py +0 -2
  66. commands/tech/{generic_actions.py → element_actions.py} +6 -11
  67. commands/tech/get_element_info.py +20 -29
  68. commands/tech/get_guid_info.py +23 -42
  69. commands/tech/get_tech_details.py +20 -35
  70. commands/tech/get_tech_type_template.py +28 -39
  71. commands/tech/list_all_om_type_elements.py +24 -30
  72. commands/tech/list_all_om_type_elements_x.py +22 -28
  73. commands/tech/list_all_related_elements.py +19 -28
  74. commands/tech/list_anchored_elements.py +22 -30
  75. commands/tech/list_asset_types.py +19 -24
  76. commands/tech/list_elements_by_classification_by_property_value.py +26 -32
  77. commands/tech/list_elements_by_property_value.py +19 -25
  78. commands/tech/list_elements_by_property_value_x.py +20 -28
  79. commands/tech/list_elements_for_classification.py +28 -41
  80. commands/tech/list_gov_action_processes.py +16 -27
  81. commands/tech/list_information_supply_chains.py +22 -30
  82. commands/tech/list_registered_services.py +14 -26
  83. commands/tech/list_related_elements_with_prop_value.py +15 -25
  84. commands/tech/list_related_specification.py +1 -4
  85. commands/tech/list_relationship_types.py +15 -25
  86. commands/tech/list_relationships.py +20 -36
  87. commands/tech/list_solution_blueprints.py +28 -33
  88. commands/tech/list_solution_components.py +23 -29
  89. commands/tech/list_solution_roles.py +21 -32
  90. commands/tech/list_tech_templates.py +51 -54
  91. commands/tech/list_valid_metadata_values.py +5 -9
  92. commands/tech/table_tech_templates.py +2 -6
  93. commands/tech/x_list_related_elements.py +1 -4
  94. examples/GeoSpatial Products Example.py +524 -0
  95. examples/Jupyter Notebooks/P-egeria-server-config.ipynb +2137 -0
  96. examples/Jupyter Notebooks/README.md +2 -0
  97. examples/Jupyter Notebooks/common/P-environment-check.ipynb +115 -0
  98. examples/Jupyter Notebooks/common/__init__.py +14 -0
  99. examples/Jupyter Notebooks/common/common-functions.ipynb +4694 -0
  100. examples/Jupyter Notebooks/common/environment-check.ipynb +52 -0
  101. examples/Jupyter Notebooks/common/globals.ipynb +184 -0
  102. examples/Jupyter Notebooks/common/globals.py +154 -0
  103. examples/Jupyter Notebooks/common/orig_globals.py +152 -0
  104. examples/format_sets/all_format_sets.json +910 -0
  105. examples/format_sets/custom_format_sets.json +268 -0
  106. examples/format_sets/subset_format_sets.json +187 -0
  107. examples/format_sets_save_load_example.py +291 -0
  108. examples/jacquard_data_sets.py +129 -0
  109. examples/output_formats_example.py +193 -0
  110. examples/test_jacquard_data_sets.py +54 -0
  111. examples/test_jacquard_data_sets_scenarios.py +94 -0
  112. md_processing/__init__.py +90 -0
  113. md_processing/command_dispatcher.py +33 -0
  114. md_processing/command_mapping.py +221 -0
  115. md_processing/data/commands/commands_data_designer.json +537 -0
  116. md_processing/data/commands/commands_external_reference.json +733 -0
  117. md_processing/data/commands/commands_feedback.json +155 -0
  118. md_processing/data/commands/commands_general.json +204 -0
  119. md_processing/data/commands/commands_glossary.json +218 -0
  120. md_processing/data/commands/commands_governance.json +3678 -0
  121. md_processing/data/commands/commands_product_manager.json +865 -0
  122. md_processing/data/commands/commands_project.json +642 -0
  123. md_processing/data/commands/commands_solution_architect.json +366 -0
  124. md_processing/data/commands.json +17568 -0
  125. md_processing/data/commands_working.json +30641 -0
  126. md_processing/data/gened_report_specs.py +6584 -0
  127. md_processing/data/generated_format_sets.json +6533 -0
  128. md_processing/data/generated_format_sets_old.json +4137 -0
  129. md_processing/data/generated_format_sets_old.py +45 -0
  130. md_processing/dr_egeria.py +182 -0
  131. md_processing/md_commands/__init__.py +3 -0
  132. md_processing/md_commands/data_designer_commands.py +1276 -0
  133. md_processing/md_commands/ext_ref_commands.py +530 -0
  134. md_processing/md_commands/feedback_commands.py +726 -0
  135. md_processing/md_commands/glossary_commands.py +684 -0
  136. md_processing/md_commands/governance_officer_commands.py +600 -0
  137. md_processing/md_commands/product_manager_commands.py +1266 -0
  138. md_processing/md_commands/project_commands.py +383 -0
  139. md_processing/md_commands/solution_architect_commands.py +1184 -0
  140. md_processing/md_commands/view_commands.py +295 -0
  141. md_processing/md_processing_utils/__init__.py +4 -0
  142. md_processing/md_processing_utils/common_md_proc_utils.py +1249 -0
  143. md_processing/md_processing_utils/common_md_utils.py +578 -0
  144. md_processing/md_processing_utils/determine_width.py +103 -0
  145. md_processing/md_processing_utils/extraction_utils.py +547 -0
  146. md_processing/md_processing_utils/gen_report_specs.py +643 -0
  147. md_processing/md_processing_utils/generate_dr_help.py +193 -0
  148. md_processing/md_processing_utils/generate_md_cmd_templates.py +144 -0
  149. md_processing/md_processing_utils/generate_md_templates.py +83 -0
  150. md_processing/md_processing_utils/md_processing_constants.py +1228 -0
  151. md_processing/md_processing_utils/message_constants.py +19 -0
  152. pyegeria/__init__.py +201 -443
  153. pyegeria/core/__init__.py +40 -0
  154. pyegeria/core/_base_platform_client.py +574 -0
  155. pyegeria/core/_base_server_client.py +573 -0
  156. pyegeria/core/_exceptions.py +457 -0
  157. pyegeria/core/_globals.py +60 -0
  158. pyegeria/core/_server_client.py +6073 -0
  159. pyegeria/core/_validators.py +257 -0
  160. pyegeria/core/config.py +654 -0
  161. pyegeria/{create_tech_guid_lists.py → core/create_tech_guid_lists.py} +0 -1
  162. pyegeria/core/load_config.py +37 -0
  163. pyegeria/core/logging_configuration.py +207 -0
  164. pyegeria/core/mcp_adapter.py +144 -0
  165. pyegeria/core/mcp_server.py +212 -0
  166. pyegeria/core/utils.py +405 -0
  167. pyegeria/deprecated/__init__.py +0 -0
  168. pyegeria/{_client.py → deprecated/_client.py} +62 -24
  169. pyegeria/{_deprecated_gov_engine.py → deprecated/_deprecated_gov_engine.py} +16 -16
  170. pyegeria/{classification_manager_omvs.py → deprecated/classification_manager_omvs.py} +1988 -1878
  171. pyegeria/deprecated/output_formatter_with_machine_keys.py +1127 -0
  172. pyegeria/{runtime_manager_omvs.py → deprecated/runtime_manager_omvs.py} +216 -229
  173. pyegeria/{valid_metadata_omvs.py → deprecated/valid_metadata_omvs.py} +93 -93
  174. pyegeria/{x_action_author_omvs.py → deprecated/x_action_author_omvs.py} +2 -3
  175. pyegeria/egeria_cat_client.py +25 -51
  176. pyegeria/egeria_client.py +140 -98
  177. pyegeria/egeria_config_client.py +48 -24
  178. pyegeria/egeria_tech_client.py +170 -83
  179. pyegeria/models/__init__.py +150 -0
  180. pyegeria/models/collection_models.py +168 -0
  181. pyegeria/models/models.py +654 -0
  182. pyegeria/omvs/__init__.py +84 -0
  183. pyegeria/omvs/action_author.py +342 -0
  184. pyegeria/omvs/actor_manager.py +5980 -0
  185. pyegeria/omvs/asset_catalog.py +842 -0
  186. pyegeria/omvs/asset_maker.py +2736 -0
  187. pyegeria/omvs/automated_curation.py +4403 -0
  188. pyegeria/omvs/classification_manager.py +11213 -0
  189. pyegeria/omvs/collection_manager.py +5780 -0
  190. pyegeria/omvs/community_matters_omvs.py +468 -0
  191. pyegeria/{core_omag_server_config.py → omvs/core_omag_server_config.py} +157 -157
  192. pyegeria/{data_designer_omvs.py → omvs/data_designer.py} +1991 -1691
  193. pyegeria/omvs/data_discovery.py +869 -0
  194. pyegeria/omvs/data_engineer.py +372 -0
  195. pyegeria/omvs/digital_business.py +1133 -0
  196. pyegeria/omvs/external_links.py +1752 -0
  197. pyegeria/omvs/feedback_manager.py +834 -0
  198. pyegeria/{full_omag_server_config.py → omvs/full_omag_server_config.py} +73 -69
  199. pyegeria/omvs/glossary_manager.py +3231 -0
  200. pyegeria/omvs/governance_officer.py +3009 -0
  201. pyegeria/omvs/lineage_linker.py +314 -0
  202. pyegeria/omvs/location_arena.py +1525 -0
  203. pyegeria/omvs/metadata_expert.py +668 -0
  204. pyegeria/omvs/metadata_explorer_omvs.py +2943 -0
  205. pyegeria/omvs/my_profile.py +1042 -0
  206. pyegeria/omvs/notification_manager.py +358 -0
  207. pyegeria/omvs/people_organizer.py +394 -0
  208. pyegeria/{platform_services.py → omvs/platform_services.py} +113 -193
  209. pyegeria/omvs/product_manager.py +1825 -0
  210. pyegeria/omvs/project_manager.py +1907 -0
  211. pyegeria/omvs/reference_data.py +1140 -0
  212. pyegeria/omvs/registered_info.py +334 -0
  213. pyegeria/omvs/runtime_manager.py +2817 -0
  214. pyegeria/omvs/schema_maker.py +446 -0
  215. pyegeria/{server_operations.py → omvs/server_operations.py} +27 -26
  216. pyegeria/omvs/solution_architect.py +6490 -0
  217. pyegeria/omvs/specification_properties.py +37 -0
  218. pyegeria/omvs/subject_area.py +1042 -0
  219. pyegeria/omvs/template_manager_omvs.py +236 -0
  220. pyegeria/omvs/time_keeper.py +1761 -0
  221. pyegeria/omvs/valid_metadata.py +3221 -0
  222. pyegeria/omvs/valid_metadata_lists.py +37 -0
  223. pyegeria/omvs/valid_type_lists.py +37 -0
  224. pyegeria/view/__init__.py +28 -0
  225. pyegeria/view/_output_format_models.py +514 -0
  226. pyegeria/view/_output_formats.py +14 -0
  227. pyegeria/view/base_report_formats.py +2719 -0
  228. pyegeria/view/dr_egeria_reports.py +56 -0
  229. pyegeria/view/format_set_executor.py +397 -0
  230. pyegeria/{md_processing_utils.py → view/md_processing_utils.py} +5 -5
  231. pyegeria/{mermaid_utilities.py → view/mermaid_utilities.py} +2 -154
  232. pyegeria/view/output_formatter.py +1297 -0
  233. pyegeria-5.5.3.3.dist-info/METADATA +218 -0
  234. pyegeria-5.5.3.3.dist-info/RECORD +241 -0
  235. {pyegeria-5.3.9.9.3.dist-info → pyegeria-5.5.3.3.dist-info}/WHEEL +2 -1
  236. pyegeria-5.5.3.3.dist-info/entry_points.txt +103 -0
  237. pyegeria-5.5.3.3.dist-info/top_level.txt +4 -0
  238. commands/cat/.DS_Store +0 -0
  239. commands/cat/README.md +0 -16
  240. commands/cli/txt_custom_v2.tcss +0 -19
  241. commands/my/README.md +0 -17
  242. commands/ops/README.md +0 -24
  243. commands/ops/monitor_asset_events.py +0 -108
  244. commands/tech/README.md +0 -24
  245. pyegeria/.DS_Store +0 -0
  246. pyegeria/README.md +0 -35
  247. pyegeria/_globals.py +0 -47
  248. pyegeria/_validators.py +0 -385
  249. pyegeria/asset_catalog_omvs.py +0 -864
  250. pyegeria/automated_curation_omvs.py +0 -3765
  251. pyegeria/collection_manager_omvs.py +0 -2744
  252. pyegeria/dr.egeria spec.md +0 -9
  253. pyegeria/egeria_my_client.py +0 -56
  254. pyegeria/feedback_manager_omvs.py +0 -4573
  255. pyegeria/glossary_browser_omvs.py +0 -3728
  256. pyegeria/glossary_manager_omvs.py +0 -2440
  257. pyegeria/m_test.py +0 -118
  258. pyegeria/md_processing_helpers.py +0 -58
  259. pyegeria/md_processing_utils_orig.py +0 -1103
  260. pyegeria/metadata_explorer_omvs.py +0 -2326
  261. pyegeria/my_profile_omvs.py +0 -1022
  262. pyegeria/output_formatter.py +0 -389
  263. pyegeria/project_manager_omvs.py +0 -1933
  264. pyegeria/registered_info.py +0 -167
  265. pyegeria/solution_architect_omvs.py +0 -2156
  266. pyegeria/template_manager_omvs.py +0 -1414
  267. pyegeria/utils.py +0 -197
  268. pyegeria-5.3.9.9.3.dist-info/METADATA +0 -72
  269. pyegeria-5.3.9.9.3.dist-info/RECORD +0 -143
  270. pyegeria-5.3.9.9.3.dist-info/entry_points.txt +0 -99
  271. /pyegeria/{_exceptions.py → deprecated/_exceptions.py} +0 -0
  272. {pyegeria-5.3.9.9.3.dist-info → pyegeria-5.5.3.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,1249 @@
1
+ """
2
+ This file contains general utility functions for processing Egeria Markdown
3
+ """
4
+ import json
5
+ import os
6
+ import sys
7
+ import re
8
+ from typing import Optional
9
+
10
+ from loguru import logger
11
+ from pydantic import ValidationError
12
+ from rich import print
13
+ from rich.markdown import Markdown
14
+ from rich.console import Console
15
+ from pyegeria.core.utils import parse_to_dict
16
+ from pyegeria.core.config import settings
17
+ from md_processing.md_processing_utils.common_md_utils import (get_current_datetime_string, split_tb_string, str_to_bool, )
18
+ from md_processing.md_processing_utils.extraction_utils import (process_simple_attribute, extract_attribute,
19
+ get_element_by_name)
20
+ from md_processing.md_processing_utils.common_md_utils import (update_element_dictionary, set_find_body)
21
+ from md_processing.md_processing_utils.extraction_utils import (extract_command_plus)
22
+ from md_processing.md_processing_utils.md_processing_constants import (get_command_spec, add_default_upsert_attributes,
23
+ add_default_link_attributes)
24
+ from md_processing.md_processing_utils.message_constants import (ERROR, INFO, WARNING, EXISTS_REQUIRED)
25
+ from pyegeria import EgeriaTech, PyegeriaException
26
+ from pyegeria.view.base_report_formats import select_report_spec
27
+ from pyegeria.core._exceptions import print_basic_exception, print_validation_error, print_basic_exception
28
+ from pyegeria.core._globals import DEBUG_LEVEL
29
+
30
+ debug_level = DEBUG_LEVEL
31
+ global COMMAND_DEFINITIONS
32
+
33
+ user_config = settings.User_Profile
34
+
35
+ # Constants
36
+ EGERIA_WIDTH = int(os.environ.get("EGERIA_WIDTH", "200"))
37
+ EGERIA_USAGE_LEVEL = os.environ.get("EGERIA_USAGE_LEVEL", user_config.egeria_usage_level)
38
+ LOCAL_QUALIFIER = os.environ.get("EGERIA_LOCAL_QUALIFIER", None)
39
+ console = Console(width=EGERIA_WIDTH)
40
+
41
+
42
+
43
+
44
+
45
+ @logger.catch
46
+ def process_provenance_command(file_path: str, txt: [str]) -> str:
47
+ """
48
+ Processes a provenance object_action by extracting the file path and current datetime.
49
+
50
+ Args:
51
+ file_path: The path to the file being processed.
52
+ txt: The text containing the provenance object_action.
53
+
54
+ Returns:
55
+ A string containing the provenance information.
56
+ """
57
+ now = get_current_datetime_string()
58
+ file_name = os.path.basename(file_path)
59
+ provenance = f"\n\n\n# Provenance:\n \n* Derived from processing file {file_name} on {now}\n"
60
+ return provenance
61
+
62
+
63
+ @logger.catch
64
+ def parse_upsert_command(egeria_client: EgeriaTech, object_type: str, object_action: str, txt: str,
65
+ directive: str = "display", body_type: str = None) -> dict:
66
+ parsed_attributes, parsed_output = {}, {}
67
+
68
+ parsed_output['valid'] = True
69
+ parsed_output['exists'] = False
70
+ parsed_output['display'] = ""
71
+ display_name = ""
72
+ labels = {}
73
+
74
+ command_spec = get_command_spec(f"Create {object_type}", body_type = body_type)
75
+ if command_spec is None:
76
+ logger.error("Command not found in command spec")
77
+ raise Exception("Command not found in command spec")
78
+ distinguished_attributes = command_spec.get('Attributes', [])
79
+ attributes = add_default_upsert_attributes(distinguished_attributes)
80
+ command_display_name = command_spec.get('display_name', None)
81
+ command_qn_prefix = command_spec.get('qn_prefix', None)
82
+
83
+ parsed_output['display_name'] = command_display_name
84
+ parsed_output['qn_prefix'] = command_qn_prefix
85
+
86
+ parsed_output['is_own_anchor'] = command_spec.get('isOwnAnchor', True)
87
+
88
+ parsed_output['reason'] = ""
89
+
90
+ msg = f"\tProcessing {object_action} on a {object_type} \n"
91
+ logger.info(msg)
92
+
93
+ # get the version early because we may need it to construct qualified names.
94
+ version = process_simple_attribute(txt, {'Version', "Version Identifier", "Published Version"}, INFO)
95
+ parsed_output['version'] = version
96
+
97
+ for attr in attributes:
98
+ for key in attr:
99
+ try:
100
+ # Run some checks to see if the attribute is appropriate to the operation and usage level
101
+ for_update = attr[key].get('inUpdate', True)
102
+ level = attr[key].get('level', 'Basic')
103
+ msg = (f"___\nProcessing `{key}` in `{object_action}` on a `{object_type}` "
104
+ f"\n\twith usage level: `{EGERIA_USAGE_LEVEL}` and attribute level `{level}` and for_update `"
105
+ f"{for_update}`\n")
106
+ logger.trace(msg)
107
+ if for_update is False and object_action == "Update":
108
+ logger.trace(f"Attribute `{key}`is not allowed for `Update`", highlight=True)
109
+ continue
110
+ if EGERIA_USAGE_LEVEL == "Basic" and level != "Basic":
111
+ logger.trace(f"Attribute `{key}` is not supported for `{EGERIA_USAGE_LEVEL}` usage level. Skipping.",
112
+ highlight=True)
113
+ continue
114
+ if EGERIA_USAGE_LEVEL == "Advanced" and level in ["Expert", "Invisible"]:
115
+ logger.trace(f"Attribute `{key}` is not supported for `{EGERIA_USAGE_LEVEL}` usage level. Skipping.",
116
+ highlight=True)
117
+ continue
118
+ if EGERIA_USAGE_LEVEL == "Expert" and level == "Invisible":
119
+ logger.trace(f"Attribute `{key}` is not supported for `{EGERIA_USAGE_LEVEL}` usage level. Skipping.",
120
+ highlight=True)
121
+ continue
122
+
123
+ if attr[key].get('input_required', False) is True:
124
+ if_missing = ERROR
125
+ else:
126
+ if_missing = INFO
127
+
128
+ # lab = [item.strip() for item in re.split(r'[;,\n]+',attr[key]['attr_labels'])]
129
+ lab = split_tb_string(attr[key]['attr_labels'])
130
+ labels: set = set()
131
+ labels.add(key.strip())
132
+ if key == 'Display Name':
133
+ labels.add(object_type.strip())
134
+ if lab is not None and lab != [""]:
135
+ labels.update(lab)
136
+
137
+ default_value = attr[key].get('default_value', None)
138
+
139
+ style = attr[key]['style']
140
+ if style in ['Simple', 'Comment']:
141
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value)
142
+ elif style in ['Dictionary', "Named DICT"]:
143
+ parsed_attributes[key] = proc_dictionary_attribute(txt, object_action, labels, if_missing, default_value)
144
+ if key in parsed_attributes and parsed_attributes[key] is not None:
145
+ if parsed_attributes[key].get('value', None) is not None:
146
+ if isinstance(parsed_attributes[key]['value'], dict):
147
+ parsed_attributes[key]['dict'] = json.dumps(parsed_attributes[key]['value'], indent=2)
148
+ else:
149
+ continue
150
+ else:
151
+ continue
152
+
153
+ elif style == 'Valid Value':
154
+ parsed_attributes[key] = proc_valid_value(txt, object_action, labels,
155
+ attr[key].get('valid_values', None), if_missing,
156
+ default_value)
157
+ elif style == 'QN':
158
+ parsed_attributes[key] = proc_el_id(egeria_client, command_display_name, command_qn_prefix, labels, txt,
159
+ object_action, version, if_missing)
160
+ if key == 'Qualified Name' and parsed_attributes[key]['value'] and parsed_attributes[key][
161
+ 'exists'] is False:
162
+ parsed_output['exists'] = False
163
+ elif style == 'ID':
164
+ parsed_attributes[key] = proc_el_id(egeria_client, command_display_name, command_qn_prefix, labels, txt,
165
+ object_action, version, if_missing)
166
+
167
+ parsed_output['guid'] = parsed_attributes[key].get('guid', None)
168
+ parsed_output['qualified_name'] = parsed_attributes[key].get('qualified_name', None)
169
+ parsed_output['exists'] = parsed_attributes[key]['exists']
170
+ if parsed_attributes[key]['valid'] is False:
171
+ parsed_output['valid'] = False
172
+ parsed_output['reason'] += parsed_attributes[key]['reason']
173
+
174
+ elif style == 'Reference Name':
175
+ parsed_attributes[key] = proc_ids(egeria_client, key, labels, txt, object_action, if_missing)
176
+ if ((if_missing == ERROR) and parsed_attributes[key].get("value", None) and parsed_attributes[key][
177
+ 'exists'] is False):
178
+ msg = f"Reference Name `{parsed_attributes[key]['value']}` is specified but does not exist"
179
+ logger.error(msg)
180
+ parsed_output['valid'] = False
181
+ parsed_output['reason'] += msg
182
+ elif parsed_attributes[key]['valid'] is False:
183
+ parsed_output['valid'] = False
184
+ parsed_output['reason'] += parsed_attributes[key]['reason']
185
+
186
+ elif style == 'GUID':
187
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing)
188
+ g = parsed_attributes[key].get('value', None)
189
+ if g and ("___" not in g and "---" not in g):
190
+ parsed_output['guid'] = parsed_attributes[key]['value']
191
+ elif style == 'Ordered Int':
192
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing)
193
+ elif style == 'Simple Int':
194
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value, "int")
195
+ elif style == 'Simple List':
196
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value, "list")
197
+ name_list = parsed_attributes[key]['value']
198
+ # attribute = re.split(r'[;,\n]+', name_list) if name_list is not None else None
199
+ attribute = split_tb_string(name_list)
200
+ parsed_attributes[key]['value'] = attribute
201
+ parsed_attributes[key]['name_list'] = name_list
202
+ elif style == 'Parent':
203
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value)
204
+ elif style == 'Bool':
205
+ parsed_attributes[key] = proc_bool_attribute(txt, object_action, labels, if_missing, default_value)
206
+ elif style == "Dictionary List":
207
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value)
208
+ parsed_attributes[key]['list'] = json.loads(parsed_attributes[key]['value'])
209
+
210
+
211
+ elif style == 'Reference Name List':
212
+ parsed_attributes[key] = proc_name_list(egeria_client, key, txt, labels, if_missing)
213
+ if (parsed_attributes[key].get("value", None) and (
214
+ parsed_attributes[key]['exists'] is False or parsed_attributes[key]['valid'] is False)):
215
+ msg = (f"A Reference Name in `{parsed_attributes[key].get('name_list', None)}` is specified but "
216
+ f"does not exist")
217
+ logger.error(msg)
218
+ parsed_output['valid'] = False
219
+ parsed_output['reason'] += msg
220
+ else:
221
+ msg = f"Unknown attribute style: {style} for key `{key}`"
222
+ logger.error(msg)
223
+ sys.exit(1)
224
+ parsed_attributes[key]['valid'] = False
225
+ parsed_attributes[key]['value'] = None
226
+ if key == "Display Name":
227
+ display_name = parsed_attributes[key]['value']
228
+
229
+ value = parsed_attributes[key].get('value', None)
230
+
231
+ if value is not None:
232
+ if isinstance(value, list):
233
+ list_out = f"\n\t* {key}:\n\n"
234
+ for item in value:
235
+ list_out += f"\t{item}\n"
236
+ parsed_output['display'] += list_out
237
+ elif isinstance(value, dict):
238
+ list_out = f"\n\t* {key}:\n\n"
239
+ for k, v in value.items():
240
+ list_out += f"\t{k}: \n\t\t{v}\n"
241
+ parsed_output['display'] += list_out
242
+ else:
243
+ parsed_output['display'] += f"\n\t* {key}: \n`{value}`\n\t"
244
+ except PyegeriaException as e:
245
+ logger.error(f"PyegeriaException occurred: {e}")
246
+
247
+ print_basic_exception(e)
248
+
249
+ except ValidationError as e:
250
+ parsed_attributes[key]['valid'] = False
251
+ parsed_attributes[key]['value'] = None
252
+ print_validation_error(e)
253
+
254
+
255
+ parsed_output['attributes'] = parsed_attributes
256
+
257
+ if display_name is None:
258
+ msg = f"No display name or name identifier found"
259
+ logger.error(msg)
260
+ parsed_output['valid'] = False
261
+ parsed_output['reason'] = msg
262
+ return parsed_output
263
+
264
+
265
+ if parsed_attributes.get('Parent ID', {}).get('value', None) is not None:
266
+ if (parsed_attributes.get('Parent Relationship Type Name',{}).get('value', None) is None) or (
267
+ parsed_attributes.get('Parent at End1',{}).get('value',None) is None):
268
+ msg = "Parent ID was found but either Parent `Relationship Type Name` or `Parent at End1` are missing"
269
+ logger.error(msg)
270
+ parsed_output['valid'] = False
271
+ parsed_output['reason'] = msg
272
+ if parsed_attributes['Parent Relationship Type Name'].get('exists', False) is False:
273
+ msg = "Parent ID was found but does not exist"
274
+ logger.error(msg)
275
+ parsed_output['valid'] = False
276
+ parsed_output['reason'] = msg
277
+
278
+ if directive in ["validate", "process"] and object_action == "Update" and not parsed_output[
279
+ 'exists']: # check to see if provided information exists and is consistent with existing info
280
+ msg = f"Update request invalid, element `{display_name}` does not exist\n"
281
+ logger.error(msg)
282
+ parsed_output['valid'] = False
283
+ if directive in ["validate", "process"] and not parsed_output['valid'] and object_action == "Update":
284
+ msg = f"Request is invalid, `{object_action} {object_type}` is not valid - see previous messages\n"
285
+ logger.error(msg)
286
+
287
+ elif directive in ["validate",
288
+ "process"] and object_action == 'Create': # if the object_action is create, check that it
289
+ # doesn't already exist
290
+ if parsed_output['exists']:
291
+ msg = f"Element `{display_name}` cannot be created since it already exists\n"
292
+ logger.error(msg)
293
+ parsed_output['valid'] = False
294
+ else:
295
+ msg = f"Element `{display_name}` does not exist so it can be created\n"
296
+ logger.info(msg)
297
+
298
+
299
+ if parsed_output.get('qualified_name',None) and "* Qualified Name" not in parsed_output['display']:
300
+ parsed_output['display'] += f"\n\t* Qualified Name: `{parsed_output['qualified_name']}`\n\t"
301
+ if parsed_output.get('guid',None):
302
+ parsed_output['display'] += f"\n\t* GUID: `{parsed_output['guid']}`\n\t"
303
+
304
+ return parsed_output
305
+
306
+
307
+ @logger.catch
308
+ def parse_view_command(egeria_client: EgeriaTech, object_type: str, object_action: str, txt: str,
309
+ directive: str = "display") -> dict:
310
+ parsed_attributes, parsed_output = {}, {}
311
+
312
+ parsed_output['valid'] = True
313
+ parsed_output['exists'] = False
314
+
315
+ labels = {}
316
+ if object_action in ["Link", "Attach", "Unlink", "Detach"]:
317
+ command_spec = get_command_spec(f"Link {object_type}")
318
+ if command_spec is None:
319
+ logger.error(f"Command specification not found for 'Link {object_type}'")
320
+ return None
321
+ distinguished_attributes = command_spec.get('distinguished_attributes', None)
322
+ if distinguished_attributes:
323
+ attributes = add_default_link_attributes(distinguished_attributes)
324
+ else:
325
+ command_spec = get_command_spec(f"{object_action} {object_type}")
326
+ if command_spec is None:
327
+ logger.error(f"Command specification not found for '{object_action} {object_type}'")
328
+ return None
329
+ attributes = command_spec.get('Attributes', None)
330
+
331
+ command_display_name = command_spec.get('display_name', None)
332
+
333
+ parsed_output['reason'] = ""
334
+ parsed_output['display'] = ""
335
+
336
+ msg = f"\tProcessing {object_action} on {object_type} \n"
337
+ logger.info(msg)
338
+
339
+ # Helper: convert label to snake_case
340
+ def _to_snake_case(name: str) -> str:
341
+ name = name.strip()
342
+ # Replace non-alphanumeric with space, then collapse spaces to underscores
343
+ name = re.sub(r"[^0-9A-Za-z]+", "_", name)
344
+ # Lowercase and trim possible leading/trailing underscores
345
+ return name.strip("_").lower()
346
+
347
+ # Build known labels set from command spec
348
+ known_labels: set[str] = set()
349
+
350
+ # get the version early because we may need it to construct qualified names.
351
+
352
+ for attr in attributes:
353
+ for key in attr:
354
+ # Run some checks to see if the attribute is appropriate to the operation and usage level
355
+
356
+ level = attr[key].get('level', 'Basic')
357
+ msg = (f"___\nProcessing `{key}` in `{object_action}` on a `{object_type}` "
358
+ f"\n\twith usage level: `{EGERIA_USAGE_LEVEL}` ")
359
+ logger.trace(msg)
360
+
361
+ if EGERIA_USAGE_LEVEL == "Basic" and level != "Basic":
362
+ logger.trace(f"Attribute `{key}` is not supported for `{EGERIA_USAGE_LEVEL}` usage level. Skipping.",
363
+ highlight=True)
364
+ continue
365
+ if EGERIA_USAGE_LEVEL == "Advanced" and level in ["Expert", "Invisible"]:
366
+ logger.trace(f"Attribute `{key}` is not supported for `{EGERIA_USAGE_LEVEL}` usage level. Skipping.",
367
+ highlight=True)
368
+ continue
369
+ if EGERIA_USAGE_LEVEL == "Expert" and level == "Invisible":
370
+ logger.trace(f"Attribute `{key}` is not supported for `{EGERIA_USAGE_LEVEL}` usage level. Skipping.",
371
+ highlight=True)
372
+ continue
373
+
374
+ if attr[key].get('input_required', False) is True:
375
+ if_missing = ERROR
376
+ else:
377
+ if_missing = INFO
378
+
379
+ # lab = [item.strip() for item in re.split(r'[;,\n]+',attr[key]['attr_labels'])]
380
+ lab = split_tb_string(attr[key]['attr_labels'])
381
+ labels: set = set()
382
+ labels.add(key.strip())
383
+
384
+ if lab is not None and lab != [""]:
385
+ labels.update(lab)
386
+
387
+ # Keep track of all known labels for later filtering of kwargs
388
+ for lab_entry in labels:
389
+ if lab_entry is not None and lab_entry != "":
390
+ known_labels.add(lab_entry.strip())
391
+
392
+ # set these to none since not needed for view commands
393
+ version = None
394
+ command_qn_prefix = None
395
+
396
+ default_value = attr[key].get('default_value', None)
397
+
398
+ style = attr[key]['style']
399
+ if style in ['Simple', 'Comment']:
400
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value)
401
+ elif style == 'Dictionary':
402
+ parsed_attributes[key] = proc_dictionary_attribute(txt, object_action, labels, if_missing,
403
+ default_value)
404
+ parsed_attributes[key]['name_list'] = json.dumps(parsed_attributes[key]['value'], indent=2)
405
+ elif style == 'Valid Value':
406
+ parsed_attributes[key] = proc_valid_value(txt, object_action, labels,
407
+ attr[key].get('valid_values', None), if_missing,
408
+ default_value)
409
+ elif style == 'QN':
410
+ parsed_attributes[key] = proc_el_id(egeria_client, command_display_name, command_qn_prefix, labels, txt,
411
+ object_action, version, if_missing)
412
+ if key == 'Qualified Name' and parsed_attributes[key]['value'] and parsed_attributes[key][
413
+ 'exists'] is False:
414
+ parsed_output['exists'] = False
415
+ elif style == 'ID':
416
+ parsed_attributes[key] = proc_el_id(egeria_client, command_display_name, command_qn_prefix, labels, txt,
417
+ object_action, version, if_missing)
418
+
419
+ parsed_output['guid'] = parsed_attributes[key].get('guid', None)
420
+ parsed_output['qualified_name'] = parsed_attributes[key].get('qualified_name', None)
421
+ parsed_output['exists'] = parsed_attributes.get(key,{}).get('exists',None)
422
+ if parsed_attributes.get(key,{}).get('valid',None) is False:
423
+ parsed_output['valid'] = False
424
+ parsed_output['reason'] += parsed_attributes.get(key,{}).get('reason',None)
425
+
426
+ elif style == 'Reference Name':
427
+ parsed_attributes[key] = proc_ids(egeria_client, key, labels, txt, object_action, if_missing)
428
+ if ((if_missing == ERROR) and parsed_attributes[key].get("value", None) is None):
429
+ msg = f"Required parameter `{parsed_attributes.get(key,{}).get('value',None)}` is missing"
430
+ logger.error(msg)
431
+ parsed_output['valid'] = False
432
+ parsed_output['reason'] += msg
433
+ elif parsed_attributes.get(key,{}).get('value',None) and parsed_attributes.get(key,{}).get('exists',None) is False:
434
+ msg = f"Reference Name `{parsed_attributes.get(key,{}).get('value',None)}` is specified but does not exist"
435
+ logger.error(msg)
436
+ parsed_output['valid'] = False
437
+ parsed_output['reason'] += msg
438
+
439
+ elif style == 'GUID':
440
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing)
441
+ elif style == 'Ordered Int':
442
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing)
443
+ elif style == 'Simple Int':
444
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value, "int")
445
+ elif style == 'Simple List':
446
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value)
447
+ name_list = parsed_attributes[key]['value']
448
+ # attribute = re.split(r'[;,\n]+', name_list) if name_list is not None else None
449
+ attribute = split_tb_string(name_list)
450
+ parsed_attributes[key]['value'] = attribute
451
+ parsed_attributes[key]['name_list'] = name_list
452
+ elif style == 'Parent':
453
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value)
454
+ elif style == 'Bool':
455
+ parsed_attributes[key] = proc_bool_attribute(txt, object_action, labels, if_missing, default_value)
456
+ elif style == "Dictionary List":
457
+ parsed_attributes[key] = proc_simple_attribute(txt, object_action, labels, if_missing, default_value)
458
+ # parsed_attributes[key]['list'] = json.loads(parsed_attributes[key]['value'])
459
+
460
+ elif style == 'Reference Name List':
461
+ parsed_attributes[key] = proc_name_list(egeria_client, key, txt, labels, if_missing)
462
+ if (parsed_attributes[key].get("value", None) and (
463
+ parsed_attributes[key]['exists'] is False or parsed_attributes[key]['valid'] is False)):
464
+ msg = (f"A Reference Name in `{parsed_attributes[key].get('name_list', None)}` is specified but "
465
+ f"does not exist")
466
+ logger.error(msg)
467
+ parsed_output['valid'] = False
468
+ parsed_output['reason'] += msg
469
+ else:
470
+ msg = f"Unknown attribute style: {style}"
471
+ logger.error(msg)
472
+ sys.exit(1)
473
+ parsed_attributes[key]['valid'] = False
474
+ parsed_attributes[key]['value'] = None
475
+
476
+ value = parsed_attributes[key].get('value', None)
477
+
478
+ if value is not None:
479
+ # if the value is a dict or list, get the stringifiedt
480
+ value = parsed_attributes[key].get('name_list', None) if isinstance(value, (dict, list)) else value
481
+ parsed_output['display'] += f"\n\t* {key}: `{value}`\n\t"
482
+
483
+ parsed_output['attributes'] = parsed_attributes
484
+
485
+ # Now, collect any unrecognized commands into kwargs
486
+ # Find all level-2 headings in the text
487
+ all_headings = set(re.findall(r"^\s*##\s*([^\n#]+)", txt, flags=re.MULTILINE))
488
+
489
+ kwargs: dict = {}
490
+ for heading in all_headings:
491
+ h = heading.strip()
492
+ if not h:
493
+ continue
494
+ if h in known_labels:
495
+ continue # already known/processed
496
+ # Parse this unknown attribute using the simple attribute logic
497
+ parsed = proc_simple_attribute(txt, object_action, {h}, INFO, None)
498
+ value = parsed.get('value', None)
499
+ if value is not None:
500
+ kwargs[_to_snake_case(h)] = value.replace('\n', '') if isinstance(value, str) else value
501
+
502
+ if kwargs:
503
+ parsed_output['kwargs'] = kwargs
504
+
505
+
506
+ if directive in ["validate", "process"] and not parsed_output['valid'] and object_action == "Update":
507
+ msg = f"Request is invalid, `{object_action} {object_type}` is not valid - see previous messages\n"
508
+ logger.error(msg)
509
+
510
+
511
+ return parsed_output
512
+
513
+
514
+ @logger.catch
515
+ def proc_simple_attribute(txt: str, action: str, labels: set, if_missing: str = INFO, default_value=None,
516
+ simp_type: str = None) -> dict:
517
+ """Process a simple attribute based on the provided labels and if_missing value.
518
+ Extract the attribute value from the text and store it in a dictionary along with valid.
519
+ If it doesn`t exist, mark the dictionary entry as invalid and print an error message with severity of if_missing.
520
+
521
+ Parameters:
522
+ ----------
523
+ txt: str
524
+ The block of object_action text to extract attributes from.
525
+ labels: list
526
+ The possible attribute labels to search for. The first label will be used in messages.
527
+ if_missing: str, default is INFO
528
+ Can be one of "WARNING", "ERROR", "INFO". The severity of the missing attribute.
529
+ default_value: default is None
530
+ The default value to return if the attribute is missing.
531
+ """
532
+ valid = True
533
+
534
+ if if_missing not in ["WARNING", "ERROR", "INFO"]:
535
+ msg = "Invalid severity for missing attribute"
536
+ logger.error(msg)
537
+ return {"status": ERROR, "reason": msg, "value": None, "valid": False}
538
+
539
+ if default_value == "":
540
+ default_value = None
541
+
542
+ attribute = extract_attribute(txt, labels)
543
+
544
+ # attribute = default_value if attribute is None else attribute.replace('\n', '')
545
+ attribute = default_value if attribute is None else attribute
546
+ if isinstance(attribute, str):
547
+ attribute = re.sub(r'\n+', '\n', attribute)
548
+ attribute = None if attribute.startswith('___') or attribute.startswith('---') else attribute
549
+
550
+
551
+ if attribute is None:
552
+ if if_missing == INFO or if_missing == WARNING:
553
+ msg = f"Optional attribute with labels: `{labels}` missing"
554
+ valid = True
555
+ logger.info(msg)
556
+ else:
557
+ msg = f"Missing attribute with labels `{labels}` "
558
+ valid = False
559
+ logger.error(msg)
560
+ return {"status": if_missing, "reason": msg, "value": None, "valid": valid, "exists": False}
561
+
562
+ if attribute and simp_type == "int" :
563
+ attribute = int(attribute)
564
+ # elif attribute and simp_type == "list":
565
+ # if isinstance(attribute, str):
566
+ # attribute = [piece.strip() for piece in re.split(r'[,\n]', attribute) if piece.strip()]
567
+
568
+
569
+
570
+ return {"status": INFO, "OK": None, "value": attribute, "valid": valid, "exists": True}
571
+
572
+ @logger.catch
573
+ def proc_dictionary_attribute(txt: str, action: str, labels: set, if_missing: str = INFO, default_value=None,
574
+ simp_type: str = None) -> dict:
575
+ """Process a dictionary attribute based on the provided labels and if_missing value.
576
+ Extract the attribute value from the text and store it in a dictionary along with valid.
577
+ If it doesn`t exist, mark the dictionary entry as invalid and print an error message with severity of if_missing.
578
+
579
+ Parameters:
580
+ ----------
581
+ txt: str
582
+ The block of object_action text to extract attributes from.
583
+ labels: list
584
+ The possible attribute labels to search for. The first label will be used in messages.
585
+ if_missing: str, default is INFO
586
+ Can be one of "WARNING", "ERROR", "INFO". The severity of the missing attribute.
587
+ default_value: default is None
588
+ The default value to return if the attribute is missing.
589
+ """
590
+ valid = True
591
+
592
+ if if_missing not in ["WARNING", "ERROR", "INFO"]:
593
+ msg = "Invalid severity for missing attribute"
594
+ logger.error(msg)
595
+ return {"status": ERROR, "reason": msg, "value": None, "valid": False}
596
+
597
+ if default_value == "":
598
+ default_value = None
599
+
600
+ attr = extract_attribute(txt, labels)
601
+ # attribute = json.loads(attr) if attr is not None else default_value
602
+ attribute = parse_to_dict(attr)
603
+
604
+ if attribute is None:
605
+ if if_missing == INFO or if_missing == WARNING:
606
+ msg = f"Optional attribute with labels: `{labels}` missing"
607
+ valid = True
608
+ logger.info(msg)
609
+ else:
610
+ msg = f"Missing attribute with labels `{labels}` "
611
+ valid = False
612
+ logger.error(msg)
613
+ return {"status": if_missing, "reason": msg, "value": None, "valid": valid, "exists": False}
614
+
615
+ return {"status": INFO, "OK": None, "value": attribute, "valid": valid, "exists": True}
616
+
617
+ @logger.catch
618
+ def proc_valid_value(txt: str, action: str, labels: set, valid_values: [], if_missing: str = INFO,
619
+ default_value=None) -> dict:
620
+ """Process a string attribute to check that it is a member of the associated value values list.
621
+ Extract the attribute value from the text and store it in a dictionary along with valid.
622
+ If it doesn`t exist, mark the dictionary entry as invalid and print an error message with severity of if_missing.
623
+
624
+ Parameters:
625
+ ----------
626
+ txt: str
627
+ The block of object_action text to extract attributes from.
628
+ labels: list
629
+ The possible attribute labels to search for. The first label will be used in messages.
630
+ if_missing: str, default is INFO
631
+ Can be one of "WARNING", "ERROR", "INFO". The severity of the missing attribute.
632
+ default_value: default is None
633
+ The default value to return if the attribute is missing.
634
+ """
635
+ valid = True
636
+ v_values = []
637
+
638
+ if if_missing not in ["WARNING", "ERROR", "INFO"]:
639
+ msg = "Invalid severity for missing attribute"
640
+ logger.error(msg)
641
+ return {"status": ERROR, "reason": msg, "value": None, "valid": False}
642
+ if valid_values is None:
643
+ msg = "Missing valid values list"
644
+ logger.error(msg)
645
+ return {"status": WARNING, "reason": msg, "value": None, "valid": False}
646
+ if isinstance(valid_values, str):
647
+ # v_values = [item.strip() for item in re.split(r'[;,\n]+', valid_values)]
648
+ v_values = split_tb_string(valid_values)
649
+ if isinstance(valid_values, list):
650
+ v_values = valid_values
651
+ if not isinstance(v_values, list):
652
+ msg = "Valid values list is not a list"
653
+ logger.error(msg)
654
+ return {"status": WARNING, "reason": msg, "value": None, "valid": False}
655
+ if len(v_values) == 0:
656
+ msg = "Valid values list is empty"
657
+ logger.error(msg)
658
+ return {"status": WARNING, "reason": msg, "value": None, "valid": False}
659
+
660
+ attribute = extract_attribute(txt, labels)
661
+ if default_value == "":
662
+ default_value = None
663
+ attribute = default_value if attribute is None else attribute
664
+
665
+ if attribute is None:
666
+ if if_missing == INFO or if_missing == WARNING:
667
+ msg = f"Optional attribute with labels: `{labels}` missing"
668
+ logger.info(msg)
669
+ valid = True
670
+ else:
671
+ msg = f"Missing attribute with labels `{labels}` "
672
+ valid = False
673
+ logger.error(msg)
674
+ return {"status": if_missing, "reason": msg, "value": None, "valid": valid, "exists": False}
675
+ else:
676
+ # Todo: look at moving validation into pydantic or another style...
677
+ if "Status" in labels:
678
+ attribute = attribute.upper()
679
+ if attribute not in v_values:
680
+ msg = f"Invalid value for attribute `{labels}` attribute is `{attribute}`"
681
+ logger.warning(msg)
682
+ return {"status": WARNING, "reason": msg, "value": attribute, "valid": False, "exists": True}
683
+
684
+ return {"status": INFO, "OK": "OK", "value": attribute, "valid": valid, "exists": True}
685
+
686
+
687
+ @logger.catch
688
+ def proc_bool_attribute(txt: str, action: str, labels: set, if_missing: str = INFO, default_value=None) -> dict:
689
+ """Process a boolean attribute based on the provided labels and if_missing value.
690
+ Extract the attribute value from the text and store it in a dictionary along with valid.
691
+ If it doesn`t exist, mark the dictionary entry as invalid and print an error message with severity of if_missing.
692
+
693
+ Parameters:
694
+ ----------
695
+ txt: str
696
+ The block of object_action text to extract attributes from.
697
+ labels: list
698
+ The possible attribute labels to search for. The first label will be used in messages.
699
+ if_missing: str, default is INFO
700
+ Can be one of "WARNING", "ERROR", "INFO". The severity of the missing attribute.
701
+ default_value: default is None
702
+ The default value to return if the attribute is missing.
703
+ """
704
+ valid = True
705
+
706
+ if if_missing not in ["WARNING", "ERROR", "INFO"]:
707
+ msg = "Invalid severity for missing attribute"
708
+ logger.error(msg)
709
+ return {"status": ERROR, "reason": msg, "value": None, "valid": False}
710
+
711
+ attribute = extract_attribute(txt, labels)
712
+ if default_value == "":
713
+ default = None
714
+ else:
715
+ default = str_to_bool(default_value)
716
+ attribute = default if attribute is None else attribute
717
+
718
+ if attribute is None:
719
+ if if_missing == INFO or if_missing == WARNING:
720
+ msg = f"Optional attribute with labels: `{labels}` missing"
721
+ logger.info(msg)
722
+ valid = True
723
+ else:
724
+ msg = f"Missing attribute with labels `{labels}` "
725
+ valid = False
726
+ logger.error(msg)
727
+ return {"status": if_missing, "reason": msg, "value": None, "valid": valid, "exists": False}
728
+
729
+ if isinstance(attribute, str):
730
+ attribute = attribute.strip().lower()
731
+ if attribute in ["true", "yes", "1"]:
732
+ attribute = True
733
+ elif attribute in ["false", "no", "0"]:
734
+ attribute = False
735
+ else:
736
+ msg = f"Invalid value for boolean attribute `{labels}`"
737
+ logger.error(msg)
738
+ return {"status": ERROR, "reason": msg, "value": attribute, "valid": False, "exists": True}
739
+
740
+ return {"status": INFO, "OK": None, "value": attribute, "valid": valid, "exists": True}
741
+
742
+
743
+ @logger.catch
744
+ def proc_el_id(egeria_client: EgeriaTech, element_type: str, qn_prefix: str, element_labels: list[str], txt: str,
745
+ action: str, version: str = None, if_missing: str = INFO) -> dict:
746
+ """
747
+ Processes display_name and qualified_name by extracting them from the input text,
748
+ checking if the element exists in Egeria, and validating the information. If a qualified
749
+ name isn't found, one will be created.
750
+
751
+ Parameters
752
+ ----------
753
+ egeria_client: EgeriaTech
754
+ Client object for interacting with Egeria.
755
+ element_type: str
756
+ type of element to process (e.g., 'blueprint', 'category', 'term')
757
+ element_labels: a list of equivalent label names to use in processing the element.
758
+ txt: str
759
+ A string representing the input text to be processed for extracting element identifiers.
760
+ action: str
761
+ The action object_action to be executed (e.g., 'Create', 'Update', 'Display', ...)
762
+ version: str, optional = None
763
+ An optional version identifier used if we need to construct the qualified name
764
+
765
+ Returns: dict with keys:
766
+ status
767
+ reason
768
+ value - value of display name
769
+ valid - name we parse out
770
+ exists
771
+ qualified_name - qualified name - either that we find or the one we construct
772
+ guid - guid of the element if it already exists
773
+ """
774
+ valid = True
775
+ exists = False
776
+ identifier_output = {}
777
+
778
+ element_name = extract_attribute(txt, element_labels)
779
+ qualified_name = extract_attribute(txt, ["Qualified Name"])
780
+
781
+ if element_name is None:
782
+ msg = f"Optional attribute with label`{element_type}` missing"
783
+ logger.info(msg)
784
+ identifier_output = {"status": INFO, "reason": msg, "value": None, "valid": False, "exists": False, }
785
+ return identifier_output
786
+
787
+ if qualified_name:
788
+ q_name, guid, unique, exists = get_element_by_name(egeria_client, element_type,
789
+ qualified_name) # Qualified name could be different if it
790
+ # is being updated
791
+ else:
792
+ q_name, guid, unique, exists = get_element_by_name(egeria_client, element_type, element_name)
793
+ qualified_name = q_name
794
+
795
+ if unique is False:
796
+ msg = f"Multiple elements named {element_name} found"
797
+ logger.error(msg)
798
+ identifier_output = {"status": ERROR, "reason": msg, "value": element_name, "valid": False, "exists": True, }
799
+ valid = False
800
+
801
+ if action == "Update" and not exists:
802
+ msg = f"Element {element_name} does not exist"
803
+ logger.error(msg)
804
+ identifier_output = {"status": ERROR, "reason": msg, "value": element_name, "valid": False, "exists": False, }
805
+
806
+ elif action in ["Update", "View", "Link", "Detach"] and exists:
807
+ msg = f"Element {element_name} exists"
808
+ logger.info(msg)
809
+ identifier_output = {
810
+ "status": INFO, "reason": msg, "value": element_name, "valid": True, "exists": True,
811
+ 'qualified_name': q_name, 'guid': guid
812
+ }
813
+
814
+ elif action == "Create" and exists:
815
+ msg = f"Element {element_name} already exists"
816
+ logger.error(msg)
817
+ identifier_output = {
818
+ "status": ERROR, "reason": msg, "value": element_name, "valid": False, "exists": True,
819
+ 'qualified_name': qualified_name, 'guid': guid,
820
+ }
821
+
822
+ elif action == "Create" and not exists and valid:
823
+ msg = f"{element_type} `{element_name}` does not exist"
824
+ logger.info(msg)
825
+
826
+ if q_name is None and qualified_name is None:
827
+ q_name = egeria_client.__create_qualified_name__(qn_prefix, element_name, LOCAL_QUALIFIER, version)
828
+ update_element_dictionary(q_name, {'display_name': element_name})
829
+
830
+ elif qualified_name:
831
+ update_element_dictionary(qualified_name, {'display_name': element_name})
832
+ q_name = qualified_name
833
+
834
+ identifier_output = {
835
+ "status": INFO, "reason": msg, "value": element_name, "valid": True, "exists": False,
836
+ 'qualified_name': q_name
837
+ }
838
+
839
+ return identifier_output
840
+
841
+
842
+ @logger.catch
843
+ def proc_ids(egeria_client: EgeriaTech, element_type: str, element_labels: set, txt: str, action: str,
844
+ if_missing: str = INFO, version: str = None) -> dict:
845
+ """
846
+ Processes element identifiers from the input text using the labels supplied,
847
+ checking if the element exists in Egeria, and validating the information.
848
+ Only a single element is allowed.
849
+
850
+ Parameters
851
+ ----------
852
+ egeria_client: EgeriaTech
853
+ Client object for interacting with Egeria.
854
+ element_type: str
855
+ type of element to process (e.g., 'blueprint', 'category', 'term')
856
+ element_labels: a list of equivalent label names to use in processing the element.
857
+ txt: str
858
+ A string representing the input text to be processed for extracting element identifiers.
859
+ action: str
860
+ The action object_action to be executed (e.g., 'Create', 'Update', 'Display', ...)
861
+ if_missing: str, optional = None
862
+ Optional version identifier used if we need to construct the qualified name
863
+ version: str, optional = INFO
864
+ What to do if the element doesn't exist. Default is INFO. Can be one of "WARNING", "ERROR", "INFO".
865
+
866
+ Returns: dict with keys:
867
+ status
868
+ reason
869
+ value
870
+ valid - name we parse out
871
+ exists
872
+ qualified_name - what we find exists
873
+ guid
874
+ """
875
+ valid = True
876
+ exists = False
877
+ identifier_output = {}
878
+ unique = True
879
+ value = None
880
+
881
+ element_name = extract_attribute(txt, element_labels)
882
+
883
+ if element_name:
884
+ if element_type == "Tag ID": # Special case for informal tags
885
+ element_type = "InformalTag"
886
+
887
+ if '\n' in element_name or ',' in element_name:
888
+ msg = f"Element name `{element_name}` appears to be a list rather than a single element"
889
+ logger.error(msg)
890
+ return {"status": ERROR, "reason": msg, "value": None, "valid": False, "exists": False, }
891
+ q_name, guid, unique, exists = get_element_by_name(egeria_client, element_type, element_name)
892
+ value = element_name
893
+ else:
894
+ exists = False
895
+ q_name = None
896
+ unique = None
897
+
898
+ if exists is True and unique is False:
899
+ # Multiple elements found - so need to respecify with qualified name
900
+ msg = f"Multiple elements named {element_name} found"
901
+ logger.error(msg)
902
+ identifier_output = {"status": ERROR, "reason": msg, "value": element_name, "valid": False, "exists": True, }
903
+
904
+
905
+ elif action == EXISTS_REQUIRED or if_missing == ERROR and not exists:
906
+ # a required identifier doesn't exist
907
+ msg = f"Required {element_type} `{element_name}` does not exist"
908
+ logger.error(msg)
909
+ identifier_output = {"status": ERROR, "reason": msg, "value": element_name, "valid": False, "exists": False, }
910
+ elif value is None and if_missing == INFO:
911
+ # an optional identifier is empty
912
+ msg = f"Optional attribute with label`{element_type}` missing"
913
+ logger.info(msg)
914
+ identifier_output = {"status": INFO, "reason": msg, "value": None, "valid": True, "exists": False, }
915
+ elif value and exists is False:
916
+ # optional identifier specified but doesn't exist
917
+ msg = f"Optional attribute with label`{element_type}` specified but doesn't exist"
918
+ logger.error(msg)
919
+ identifier_output = {"status": ERROR, "reason": msg, "value": value, "valid": False, "exists": False, }
920
+
921
+ else:
922
+ # all good.
923
+ msg = f"Element {element_type} `{element_name}` exists"
924
+ logger.info(msg)
925
+ identifier_output = {
926
+ "status": INFO, "reason": msg, "value": element_name, "valid": True, "exists": True,
927
+ "qualified_name": q_name, 'guid': guid
928
+ }
929
+
930
+ return identifier_output
931
+
932
+
933
+ @logger.catch
934
+ def proc_name_list(egeria_client: EgeriaTech, element_type: str, txt: str, element_labels: set,
935
+ if_missing: str = INFO) -> dict:
936
+ """
937
+ Processes a list of names specified in the given text, retrieves details for each
938
+ element based on the provided type, and generates a list of valid qualified names.
939
+
940
+ The function reads a text block, extracts a list of element names according to the specified
941
+ element type, looks them up using the provided Egeria client, and classifies them as valid or
942
+ invalid. It returns the processed names, a list of qualified names, and validity and existence
943
+ flags.
944
+
945
+ Args:
946
+
947
+ egeria_client (EgeriaTech): The client instance to connect and query elements from an
948
+ external system.
949
+ Element_type (str): The type of element, such as schema or attribute, to process.
950
+ Txt (str): The raw input text containing element names to be processed.
951
+ element_labels: a list of equivalent label names to use in processing the element.
952
+ required: bool, default is False
953
+ - indicates whether the list of names is required to be present in the input text.
954
+
955
+ Returns:
956
+ Dict containing:
957
+ 'names' - Concatenated valid input names as a single string (or None if empty).
958
+ 'name_list' - A list of known qualified names extracted from the processed elements.
959
+ 'valid' - A boolean indicating whether all elements are valid.
960
+ 'exists' - A boolean indicating whether all elements exist.
961
+ """
962
+ valid = True
963
+ exists = True
964
+ id_list_output = {}
965
+ elements = ""
966
+ new_element_list = []
967
+ guid_list = []
968
+ elements_txt = extract_attribute(txt, element_labels)
969
+
970
+ if elements_txt is None:
971
+ msg = f"Attribute with labels `{{element_type}}` missing"
972
+ logger.debug(msg)
973
+ return {"status": if_missing, "reason": msg, "value": None, "valid": False, "exists": False, }
974
+ else:
975
+ # element_list = re.split(r'[;,\n]+', elements_txt)
976
+ element_list = split_tb_string(elements_txt)
977
+ element_details = {}
978
+ for element in element_list:
979
+ # Get the element using the generalized function
980
+ known_q_name, known_guid, el_valid, el_exists = get_element_by_name(egeria_client, element_type, element)
981
+ details = {"known_q_name": known_q_name, "known_guid": known_guid, "el_valid": el_valid}
982
+ elements += element + ", " # list of the input names
983
+
984
+ if el_exists and el_valid:
985
+ new_element_list.append(known_q_name) # list of qualified names
986
+ guid_list.append(known_guid)
987
+ elif not el_exists:
988
+ msg = f"No {element_type} `{element}` found"
989
+ logger.debug(msg)
990
+ valid = False
991
+ valid = valid if el_valid is None else (valid and el_valid)
992
+ exists = exists and el_exists
993
+ element_details[element] = details
994
+
995
+ if elements:
996
+ msg = f"Found {element_type}: {elements}"
997
+ logger.debug(msg)
998
+ id_list_output = {
999
+ "status": INFO, "reason": msg, "value": element_details, "valid": valid, "exists": exists,
1000
+ "name_list": new_element_list, "guid_list": guid_list,
1001
+ }
1002
+ else:
1003
+ msg = f" Name list contains one or more invalid qualified names."
1004
+ logger.debug(msg)
1005
+ id_list_output = {
1006
+ "status": if_missing, "reason": msg, "value": elements, "valid": valid, "exists": exists,
1007
+ "dict_list": element_details, "guid_list": guid_list
1008
+ }
1009
+ return id_list_output
1010
+
1011
+
1012
+ @logger.catch
1013
+ def sync_collection_memberships(egeria_client: EgeriaTech, guid: str, get_method: callable, collection_types: list,
1014
+ to_be_collection_guids:list, merge_update: bool = True)-> None:
1015
+ """
1016
+ Synchronize collection memberships for an element.
1017
+
1018
+ Parameters
1019
+ - egeria_client: EgeriaTech composite client used to call add/remove operations.
1020
+ - guid: the GUID of the element (e.g., GlossaryTerm) whose memberships we sync.
1021
+ - get_method: callable to fetch the element details by guid; must accept (guid, output_format="JSON").
1022
+ - collection_types: list of collection type identifiers to consider when syncing (e.g., ["Glossary", "Folder"]).
1023
+ - to_be_collection_guids: list of lists of GUIDs corresponding positionally to collection_types; may contain None.
1024
+ - merge_update: if True, only add missing memberships; if False, remove existing memberships for the
1025
+ specified collection_types and then add the desired memberships.
1026
+
1027
+ Behavior
1028
+ - When merge_update is True: determine the element's current memberships and add the missing ones only.
1029
+ - When merge_update is False: remove the element from all collections of the specified types, then add the
1030
+ provided target memberships for those types.
1031
+ """
1032
+ try:
1033
+ # Defensive defaults and shape normalization
1034
+ collection_types = collection_types or []
1035
+ to_be_collection_guids = to_be_collection_guids or []
1036
+ # Ensure the lists align by index; pad to length
1037
+ max_len = max(len(collection_types), len(to_be_collection_guids)) if (collection_types or to_be_collection_guids) else 0
1038
+ if len(collection_types) < max_len:
1039
+ collection_types = collection_types + [None] * (max_len - len(collection_types))
1040
+ if len(to_be_collection_guids) < max_len:
1041
+ to_be_collection_guids = to_be_collection_guids + [None] * (max_len - len(to_be_collection_guids))
1042
+
1043
+ # Get current element details with raw JSON to inspect relationships
1044
+ element = None
1045
+ try:
1046
+ element = get_method(guid, output_format="JSON")
1047
+ except TypeError:
1048
+ # Some get methods require element_type parameter; fallback best-effort
1049
+ element = get_method(guid, element_type=None, output_format="JSON")
1050
+ if isinstance(element, str):
1051
+ # e.g., "No elements found"; nothing to do
1052
+ logger.debug(f"sync_collection_memberships: element lookup returned: {element}")
1053
+ return
1054
+ if not isinstance(element, dict):
1055
+ logger.debug("sync_collection_memberships: element lookup did not return a dict; skipping")
1056
+ return
1057
+
1058
+ member_rels = element.get("memberOfCollections", []) or []
1059
+
1060
+ # Build current membership maps
1061
+ # - by GUID: set of current collection guids
1062
+ # - by type name (classification names found on related collection): map type->set(guids)
1063
+ current_all_guids: set[str] = set()
1064
+ current_by_type: dict[str, set[str]] = {}
1065
+
1066
+ for rel in member_rels:
1067
+ try:
1068
+ related = (rel or {}).get("relatedElement", {})
1069
+ rel_guid = ((related.get("elementHeader") or {}).get("guid"))
1070
+ if not rel_guid:
1071
+ continue
1072
+ current_all_guids.add(rel_guid)
1073
+
1074
+ # Collect type hints from classifications and from properties.collectionType
1075
+ type_names: set[str] = set()
1076
+ classifications = ((related.get("elementHeader") or {}).get("classifications")) or []
1077
+ for cls in classifications:
1078
+ tname = (((cls or {}).get("type") or {}).get("typeName"))
1079
+ if tname:
1080
+ type_names.add(tname)
1081
+ ctype = ((related.get("properties") or {}).get("collectionType"))
1082
+ if isinstance(ctype, str) and ctype:
1083
+ type_names.add(ctype)
1084
+
1085
+ if not type_names:
1086
+ # Fallback: try elementHeader.type.typeName
1087
+ tname2 = (((related.get("elementHeader") or {}).get("type") or {}).get("typeName"))
1088
+ if tname2:
1089
+ type_names.add(tname2)
1090
+
1091
+ for tn in type_names:
1092
+ s = current_by_type.setdefault(tn, set())
1093
+ s.add(rel_guid)
1094
+ except Exception as e:
1095
+ logger.debug(f"sync_collection_memberships: skipping malformed relationship: {e}")
1096
+ continue
1097
+
1098
+ # Helper to coerce incoming desired list entry to a set of guids
1099
+ def to_guid_set(maybe_list) -> set[str]:
1100
+ if not maybe_list:
1101
+ return set()
1102
+ if isinstance(maybe_list, list):
1103
+ return {g for g in maybe_list if isinstance(g, str) and g}
1104
+ # Sometimes a single guid may slip through
1105
+ if isinstance(maybe_list, str):
1106
+ return {maybe_list}
1107
+ return set()
1108
+
1109
+ # If merge_update is False: remove all existing memberships for the specified types
1110
+ if not merge_update:
1111
+ # Build a set of guids to remove across specified types
1112
+ to_remove: set[str] = set()
1113
+ for t in collection_types:
1114
+ if not t:
1115
+ continue
1116
+ # Match by exact type name as seen in current_by_type
1117
+ guids_for_type = current_by_type.get(t) or set()
1118
+ if not guids_for_type and t.lower() in {k.lower() for k in current_by_type.keys()}:
1119
+ # Case-insensitive fallback
1120
+ for k, v in current_by_type.items():
1121
+ if k.lower() == t.lower():
1122
+ guids_for_type = v
1123
+ break
1124
+ to_remove.update(guids_for_type)
1125
+
1126
+ for coll_guid in to_remove:
1127
+ try:
1128
+ egeria_client.remove_from_collection(coll_guid, guid)
1129
+ logger.info(f"Removed element {guid} from collection {coll_guid}")
1130
+ except Exception as e:
1131
+ logger.debug(f"Failed to remove element {guid} from collection {coll_guid}: {e}")
1132
+
1133
+ # Now add desired memberships (for both merge and replace flows)
1134
+ for idx, t in enumerate(collection_types):
1135
+ desired_set = to_guid_set(to_be_collection_guids[idx] if idx < len(to_be_collection_guids) else None)
1136
+ if not desired_set:
1137
+ continue
1138
+ for coll_guid in desired_set:
1139
+ # If merge_update True, skip if already a member; if False, we removed earlier so can re-add
1140
+ if merge_update and coll_guid in current_all_guids:
1141
+ continue
1142
+ try:
1143
+ egeria_client.add_to_collection(coll_guid, guid)
1144
+ logger.info(f"Added element {guid} to collection {coll_guid}")
1145
+ except Exception as e:
1146
+ logger.debug(f"Failed to add element {guid} to collection {coll_guid}: {e}")
1147
+
1148
+ return
1149
+ except Exception as e:
1150
+ logger.error(f"sync_collection_memberships: unexpected error: {e}")
1151
+ return
1152
+
1153
+ @logger.catch
1154
+ def process_output_command(egeria_client: EgeriaTech, txt: str, directive: str = "display") -> Optional[str]:
1155
+ """
1156
+ Processes a generic output request by extracting attributes (including Output Format and
1157
+ Output Format Set) and dynamically invoking the find function specified by the
1158
+ report_spec, following the approach used in commands/cat/list_format_set.
1159
+
1160
+ This is modeled on process_gov_definition_list_command but uses the dynamic
1161
+ dispatch via the output format set rather than directly calling a specific
1162
+ egeria_client method.
1163
+
1164
+ :param egeria_client: EgeriaTech composite client instance
1165
+ :param txt: The command text (e.g., parsed from a markdown cell)
1166
+ :param directive: display | validate | process
1167
+ :return: Markdown string for processed output or None
1168
+ """
1169
+ command, object_type, object_action = extract_command_plus(txt)
1170
+ print(Markdown(f"# {command}\n"))
1171
+
1172
+ parsed_output = parse_view_command(egeria_client, object_type, object_action, txt, directive)
1173
+
1174
+ valid = parsed_output['valid']
1175
+ print(Markdown(f"Performing {command}"))
1176
+ print(Markdown(parsed_output['display']))
1177
+
1178
+ attr = parsed_output.get('attributes', {})
1179
+
1180
+ search_string = attr.get('Search String', {}).get('value', '*')
1181
+ output_format = attr.get('Output Format', {}).get('value', 'LIST')
1182
+ report_spec = attr.get('Output Format Set', {}).get('value', object_type)
1183
+
1184
+ if directive == "display":
1185
+ return None
1186
+ elif directive == "validate":
1187
+ # Validate that the format set exists and has an action
1188
+ fmt = select_report_spec(report_spec, "ANY") if valid else None
1189
+ if valid and fmt and fmt.get("action"):
1190
+ print(Markdown(f"==> Validation of {command} completed successfully!\n"))
1191
+ return True
1192
+ else:
1193
+ msg = f"Validation failed for object_action `{command}`"
1194
+ logger.error(msg)
1195
+ return False
1196
+
1197
+ elif directive == "process":
1198
+ try:
1199
+ if not valid:
1200
+ msg = f"Validation failed for {object_action} `{object_type}`"
1201
+ logger.error(msg)
1202
+ return None
1203
+
1204
+ # Resolve the find function from the output format set
1205
+ fmt = select_report_spec(report_spec, output_format)
1206
+ if not fmt:
1207
+ logger.error(f"Output format set '{report_spec}' not found or not compatible with '{output_format}'.")
1208
+ return None
1209
+ action = fmt.get("action", {})
1210
+ func_spec = action.get("function")
1211
+ if not func_spec or "." not in func_spec:
1212
+ func_spec = f"EgeriaTech.find_{object_type.replace(' ', '_').lower()}"
1213
+
1214
+
1215
+ # Extract method name and get it from the composite client
1216
+ _, method_name = func_spec.split(".", 1)
1217
+ if not hasattr(egeria_client, method_name):
1218
+ logger.error(f"Method '{method_name}' not available on EgeriaTech client.")
1219
+ return None
1220
+ method = getattr(egeria_client, method_name)
1221
+
1222
+ # Build body and params
1223
+ list_md = f"\n# `{object_type}` with filter: `{search_string}`\n\n"
1224
+ body = set_find_body(object_type, attr)
1225
+
1226
+ params = {
1227
+ 'search_string': search_string,
1228
+ 'body': body,
1229
+ 'output_format': output_format,
1230
+ 'report_spec': report_spec,
1231
+ }
1232
+
1233
+ # Call the resolved method
1234
+ struct = method(**params)
1235
+
1236
+ if output_format.upper() == "DICT":
1237
+ list_md += f"```\n{json.dumps(struct, indent=4)}\n```\n"
1238
+ else:
1239
+ list_md += struct
1240
+ logger.info(f"Wrote `{object_type}` for search string: `{search_string}` using format set '{report_spec}'")
1241
+
1242
+ return list_md
1243
+
1244
+ except Exception as e:
1245
+ logger.error(f"Error performing {command}: {e}")
1246
+ console.print_exception(show_locals=True)
1247
+ return None
1248
+ else:
1249
+ return None