pyegeria 5.4.0.22__py3-none-any.whl → 5.4.0.24__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.
- commands/cat/.DS_Store +0 -0
- commands/cat/.env +8 -0
- commands/cat/debug_log +2046 -465
- commands/cat/debug_log.2025-08-15_09-14-07_444802.zip +0 -0
- commands/cat/debug_log.2025-08-16_10-21-59_388912.zip +0 -0
- commands/cat/debug_log.log +0 -0
- commands/cat/dr_egeria_md.py +16 -3
- commands/cat/list_collections.py +15 -6
- commands/cat/list_format_set.py +90 -85
- commands/cli/debug_log.log +0 -0
- commands/ops/logs/pyegeria.log +0 -0
- md_processing/.DS_Store +0 -0
- md_processing/__init__.py +5 -3
- md_processing/data/commands.json +8310 -903
- md_processing/dr-egeria-outbox/Collections-2025-08-12-13-30-37.md +163 -0
- md_processing/dr-egeria-outbox/Collections-2025-08-12-13-35-58.md +474 -0
- md_processing/dr_egeria_inbox/Derive-Dr-Gov-Defs.md +8 -0
- md_processing/dr_egeria_inbox/Dr.Egeria Templates.md +873 -0
- md_processing/dr_egeria_inbox/arch_test.md +57 -0
- md_processing/dr_egeria_inbox/archive/dr_egeria_intro.md +254 -0
- md_processing/dr_egeria_inbox/archive/dr_egeria_intro_more_terms.md +696 -0
- md_processing/dr_egeria_inbox/archive/dr_egeria_intro_part1.md +254 -0
- md_processing/dr_egeria_inbox/archive/dr_egeria_intro_part2.md +298 -0
- md_processing/dr_egeria_inbox/archive/dr_egeria_intro_part3.md +608 -0
- md_processing/dr_egeria_inbox/archive/dr_egeria_intro_part4.md +94 -0
- md_processing/dr_egeria_inbox/archive/freddie_intro.md +284 -0
- md_processing/dr_egeria_inbox/archive/freddie_intro_orig.md +275 -0
- md_processing/dr_egeria_inbox/archive/test-term.md +110 -0
- md_processing/dr_egeria_inbox/cat_test.md +100 -0
- md_processing/dr_egeria_inbox/collections.md +39 -0
- md_processing/dr_egeria_inbox/data_designer_debug.log +6 -0
- md_processing/dr_egeria_inbox/data_designer_out.md +60 -0
- md_processing/dr_egeria_inbox/data_designer_search_test.md +11 -0
- md_processing/dr_egeria_inbox/data_field.md +54 -0
- md_processing/dr_egeria_inbox/data_spec.md +77 -0
- md_processing/dr_egeria_inbox/data_spec_test.md +2406 -0
- md_processing/dr_egeria_inbox/data_test.md +179 -0
- md_processing/dr_egeria_inbox/data_test2.md +429 -0
- md_processing/dr_egeria_inbox/data_test3.md +462 -0
- md_processing/dr_egeria_inbox/dr_egeria_data_designer_1.md +124 -0
- md_processing/dr_egeria_inbox/dr_egeria_intro_categories.md +168 -0
- md_processing/dr_egeria_inbox/dr_egeria_intro_part1.md +280 -0
- md_processing/dr_egeria_inbox/dr_egeria_intro_part2.md +313 -0
- md_processing/dr_egeria_inbox/dr_egeria_intro_part3.md +1073 -0
- md_processing/dr_egeria_inbox/dr_egeria_isc1.md +44 -0
- md_processing/dr_egeria_inbox/generated_help_report.md +9 -0
- md_processing/dr_egeria_inbox/glossary_list.md +5 -0
- md_processing/dr_egeria_inbox/glossary_search_test.md +40 -0
- md_processing/dr_egeria_inbox/glossary_test1.md +324 -0
- md_processing/dr_egeria_inbox/gov_def.md +482 -0
- md_processing/dr_egeria_inbox/gov_def2.md +447 -0
- md_processing/dr_egeria_inbox/img.png +0 -0
- md_processing/dr_egeria_inbox/product.md +162 -0
- md_processing/dr_egeria_inbox/rel.md +8 -0
- md_processing/dr_egeria_inbox/sb.md +119 -0
- md_processing/dr_egeria_inbox/solution-components.md +136 -0
- md_processing/dr_egeria_inbox/solution_blueprints.md +118 -0
- md_processing/dr_egeria_inbox/synonym_test.md +42 -0
- md_processing/dr_egeria_inbox/t2.md +268 -0
- md_processing/dr_egeria_outbox/.obsidian/app.json +1 -0
- md_processing/dr_egeria_outbox/.obsidian/appearance.json +1 -0
- md_processing/dr_egeria_outbox/.obsidian/community-plugins.json +6 -0
- md_processing/dr_egeria_outbox/.obsidian/core-plugins.json +31 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/calendar/data.json +10 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/calendar/main.js +4459 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/calendar/manifest.json +10 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/obsidian-kanban/data.json +3 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/obsidian-kanban/main.js +153 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/obsidian-kanban/manifest.json +11 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/obsidian-kanban/styles.css +1 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/obsidian-tasks-plugin/main.js +500 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/templater-obsidian/main.js +37 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/templater-obsidian/manifest.json +11 -0
- md_processing/dr_egeria_outbox/.obsidian/plugins/templater-obsidian/styles.css +220 -0
- md_processing/dr_egeria_outbox/.obsidian/types.json +28 -0
- md_processing/dr_egeria_outbox/.obsidian/workspace.json +220 -0
- md_processing/dr_egeria_outbox/Untitled.canvas +1 -0
- md_processing/dr_egeria_outbox/monday/processed-2025-07-14 12:38-data_designer_out.md +663 -0
- md_processing/dr_egeria_outbox/monday/processed-2025-07-21 10:52-generated_help_report.md +2744 -0
- md_processing/dr_egeria_outbox/monday/processed-2025-07-21 18:38-collections.md +62 -0
- md_processing/dr_egeria_outbox/monday/processed-2025-08-01 11:34-gov_def.md +444 -0
- md_processing/dr_egeria_outbox/monday/processed-2025-08-17 21:04-product.md +97 -0
- md_processing/dr_egeria_outbox/sunday/processed-2025-07-20 14:55-product.md +77 -0
- md_processing/dr_egeria_outbox/sunday/processed-2025-07-20 15:05-product.md +75 -0
- md_processing/dr_egeria_outbox/sunday/processed-2025-07-20 15:11-product.md +74 -0
- md_processing/dr_egeria_outbox/sunday/processed-2025-07-20 20:40-collections.md +49 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-17 15:00-Derive-Dr-Gov-Defs.md +719 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-17 20:13-Derive-Dr-Gov-Defs.md +41 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-17 20:14-Derive-Dr-Gov-Defs.md +33 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-17 20:50-Derive-Dr-Gov-Defs.md +192 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-17 22:08-gov_def2.md +486 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-17 22:10-gov_def2.md +486 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-18 08:53-gov_def2.md +486 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-18 08:54-gov_def2.md +486 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-18 09:03-gov_def2.md +486 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-18 09:06-gov_def2.md +486 -0
- md_processing/dr_egeria_outbox/thursday/processed-2025-07-18 09:10-gov_def2.md +486 -0
- md_processing/dr_egeria_outbox/tuesday/processed-2025-07-16 19:15-gov_def2.md +527 -0
- md_processing/dr_egeria_outbox/tuesday/processed-2025-07-17 12:08-gov_def2.md +527 -0
- md_processing/dr_egeria_outbox/tuesday/processed-2025-07-17 14:27-gov_def2.md +485 -0
- md_processing/md_commands/governance_officer_commands.py +291 -150
- md_processing/md_commands/product_manager_commands.py +309 -401
- md_processing/md_processing_utils/common_md_proc_utils.py +110 -7
- md_processing/md_processing_utils/common_md_utils.py +112 -26
- md_processing/md_processing_utils/debug_log.log +0 -0
- md_processing/md_processing_utils/md_processing_constants.py +8 -5
- md_processing/md_processing_utils/solution_architect_log.log +0 -0
- pyegeria/.DS_Store +0 -0
- pyegeria/__init__.py +3 -3
- pyegeria/_client_new.py +48 -51
- pyegeria/_exceptions_new.py +6 -0
- pyegeria/_output_format_models.py +22 -17
- pyegeria/_output_formats.py +122 -34
- pyegeria/collection_manager.py +154 -50
- pyegeria/collection_manager_omvs.py +47 -18
- pyegeria/egeria_cat_client.py +1 -1
- pyegeria/egeria_client.py +6 -0
- pyegeria/egeria_tech_client.py +6 -1
- pyegeria/governance_officer.py +2513 -0
- pyegeria/load_config.py +1 -1
- pyegeria/models.py +48 -5
- pyegeria/output_formatter.py +298 -79
- pyegeria/utils.py +1 -1
- {pyegeria-5.4.0.22.dist-info → pyegeria-5.4.0.24.dist-info}/METADATA +1 -1
- {pyegeria-5.4.0.22.dist-info → pyegeria-5.4.0.24.dist-info}/RECORD +130 -30
- {pyegeria-5.4.0.22.dist-info → pyegeria-5.4.0.24.dist-info}/LICENSE +0 -0
- {pyegeria-5.4.0.22.dist-info → pyegeria-5.4.0.24.dist-info}/WHEEL +0 -0
- {pyegeria-5.4.0.22.dist-info → pyegeria-5.4.0.24.dist-info}/entry_points.txt +0 -0
pyegeria/load_config.py
CHANGED
@@ -381,7 +381,7 @@ def load_app_config(env_file: str = None):
|
|
381
381
|
|
382
382
|
# Logging section
|
383
383
|
log = config_dict["Logging"]
|
384
|
-
log["console_filter_levels"] = _parse_list_env("PYEGERIA_CONSOLE_FILTER_LEVELS", log.get("console_filter_levels", ["ERROR"]))
|
384
|
+
log["console_filter_levels"] = _parse_list_env("PYEGERIA_CONSOLE_FILTER_LEVELS", log.get("console_filter_levels", ["ERROR","WARNING","INFO","SUCCESS"]))
|
385
385
|
log["console_logging_enabled"] = _parse_list_env("PYEGERIA_CONSOLE_LOGGING_ENABLED", log.get("console_logging_enabled", ["pyegeria"]))
|
386
386
|
log["console_logging_level"] = os.getenv("PYEGERIA_CONSOLE_LOG_LVL", log.get("console_logging_level", "INFO"))
|
387
387
|
log["enable_logging"] = _parse_bool_env("PYEGERIA_ENABLE_LOGGING", log.get("enable_logging", False))
|
pyegeria/models.py
CHANGED
@@ -177,6 +177,33 @@ class OpenMetadataRootProperties(PyegeriaModel):
|
|
177
177
|
type_name: str | None = None
|
178
178
|
extended_properties: dict | None = None
|
179
179
|
|
180
|
+
@model_validator(mode='before')
|
181
|
+
@classmethod
|
182
|
+
def preprocess_data(cls, values):
|
183
|
+
"""
|
184
|
+
This model validator performs pre-processing on the entire
|
185
|
+
input dictionary before field validation.
|
186
|
+
It converts any empty tuples to None and ensures a metadata key exists.
|
187
|
+
|
188
|
+
This is the modern equivalent of @root_validator(pre=True).
|
189
|
+
"""
|
190
|
+
# Ensure the 'data' key exists before trying to access it.
|
191
|
+
if 'data' in values and isinstance(values['data'], dict):
|
192
|
+
# Convert empty tuples to None
|
193
|
+
processed_data = {}
|
194
|
+
for key, value in values['data'].items():
|
195
|
+
if isinstance(value, tuple) and not value:
|
196
|
+
processed_data[key] = None
|
197
|
+
else:
|
198
|
+
processed_data[key] = value
|
199
|
+
values['data'] = processed_data
|
200
|
+
|
201
|
+
# Ensure a 'metadata' key is always a dictionary.
|
202
|
+
if 'metadata' not in values:
|
203
|
+
values['metadata'] = {}
|
204
|
+
|
205
|
+
return values
|
206
|
+
|
180
207
|
|
181
208
|
class RelationshipBeanProperties(PyegeriaModel):
|
182
209
|
effective_from: datetime | None = None
|
@@ -215,7 +242,7 @@ class NewRelationshipRequestBody(RequestBody):
|
|
215
242
|
class_: Annotated[Literal["NewRelationshipRequestBody"], Field(alias="class")]
|
216
243
|
make_anchor: bool | None = False
|
217
244
|
anchor_scope_guid: str | None = None
|
218
|
-
properties: RelationshipBeanProperties
|
245
|
+
properties: dict | RelationshipBeanProperties | None = None
|
219
246
|
|
220
247
|
|
221
248
|
class DeleteRequestBody(RequestBody):
|
@@ -250,11 +277,11 @@ class NewElementRequestBody(RequestBody):
|
|
250
277
|
is_own_anchor: bool | None = True
|
251
278
|
anchor_scope_guid: str | None = None
|
252
279
|
initial_classifications: Dict[str, InitialClassifications] | None = None
|
253
|
-
initial_status: ValidStatusValues = ValidStatusValues.ACTIVE
|
280
|
+
initial_status: ValidStatusValues | str= ValidStatusValues.ACTIVE
|
254
281
|
parent_guid: str | None = None
|
255
282
|
parent_relationship_type_name: str | None = None
|
256
283
|
parent_at_end_1: bool | None = True
|
257
|
-
properties:
|
284
|
+
properties: dict | None = None
|
258
285
|
|
259
286
|
|
260
287
|
class NewClassificationRequestBody(RequestBody):
|
@@ -292,8 +319,8 @@ class UpdateElementRequestBody(PyegeriaModel):
|
|
292
319
|
class_: Annotated[Literal["UpdateElementRequestBody"], Field(alias="class")]
|
293
320
|
properties: dict[str, Any] = {}
|
294
321
|
merge_update: bool | None = True
|
295
|
-
external_source_guid: str = None
|
296
|
-
external_source_name: str = None
|
322
|
+
external_source_guid: str | None = None
|
323
|
+
external_source_name: str | None = None
|
297
324
|
effective_time: datetime | None = None
|
298
325
|
for_lineage: bool | None = False
|
299
326
|
for_duplicate_processing: bool | None = False
|
@@ -349,7 +376,23 @@ class SearchStringRequestBody(ResultsRequestBody):
|
|
349
376
|
|
350
377
|
|
351
378
|
#######
|
379
|
+
# This gets only the fields in the most specific model
|
380
|
+
def get_defined_fields(model):
|
381
|
+
return {
|
382
|
+
field_name: field
|
383
|
+
for field_name, field in model.__fields__.items()
|
384
|
+
if field_name in model.__annotations__ # Only fields defined in the current model
|
385
|
+
}
|
352
386
|
|
387
|
+
def get_defined_field_values(model_instance):
|
388
|
+
# Extract the subset of the model's fields
|
389
|
+
defined_fields = get_defined_fields(model_instance.__class__).keys()
|
390
|
+
# Return only the defined fields with their values
|
391
|
+
return {field: getattr(model_instance, field) for field in defined_fields}
|
392
|
+
|
393
|
+
|
394
|
+
|
395
|
+
#######
|
353
396
|
|
354
397
|
# --- Custom Base Model for JSON Key Conversion ---
|
355
398
|
|
pyegeria/output_formatter.py
CHANGED
@@ -8,6 +8,7 @@ from loguru import logger
|
|
8
8
|
|
9
9
|
from pyegeria.mermaid_utilities import construct_mermaid_web
|
10
10
|
from pyegeria._output_formats import select_output_format_set, MD_SEPARATOR
|
11
|
+
from pyegeria.models import to_camel_case
|
11
12
|
|
12
13
|
"""
|
13
14
|
Note on select_output_format_set function:
|
@@ -31,21 +32,21 @@ def _extract_referenceable_properties(element: dict[str, Any]) -> dict[str, Any]
|
|
31
32
|
version = element['elementHeader']["versions"].get("version", None)
|
32
33
|
type_name = element['elementHeader']["type"].get("typeName", None)
|
33
34
|
classifications = element['elementHeader'].get("classifications", [])
|
34
|
-
|
35
|
-
# Get attributes from properties
|
36
|
-
properties = element['properties']
|
37
|
-
display_name = properties.get("name", "") or ""
|
38
|
-
if display_name == "":
|
39
|
-
display_name = properties.get("displayName","")
|
40
|
-
description = properties.get("description", "") or ""
|
41
|
-
qualified_name = properties.get("qualifiedName", "") or ""
|
42
|
-
category = properties.get("category", "") or ""
|
43
|
-
version_identifier = properties.get("versionIdentifier", "") or ""
|
44
|
-
additional_properties = properties.get("additionalProperties", {}) or {}
|
45
|
-
extended_properties = properties.get("extendedProperties", {}) or {}
|
46
35
|
effective_from = element['elementHeader'].get("effectiveFrom", None)
|
47
36
|
effective_to = element['elementHeader'].get("effectiveTo", None)
|
48
37
|
|
38
|
+
# Get attributes from properties
|
39
|
+
# properties = element['properties']
|
40
|
+
# display_name = properties.get("name", "") or ""
|
41
|
+
# if display_name == "":
|
42
|
+
# display_name = properties.get("displayName","")
|
43
|
+
# description = properties.get("description", "") or ""
|
44
|
+
# qualified_name = properties.get("qualifiedName", "") or ""
|
45
|
+
# category = properties.get("category", "") or ""
|
46
|
+
# version_identifier = properties.get("versionIdentifier", "") or ""
|
47
|
+
# additional_properties = properties.get("additionalProperties", {}) or {}
|
48
|
+
# extended_properties = properties.get("extendedProperties", {}) or {}
|
49
|
+
#
|
49
50
|
return {
|
50
51
|
"GUID": guid,
|
51
52
|
"metadata_collection_id": metadata_collection_id,
|
@@ -58,13 +59,13 @@ def _extract_referenceable_properties(element: dict[str, Any]) -> dict[str, Any]
|
|
58
59
|
"type_name": type_name,
|
59
60
|
"classifications": classifications,
|
60
61
|
|
61
|
-
"display_name": display_name,
|
62
|
-
"description": description,
|
63
|
-
"qualified_name": qualified_name,
|
64
|
-
"category": category,
|
65
|
-
"version_identifier": version_identifier,
|
66
|
-
"additional_properties": additional_properties,
|
67
|
-
"extended_properties": extended_properties,
|
62
|
+
# "display_name": display_name,
|
63
|
+
# "description": description,
|
64
|
+
# "qualified_name": qualified_name,
|
65
|
+
# "category": category,
|
66
|
+
# "version_identifier": version_identifier,
|
67
|
+
# "additional_properties": additional_properties,
|
68
|
+
# "extended_properties": extended_properties,
|
68
69
|
"effective_from": effective_from,
|
69
70
|
"effective_to": effective_to,
|
70
71
|
}
|
@@ -228,7 +229,154 @@ def format_for_markdown_table(text: str, guid: str = None) -> str:
|
|
228
229
|
return t
|
229
230
|
|
230
231
|
|
231
|
-
def
|
232
|
+
def populate_columns_from_properties(element: dict, columns_struct: dict) -> dict:
|
233
|
+
"""
|
234
|
+
Populate a columns_struct with values from the element's properties.
|
235
|
+
|
236
|
+
The element dict is expected to have a nested 'properties' dict whose keys are in camelCase.
|
237
|
+
The columns_struct is expected to follow the format returned by select_output_format_set, where
|
238
|
+
columns are located at columns_struct['formats']['columns'] and each column is a dict containing
|
239
|
+
at least a 'key' field expressed in snake_case. For each column whose snake_case key corresponds
|
240
|
+
to a key in the element properties (after converting to camelCase), this function adds a 'value'
|
241
|
+
entry to the column with the matching property's value.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
element: The element containing a 'properties' dict with camelCase keys.
|
245
|
+
columns_struct: The columns structure whose columns have snake_case 'key' fields.
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
The updated columns_struct (the input structure is modified in place and also returned).
|
249
|
+
"""
|
250
|
+
if not isinstance(columns_struct, dict):
|
251
|
+
return columns_struct
|
252
|
+
|
253
|
+
props = (element or {}).get('properties') or {}
|
254
|
+
# If properties is not a dict, do nothing
|
255
|
+
if not isinstance(props, dict):
|
256
|
+
return columns_struct
|
257
|
+
|
258
|
+
# Get the columns list if present
|
259
|
+
formats = columns_struct.get('formats') or {}
|
260
|
+
columns = formats.get('columns') if isinstance(formats, dict) else None
|
261
|
+
if not isinstance(columns, list):
|
262
|
+
return columns_struct
|
263
|
+
|
264
|
+
for col in columns:
|
265
|
+
try:
|
266
|
+
key_snake = col.get('key') if isinstance(col, dict) else None
|
267
|
+
if not key_snake:
|
268
|
+
continue
|
269
|
+
# Convert the snake_case key to camelCase to look up in properties
|
270
|
+
key_camel = to_camel_case(key_snake)
|
271
|
+
if key_camel in props:
|
272
|
+
col['value'] = props.get(key_camel)
|
273
|
+
except Exception as e:
|
274
|
+
# Be resilient; log and continue
|
275
|
+
logger.debug(f"populate_columns_from_properties: skipping column due to error: {e}")
|
276
|
+
continue
|
277
|
+
|
278
|
+
return columns_struct
|
279
|
+
|
280
|
+
|
281
|
+
def get_required_relationships(element: dict, columns_struct: dict) -> dict:
|
282
|
+
"""
|
283
|
+
Populate relationship-derived column values in columns_struct based on top-level keys in the element.
|
284
|
+
|
285
|
+
This function inspects the requested columns in columns_struct, converts each column key from
|
286
|
+
snake_case to camelCase, and if a matching top-level key exists in the element, parses that value
|
287
|
+
(typically lists of relationship beans) into a human-readable value (e.g., a comma-separated list
|
288
|
+
of qualified names) and stores it under the column's 'value'. Columns not specified in the
|
289
|
+
columns_struct are ignored. Existing non-empty 'value's are left as-is.
|
290
|
+
|
291
|
+
Example: if a column with key 'member_of_collections' is present, this function will look for the
|
292
|
+
top-level key 'memberOfCollections' in the element and derive a value if found.
|
293
|
+
|
294
|
+
Args:
|
295
|
+
element: The element dictionary containing top-level relationship lists (e.g., associatedGlossaries,
|
296
|
+
memberOfCollections, collectionMembers).
|
297
|
+
columns_struct: The columns structure to augment with derived 'value's.
|
298
|
+
|
299
|
+
Returns:
|
300
|
+
The updated columns_struct (modified in place and returned).
|
301
|
+
"""
|
302
|
+
if not isinstance(columns_struct, dict):
|
303
|
+
return columns_struct
|
304
|
+
|
305
|
+
formats = columns_struct.get('formats') or {}
|
306
|
+
columns = formats.get('columns') if isinstance(formats, dict) else None
|
307
|
+
if not isinstance(columns, list):
|
308
|
+
return columns_struct
|
309
|
+
|
310
|
+
def _extract_name_from_item(item: Any) -> Optional[str]:
|
311
|
+
"""Best-effort extraction of a display/qualified name from a relationship item."""
|
312
|
+
try:
|
313
|
+
if isinstance(item, dict):
|
314
|
+
# Common pattern: item['relatedElement']['properties']['qualifiedName']
|
315
|
+
related = item.get('relatedElement') or item.get('related_element')
|
316
|
+
if isinstance(related, dict):
|
317
|
+
props = related.get('properties') or {}
|
318
|
+
name = (
|
319
|
+
props.get('qualifiedName')
|
320
|
+
or props.get('displayName')
|
321
|
+
or props.get('name')
|
322
|
+
)
|
323
|
+
if name:
|
324
|
+
return name
|
325
|
+
# Sometimes the properties may be at the top level of the item
|
326
|
+
name = (
|
327
|
+
item.get('qualifiedName')
|
328
|
+
or item.get('displayName')
|
329
|
+
or item.get('name')
|
330
|
+
)
|
331
|
+
if name:
|
332
|
+
return name
|
333
|
+
elif isinstance(item, str):
|
334
|
+
return item
|
335
|
+
except Exception as e:
|
336
|
+
logger.debug(f"get_required_relationships: error extracting name from item: {e}")
|
337
|
+
return None
|
338
|
+
|
339
|
+
for col in columns:
|
340
|
+
try:
|
341
|
+
if not isinstance(col, dict):
|
342
|
+
continue
|
343
|
+
key_snake = col.get('key')
|
344
|
+
if not key_snake:
|
345
|
+
continue
|
346
|
+
# If already has a non-empty value, don't overwrite
|
347
|
+
if col.get('value') not in (None, ""):
|
348
|
+
continue
|
349
|
+
|
350
|
+
# Convert the snake_case key to camelCase to look up in top-level element
|
351
|
+
key_camel = to_camel_case(key_snake)
|
352
|
+
if key_camel not in element:
|
353
|
+
continue
|
354
|
+
|
355
|
+
top_val = element.get(key_camel)
|
356
|
+
derived_value: str = ""
|
357
|
+
if isinstance(top_val, list):
|
358
|
+
names: List[str] = []
|
359
|
+
for item in top_val:
|
360
|
+
nm = _extract_name_from_item(item)
|
361
|
+
if nm:
|
362
|
+
names.append(nm)
|
363
|
+
derived_value = ", ".join(names)
|
364
|
+
elif isinstance(top_val, dict):
|
365
|
+
nm = _extract_name_from_item(top_val)
|
366
|
+
derived_value = nm or ""
|
367
|
+
else:
|
368
|
+
# Primitive or unexpected type; coerce to string if not None
|
369
|
+
derived_value = str(top_val) if top_val is not None else ""
|
370
|
+
|
371
|
+
col['value'] = derived_value
|
372
|
+
except Exception as e:
|
373
|
+
logger.debug(f"get_required_relationships: skipping column due to error: {e}")
|
374
|
+
continue
|
375
|
+
|
376
|
+
return columns_struct
|
377
|
+
|
378
|
+
|
379
|
+
def generate_entity_md(elements: List[Dict],
|
232
380
|
elements_action: str,
|
233
381
|
output_format: str,
|
234
382
|
entity_type: str,
|
@@ -250,70 +398,105 @@ def generate_entity_md(elements: List[Dict],
|
|
250
398
|
Returns:
|
251
399
|
str: Markdown representation
|
252
400
|
"""
|
253
|
-
|
254
|
-
|
401
|
+
heading = columns_struct.get("heading")
|
402
|
+
if heading == "Default Base Attributes":
|
403
|
+
elements_md = "## Reporting on Default Base Attributes - Perhaps couldn't find a valid combination of output_format_set and output_format?\n\n"
|
404
|
+
else:
|
405
|
+
elements_md = ""
|
406
|
+
base_columns = columns_struct['formats'].get('columns') if columns_struct else None
|
255
407
|
|
256
408
|
for element in elements:
|
257
409
|
if element is None:
|
258
|
-
|
259
|
-
|
410
|
+
continue
|
411
|
+
guid = element.get('elementHeader', {}).get('guid')
|
412
|
+
|
413
|
+
# Prefer new behavior: extractor returns an updated columns_struct with values
|
414
|
+
returned_struct = None
|
415
|
+
if columns_struct is not None:
|
416
|
+
try:
|
417
|
+
returned_struct = extract_properties_func(element, columns_struct)
|
418
|
+
except TypeError:
|
419
|
+
# Fallback for legacy extractors without columns_struct parameter
|
420
|
+
returned_struct = None
|
421
|
+
|
422
|
+
# Legacy fallback: get props dict if no columns_struct provided/returned
|
423
|
+
props = {}
|
424
|
+
if returned_struct is None:
|
425
|
+
props = extract_properties_func(element) if callable(extract_properties_func) else {}
|
260
426
|
|
261
427
|
# Get additional properties if function is provided
|
262
428
|
additional_props = {}
|
263
429
|
if get_additional_props_func:
|
264
|
-
|
430
|
+
# Use guid if available, else try to get from props
|
431
|
+
guid_for_fmt = guid or props.get('GUID')
|
432
|
+
additional_props = get_additional_props_func(element, guid_for_fmt, output_format)
|
433
|
+
|
434
|
+
# Determine display name
|
435
|
+
display_name = None
|
436
|
+
if returned_struct is not None:
|
437
|
+
cols = returned_struct.get('formats', {}).get('columns', [])
|
438
|
+
# Find value from 'display_name' or 'title'
|
439
|
+
for col in cols:
|
440
|
+
if col.get('key') in ('display_name', 'title'):
|
441
|
+
display_name = col.get('value')
|
442
|
+
if display_name:
|
443
|
+
break
|
444
|
+
else:
|
445
|
+
display_name = props.get('display_name') or props.get('title')
|
265
446
|
|
266
|
-
display_name = props.get('display_name', None)
|
267
447
|
if display_name is None:
|
268
|
-
display_name =
|
269
|
-
if display_name is None:
|
270
|
-
display_name = "NO DISPLAY NAME"
|
448
|
+
display_name = "NO DISPLAY NAME"
|
271
449
|
|
272
450
|
# Format header based on output format
|
273
451
|
if output_format in ['FORM', 'MD']:
|
274
452
|
elements_md += f"# {elements_action}\n\n"
|
275
453
|
elements_md += f"## {entity_type} Name \n\n{display_name}\n\n"
|
276
454
|
elif output_format == 'REPORT':
|
277
|
-
elements_md += f'<a id="{props.get("GUID"
|
455
|
+
elements_md += f'<a id="{(guid or props.get("GUID") or "No GUID" )}"></a>\n# {entity_type} Name: {display_name}\n\n'
|
278
456
|
else:
|
279
457
|
elements_md += f"## {entity_type} Name \n\n{display_name}\n\n"
|
280
458
|
|
281
|
-
|
282
|
-
if
|
283
|
-
|
459
|
+
# Add attributes based on column spec if available, otherwise, add all (legacy)
|
460
|
+
if returned_struct is not None:
|
461
|
+
cols = returned_struct.get('formats', {}).get('columns', [])
|
462
|
+
for column in cols:
|
463
|
+
name = column.get('name')
|
464
|
+
key = column.get('key')
|
465
|
+
value = column.get('value')
|
466
|
+
if value in (None, "") and key in additional_props:
|
467
|
+
value = additional_props[key]
|
468
|
+
if column.get('format'):
|
469
|
+
value = format_for_markdown_table(value, guid)
|
470
|
+
elements_md += make_md_attribute(name, value, output_format)
|
471
|
+
if wk := returned_struct.get("annotations", {}).get("wikilinks"):
|
472
|
+
elements_md += ", ".join(wk)
|
473
|
+
elif base_columns:
|
474
|
+
# If we have columns but extractor didn't return struct, use legacy props lookup
|
475
|
+
for column in base_columns:
|
284
476
|
key = column['key']
|
285
477
|
name = column['name']
|
286
478
|
value = ""
|
287
|
-
|
288
|
-
# Check if the key is in props or additional_props
|
289
479
|
if key in props:
|
290
480
|
value = props[key]
|
291
481
|
elif key in additional_props:
|
292
482
|
value = additional_props[key]
|
293
|
-
|
294
|
-
|
295
|
-
value = format_for_markdown_table(value, props['GUID'])
|
296
|
-
# elements_md += make_md_attribute(key.replace('_', ' '), value, output_format)
|
483
|
+
if column.get('format'):
|
484
|
+
value = format_for_markdown_table(value, guid or props.get('GUID'))
|
297
485
|
elements_md += make_md_attribute(name, value, output_format)
|
298
|
-
|
486
|
+
if wk := columns_struct.get("annotations", {}).get("wikilinks", None):
|
487
|
+
elements_md += ", ".join(wk)
|
299
488
|
else:
|
489
|
+
# Legacy path without columns: dump all props
|
300
490
|
for key, value in props.items():
|
301
491
|
if output_format in ['FORM', 'MD', 'DICT'] and key == 'mermaid':
|
302
492
|
continue
|
303
|
-
if key not in [
|
304
|
-
if key ==
|
493
|
+
if key not in ['properties', 'display_name']:
|
494
|
+
if key == 'mermaid' and value == '':
|
305
495
|
continue
|
306
496
|
elements_md += make_md_attribute(key.replace('_', ' '), value, output_format)
|
307
|
-
# Add additional properties
|
308
497
|
for key, value in additional_props.items():
|
309
498
|
elements_md += make_md_attribute(key.replace('_', ' '), value, output_format)
|
310
499
|
|
311
|
-
# # Add GUID
|
312
|
-
# elements_md += make_md_attribute("GUID",props['GUID'], output_format)
|
313
|
-
|
314
|
-
if wk := columns_struct.get("annotations", {}).get("wikilinks", None):
|
315
|
-
elements_md += ", ".join(wk)
|
316
|
-
# Add separator if not the last element
|
317
500
|
if element != elements[-1]:
|
318
501
|
elements_md += MD_SEPARATOR
|
319
502
|
|
@@ -344,8 +527,12 @@ def generate_entity_md_table(elements: List[Dict],
|
|
344
527
|
# Handle pluralization - if entity_type ends with 'y', use 'ies' instead of 's'
|
345
528
|
entity_type_plural = f"{entity_type[:-1]}ies" if entity_type.endswith('y') else f"{entity_type}s"
|
346
529
|
columns = columns_struct['formats'].get('columns', [])
|
530
|
+
heading = columns_struct.get("heading")
|
531
|
+
if heading == "Default Base Attributes":
|
532
|
+
elements_md = "## Reporting on Default Base Attributes - Perhaps couldn't find a valid combination of output_format_set and output_format?\n\n"
|
533
|
+
else:
|
534
|
+
elements_md = ""
|
347
535
|
|
348
|
-
elements_md = ""
|
349
536
|
if output_format == "LIST":
|
350
537
|
elements_md = f"# {entity_type_plural} Table\n\n"
|
351
538
|
elements_md += f"{entity_type_plural} found from the search string: `{search_string}`\n\n"
|
@@ -362,33 +549,47 @@ def generate_entity_md_table(elements: List[Dict],
|
|
362
549
|
|
363
550
|
# Add rows
|
364
551
|
for element in elements:
|
552
|
+
guid = element.get('elementHeader', {}).get('guid')
|
553
|
+
|
554
|
+
# Extractor returns columns_struct with values when possible
|
555
|
+
try:
|
556
|
+
returned_struct = extract_properties_func(element, columns_struct)
|
557
|
+
except TypeError:
|
558
|
+
returned_struct = None
|
559
|
+
|
560
|
+
# For help mode, bypass extraction
|
365
561
|
if output_format == "help":
|
366
|
-
|
367
|
-
else:
|
368
|
-
props = extract_properties_func(element)
|
562
|
+
returned_struct = {"formats": {"columns": columns}}
|
369
563
|
|
370
|
-
#
|
564
|
+
# Additional props (if any)
|
371
565
|
additional_props = {}
|
372
566
|
if get_additional_props_func:
|
373
|
-
additional_props = get_additional_props_func(element,
|
567
|
+
additional_props = get_additional_props_func(element, guid, output_format)
|
374
568
|
|
375
569
|
# Build row
|
376
570
|
row = "| "
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
value
|
386
|
-
|
387
|
-
#
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
571
|
+
if returned_struct is not None:
|
572
|
+
for column in returned_struct.get('formats', {}).get('columns', []):
|
573
|
+
key = column.get('key')
|
574
|
+
value = column.get('value')
|
575
|
+
if (value in (None, "")) and key in additional_props:
|
576
|
+
value = additional_props[key]
|
577
|
+
if column.get('format'):
|
578
|
+
value = format_for_markdown_table(value, guid)
|
579
|
+
row += f"{value} | "
|
580
|
+
else:
|
581
|
+
# Legacy fallback: read from props dict
|
582
|
+
props = extract_properties_func(element)
|
583
|
+
for column in columns:
|
584
|
+
key = column['key']
|
585
|
+
value = ""
|
586
|
+
if key in props:
|
587
|
+
value = props[key]
|
588
|
+
elif key in additional_props:
|
589
|
+
value = additional_props[key]
|
590
|
+
if column.get('format'):
|
591
|
+
value = format_for_markdown_table(value, guid or props.get('GUID'))
|
592
|
+
row += f"{value} | "
|
392
593
|
|
393
594
|
elements_md += row + "\n"
|
394
595
|
if wk := columns_struct.get("annotations",{}).get("wikilinks", None):
|
@@ -419,38 +620,55 @@ def generate_entity_dict(elements: List[Dict],
|
|
419
620
|
"""
|
420
621
|
result = []
|
421
622
|
|
422
|
-
#####
|
623
|
+
#####
|
423
624
|
# Add attributes based on column spec if available, otherwise, add all
|
424
625
|
for element in elements:
|
425
626
|
if element is None:
|
426
627
|
continue
|
427
|
-
|
628
|
+
|
629
|
+
guid = element.get('elementHeader', {}).get('guid')
|
630
|
+
|
631
|
+
returned_struct = None
|
632
|
+
if columns_struct is not None:
|
633
|
+
try:
|
634
|
+
returned_struct = extract_properties_func(element, columns_struct)
|
635
|
+
except TypeError:
|
636
|
+
returned_struct = None
|
637
|
+
|
428
638
|
# Get additional properties if function is provided
|
429
639
|
additional_props = {}
|
430
640
|
if get_additional_props_func:
|
431
|
-
additional_props = get_additional_props_func(element,
|
641
|
+
additional_props = get_additional_props_func(element, guid, output_format)
|
432
642
|
|
433
643
|
# Create entity dictionary
|
434
644
|
entity_dict = {}
|
435
645
|
|
436
646
|
columns = columns_struct['formats'].get('columns', None) if columns_struct else None
|
437
|
-
if
|
647
|
+
if returned_struct is not None:
|
648
|
+
for column in returned_struct.get('formats', {}).get('columns', []):
|
649
|
+
key = column.get('key')
|
650
|
+
name = column.get('name')
|
651
|
+
value = column.get('value')
|
652
|
+
if (value in (None, "")) and key in additional_props:
|
653
|
+
value = additional_props[key]
|
654
|
+
if column.get('format'):
|
655
|
+
value = format_for_markdown_table(value, guid)
|
656
|
+
entity_dict[name] = value
|
657
|
+
elif columns:
|
438
658
|
for column in columns:
|
439
659
|
key = column['key']
|
440
660
|
name = column['name']
|
441
661
|
value = ""
|
442
|
-
|
443
|
-
# Check if the key is in props or additional_props
|
662
|
+
props = extract_properties_func(element)
|
444
663
|
if key in props:
|
445
664
|
value = props[key]
|
446
665
|
elif key in additional_props:
|
447
666
|
value = additional_props[key]
|
448
|
-
# Format the value if needed
|
449
667
|
if column.get('format', None):
|
450
|
-
value = format_for_markdown_table(value, props
|
668
|
+
value = format_for_markdown_table(value, guid or props.get('GUID'))
|
451
669
|
entity_dict[name] = value
|
452
|
-
|
453
670
|
else:
|
671
|
+
props = extract_properties_func(element)
|
454
672
|
# Add properties based on include/exclude lists
|
455
673
|
for key, value in props.items():
|
456
674
|
if key not in ['properties', 'mermaid']: # Skip the raw properties object
|
@@ -583,6 +801,7 @@ def generate_output(elements: Union[Dict, List[Dict]],
|
|
583
801
|
Formatted output as string or list of dictionaries
|
584
802
|
"""
|
585
803
|
columns = columns_struct['formats'].get('columns',None) if columns_struct else None
|
804
|
+
|
586
805
|
# Ensure elements is a list
|
587
806
|
if isinstance(elements, dict):
|
588
807
|
elements = [elements]
|
pyegeria/utils.py
CHANGED
@@ -140,7 +140,7 @@ def body_slimmer(body: dict) -> dict:
|
|
140
140
|
|
141
141
|
slimmed = {}
|
142
142
|
for key, value in body.items():
|
143
|
-
if value:
|
143
|
+
if value and not isinstance(value, tuple):
|
144
144
|
if isinstance(value, dict):
|
145
145
|
# Recursively slim embedded dictionaries
|
146
146
|
slimmed_value = body_slimmer(value)
|