pyegeria 5.3.9.9.3__py3-none-any.whl → 5.5.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyegeria might be problematic. Click here for more details.
- commands/__init__.py +24 -0
- commands/cat/Dr-Egeria_md-orig.py +2 -2
- commands/cat/__init__.py +1 -17
- commands/cat/collection_actions.py +197 -0
- commands/cat/dr_egeria_command_help.py +372 -0
- commands/cat/dr_egeria_jupyter.py +7 -7
- commands/cat/dr_egeria_md.py +27 -182
- commands/cat/exp_list_glossaries.py +11 -14
- commands/cat/get_asset_graph.py +37 -267
- commands/cat/{get_collection.py → get_collection_tree.py} +10 -18
- commands/cat/get_project_dependencies.py +14 -14
- commands/cat/get_project_structure.py +15 -14
- commands/cat/get_tech_type_elements.py +16 -116
- commands/cat/glossary_actions.py +145 -298
- commands/cat/list_assets.py +3 -11
- commands/cat/list_cert_types.py +17 -63
- commands/cat/list_collections.py +46 -138
- commands/cat/list_deployed_catalogs.py +15 -27
- commands/cat/list_deployed_database_schemas.py +27 -43
- commands/cat/list_deployed_databases.py +16 -31
- commands/cat/list_deployed_servers.py +35 -54
- commands/cat/list_glossaries.py +18 -17
- commands/cat/list_projects.py +10 -12
- commands/cat/list_tech_type_elements.py +21 -37
- commands/cat/list_tech_types.py +13 -25
- commands/cat/list_terms.py +38 -79
- commands/cat/list_todos.py +4 -11
- commands/cat/list_user_ids.py +3 -10
- commands/cat/my_reports.py +559 -0
- commands/cat/run_report.py +394 -0
- commands/cat/run_report_orig.py +528 -0
- commands/cli/egeria.py +222 -247
- commands/cli/egeria_cat.py +68 -81
- commands/cli/egeria_my.py +13 -0
- commands/cli/egeria_ops.py +69 -74
- commands/cli/egeria_tech.py +17 -93
- commands/cli/ops_config.py +3 -6
- commands/{cat/list_categories.py → deprecated/list_data_designer.py} +53 -64
- commands/{cat/list_data_structures.py → deprecated/list_data_structures_full.py} +3 -6
- commands/deprecated/old_get_asset_graph.py +315 -0
- commands/my/__init__.py +0 -2
- commands/my/list_my_profile.py +27 -34
- commands/my/list_my_roles.py +1 -7
- commands/my/monitor_my_todos.py +1 -7
- commands/my/monitor_open_todos.py +6 -7
- commands/my/todo_actions.py +4 -5
- commands/ops/__init__.py +0 -2
- commands/ops/gov_server_actions.py +17 -21
- commands/ops/list_archives.py +17 -38
- commands/ops/list_catalog_targets.py +33 -40
- commands/ops/load_archive.py +35 -26
- commands/ops/{monitor_engine_activity_c.py → monitor_active_engine_activity.py} +51 -82
- commands/ops/{monitor_integ_daemon_status.py → monitor_daemon_status.py} +35 -55
- commands/ops/monitor_engine_activity.py +79 -77
- commands/ops/{monitor_gov_eng_status.py → monitor_engine_status.py} +10 -7
- commands/ops/monitor_platform_status.py +38 -50
- commands/ops/monitor_server_startup.py +6 -11
- commands/ops/monitor_server_status.py +7 -11
- commands/ops/orig_monitor_server_list.py +8 -8
- commands/ops/orig_monitor_server_status.py +1 -5
- commands/ops/refresh_integration_daemon.py +5 -5
- commands/ops/restart_integration_daemon.py +5 -5
- commands/ops/table_integ_daemon_status.py +6 -6
- commands/ops/x_engine_actions.py +7 -7
- commands/tech/__init__.py +0 -2
- commands/tech/{generic_actions.py → element_actions.py} +6 -11
- commands/tech/get_element_info.py +20 -29
- commands/tech/get_guid_info.py +23 -42
- commands/tech/get_tech_details.py +20 -35
- commands/tech/get_tech_type_template.py +28 -39
- commands/tech/list_all_om_type_elements.py +24 -30
- commands/tech/list_all_om_type_elements_x.py +22 -28
- commands/tech/list_all_related_elements.py +19 -28
- commands/tech/list_anchored_elements.py +22 -30
- commands/tech/list_asset_types.py +19 -24
- commands/tech/list_elements_by_classification_by_property_value.py +26 -32
- commands/tech/list_elements_by_property_value.py +19 -25
- commands/tech/list_elements_by_property_value_x.py +20 -28
- commands/tech/list_elements_for_classification.py +28 -41
- commands/tech/list_gov_action_processes.py +16 -27
- commands/tech/list_information_supply_chains.py +22 -30
- commands/tech/list_registered_services.py +14 -26
- commands/tech/list_related_elements_with_prop_value.py +15 -25
- commands/tech/list_related_specification.py +1 -4
- commands/tech/list_relationship_types.py +15 -25
- commands/tech/list_relationships.py +20 -36
- commands/tech/list_solution_blueprints.py +28 -33
- commands/tech/list_solution_components.py +23 -29
- commands/tech/list_solution_roles.py +21 -32
- commands/tech/list_tech_templates.py +51 -54
- commands/tech/list_valid_metadata_values.py +5 -9
- commands/tech/table_tech_templates.py +2 -6
- commands/tech/x_list_related_elements.py +1 -4
- examples/GeoSpatial Products Example.py +524 -0
- examples/Jupyter Notebooks/P-egeria-server-config.ipynb +2137 -0
- examples/Jupyter Notebooks/README.md +2 -0
- examples/Jupyter Notebooks/common/P-environment-check.ipynb +115 -0
- examples/Jupyter Notebooks/common/__init__.py +14 -0
- examples/Jupyter Notebooks/common/common-functions.ipynb +4694 -0
- examples/Jupyter Notebooks/common/environment-check.ipynb +52 -0
- examples/Jupyter Notebooks/common/globals.ipynb +184 -0
- examples/Jupyter Notebooks/common/globals.py +154 -0
- examples/Jupyter Notebooks/common/orig_globals.py +152 -0
- examples/format_sets/all_format_sets.json +910 -0
- examples/format_sets/custom_format_sets.json +268 -0
- examples/format_sets/subset_format_sets.json +187 -0
- examples/format_sets_save_load_example.py +291 -0
- examples/jacquard_data_sets.py +129 -0
- examples/output_formats_example.py +193 -0
- examples/test_jacquard_data_sets.py +54 -0
- examples/test_jacquard_data_sets_scenarios.py +94 -0
- md_processing/__init__.py +90 -0
- md_processing/command_dispatcher.py +33 -0
- md_processing/command_mapping.py +221 -0
- md_processing/data/commands/commands_data_designer.json +537 -0
- md_processing/data/commands/commands_external_reference.json +733 -0
- md_processing/data/commands/commands_feedback.json +155 -0
- md_processing/data/commands/commands_general.json +204 -0
- md_processing/data/commands/commands_glossary.json +218 -0
- md_processing/data/commands/commands_governance.json +3678 -0
- md_processing/data/commands/commands_product_manager.json +865 -0
- md_processing/data/commands/commands_project.json +642 -0
- md_processing/data/commands/commands_solution_architect.json +366 -0
- md_processing/data/commands.json +17568 -0
- md_processing/data/commands_working.json +30641 -0
- md_processing/data/gened_report_specs.py +6584 -0
- md_processing/data/generated_format_sets.json +6533 -0
- md_processing/data/generated_format_sets_old.json +4137 -0
- md_processing/data/generated_format_sets_old.py +45 -0
- md_processing/dr_egeria.py +182 -0
- md_processing/md_commands/__init__.py +3 -0
- md_processing/md_commands/data_designer_commands.py +1276 -0
- md_processing/md_commands/ext_ref_commands.py +530 -0
- md_processing/md_commands/feedback_commands.py +726 -0
- md_processing/md_commands/glossary_commands.py +684 -0
- md_processing/md_commands/governance_officer_commands.py +600 -0
- md_processing/md_commands/product_manager_commands.py +1266 -0
- md_processing/md_commands/project_commands.py +383 -0
- md_processing/md_commands/solution_architect_commands.py +1184 -0
- md_processing/md_commands/view_commands.py +295 -0
- md_processing/md_processing_utils/__init__.py +4 -0
- md_processing/md_processing_utils/common_md_proc_utils.py +1249 -0
- md_processing/md_processing_utils/common_md_utils.py +578 -0
- md_processing/md_processing_utils/determine_width.py +103 -0
- md_processing/md_processing_utils/extraction_utils.py +547 -0
- md_processing/md_processing_utils/gen_report_specs.py +643 -0
- md_processing/md_processing_utils/generate_dr_help.py +193 -0
- md_processing/md_processing_utils/generate_md_cmd_templates.py +144 -0
- md_processing/md_processing_utils/generate_md_templates.py +83 -0
- md_processing/md_processing_utils/md_processing_constants.py +1228 -0
- md_processing/md_processing_utils/message_constants.py +19 -0
- pyegeria/__init__.py +201 -443
- pyegeria/core/__init__.py +40 -0
- pyegeria/core/_base_platform_client.py +574 -0
- pyegeria/core/_base_server_client.py +573 -0
- pyegeria/core/_exceptions.py +457 -0
- pyegeria/core/_globals.py +60 -0
- pyegeria/core/_server_client.py +6073 -0
- pyegeria/core/_validators.py +257 -0
- pyegeria/core/config.py +654 -0
- pyegeria/{create_tech_guid_lists.py → core/create_tech_guid_lists.py} +0 -1
- pyegeria/core/load_config.py +37 -0
- pyegeria/core/logging_configuration.py +207 -0
- pyegeria/core/mcp_adapter.py +144 -0
- pyegeria/core/mcp_server.py +212 -0
- pyegeria/core/utils.py +405 -0
- pyegeria/deprecated/__init__.py +0 -0
- pyegeria/{_client.py → deprecated/_client.py} +62 -24
- pyegeria/{_deprecated_gov_engine.py → deprecated/_deprecated_gov_engine.py} +16 -16
- pyegeria/{classification_manager_omvs.py → deprecated/classification_manager_omvs.py} +1988 -1878
- pyegeria/deprecated/output_formatter_with_machine_keys.py +1127 -0
- pyegeria/{runtime_manager_omvs.py → deprecated/runtime_manager_omvs.py} +216 -229
- pyegeria/{valid_metadata_omvs.py → deprecated/valid_metadata_omvs.py} +93 -93
- pyegeria/{x_action_author_omvs.py → deprecated/x_action_author_omvs.py} +2 -3
- pyegeria/egeria_cat_client.py +25 -51
- pyegeria/egeria_client.py +140 -98
- pyegeria/egeria_config_client.py +48 -24
- pyegeria/egeria_tech_client.py +170 -83
- pyegeria/models/__init__.py +150 -0
- pyegeria/models/collection_models.py +168 -0
- pyegeria/models/models.py +654 -0
- pyegeria/omvs/__init__.py +84 -0
- pyegeria/omvs/action_author.py +342 -0
- pyegeria/omvs/actor_manager.py +5980 -0
- pyegeria/omvs/asset_catalog.py +842 -0
- pyegeria/omvs/asset_maker.py +2736 -0
- pyegeria/omvs/automated_curation.py +4403 -0
- pyegeria/omvs/classification_manager.py +11213 -0
- pyegeria/omvs/collection_manager.py +5780 -0
- pyegeria/omvs/community_matters_omvs.py +468 -0
- pyegeria/{core_omag_server_config.py → omvs/core_omag_server_config.py} +157 -157
- pyegeria/{data_designer_omvs.py → omvs/data_designer.py} +1991 -1691
- pyegeria/omvs/data_discovery.py +869 -0
- pyegeria/omvs/data_engineer.py +372 -0
- pyegeria/omvs/digital_business.py +1133 -0
- pyegeria/omvs/external_links.py +1752 -0
- pyegeria/omvs/feedback_manager.py +834 -0
- pyegeria/{full_omag_server_config.py → omvs/full_omag_server_config.py} +73 -69
- pyegeria/omvs/glossary_manager.py +3231 -0
- pyegeria/omvs/governance_officer.py +3009 -0
- pyegeria/omvs/lineage_linker.py +314 -0
- pyegeria/omvs/location_arena.py +1525 -0
- pyegeria/omvs/metadata_expert.py +668 -0
- pyegeria/omvs/metadata_explorer_omvs.py +2943 -0
- pyegeria/omvs/my_profile.py +1042 -0
- pyegeria/omvs/notification_manager.py +358 -0
- pyegeria/omvs/people_organizer.py +394 -0
- pyegeria/{platform_services.py → omvs/platform_services.py} +113 -193
- pyegeria/omvs/product_manager.py +1825 -0
- pyegeria/omvs/project_manager.py +1907 -0
- pyegeria/omvs/reference_data.py +1140 -0
- pyegeria/omvs/registered_info.py +334 -0
- pyegeria/omvs/runtime_manager.py +2817 -0
- pyegeria/omvs/schema_maker.py +446 -0
- pyegeria/{server_operations.py → omvs/server_operations.py} +27 -26
- pyegeria/omvs/solution_architect.py +6490 -0
- pyegeria/omvs/specification_properties.py +37 -0
- pyegeria/omvs/subject_area.py +1042 -0
- pyegeria/omvs/template_manager_omvs.py +236 -0
- pyegeria/omvs/time_keeper.py +1761 -0
- pyegeria/omvs/valid_metadata.py +3221 -0
- pyegeria/omvs/valid_metadata_lists.py +37 -0
- pyegeria/omvs/valid_type_lists.py +37 -0
- pyegeria/view/__init__.py +28 -0
- pyegeria/view/_output_format_models.py +514 -0
- pyegeria/view/_output_formats.py +14 -0
- pyegeria/view/base_report_formats.py +2719 -0
- pyegeria/view/dr_egeria_reports.py +56 -0
- pyegeria/view/format_set_executor.py +397 -0
- pyegeria/{md_processing_utils.py → view/md_processing_utils.py} +5 -5
- pyegeria/{mermaid_utilities.py → view/mermaid_utilities.py} +2 -154
- pyegeria/view/output_formatter.py +1297 -0
- pyegeria-5.5.3.3.dist-info/METADATA +218 -0
- pyegeria-5.5.3.3.dist-info/RECORD +241 -0
- {pyegeria-5.3.9.9.3.dist-info → pyegeria-5.5.3.3.dist-info}/WHEEL +2 -1
- pyegeria-5.5.3.3.dist-info/entry_points.txt +103 -0
- pyegeria-5.5.3.3.dist-info/top_level.txt +4 -0
- commands/cat/.DS_Store +0 -0
- commands/cat/README.md +0 -16
- commands/cli/txt_custom_v2.tcss +0 -19
- commands/my/README.md +0 -17
- commands/ops/README.md +0 -24
- commands/ops/monitor_asset_events.py +0 -108
- commands/tech/README.md +0 -24
- pyegeria/.DS_Store +0 -0
- pyegeria/README.md +0 -35
- pyegeria/_globals.py +0 -47
- pyegeria/_validators.py +0 -385
- pyegeria/asset_catalog_omvs.py +0 -864
- pyegeria/automated_curation_omvs.py +0 -3765
- pyegeria/collection_manager_omvs.py +0 -2744
- pyegeria/dr.egeria spec.md +0 -9
- pyegeria/egeria_my_client.py +0 -56
- pyegeria/feedback_manager_omvs.py +0 -4573
- pyegeria/glossary_browser_omvs.py +0 -3728
- pyegeria/glossary_manager_omvs.py +0 -2440
- pyegeria/m_test.py +0 -118
- pyegeria/md_processing_helpers.py +0 -58
- pyegeria/md_processing_utils_orig.py +0 -1103
- pyegeria/metadata_explorer_omvs.py +0 -2326
- pyegeria/my_profile_omvs.py +0 -1022
- pyegeria/output_formatter.py +0 -389
- pyegeria/project_manager_omvs.py +0 -1933
- pyegeria/registered_info.py +0 -167
- pyegeria/solution_architect_omvs.py +0 -2156
- pyegeria/template_manager_omvs.py +0 -1414
- pyegeria/utils.py +0 -197
- pyegeria-5.3.9.9.3.dist-info/METADATA +0 -72
- pyegeria-5.3.9.9.3.dist-info/RECORD +0 -143
- pyegeria-5.3.9.9.3.dist-info/entry_points.txt +0 -99
- /pyegeria/{_exceptions.py → deprecated/_exceptions.py} +0 -0
- {pyegeria-5.3.9.9.3.dist-info → pyegeria-5.5.3.3.dist-info/licenses}/LICENSE +0 -0
pyegeria/core/utils.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
Copyright Contributors to the ODPi Egeria project.
|
|
4
|
+
|
|
5
|
+
General utility functions in support of the Egeria Python Client package.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from rich import print, print_json
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from pyegeria.core.config import settings as app_settings
|
|
13
|
+
from typing import Callable, TypeVar
|
|
14
|
+
|
|
15
|
+
T = TypeVar('T', bound=Callable)
|
|
16
|
+
|
|
17
|
+
console = Console(width=200)
|
|
18
|
+
|
|
19
|
+
def init_log():
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def print_rest_request_body(body):
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
body:
|
|
27
|
+
"""
|
|
28
|
+
pretty_body = json.dumps(body, indent=4)
|
|
29
|
+
print_json(pretty_body, indent=4, sort_keys=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def print_rest_response(response):
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
response:
|
|
37
|
+
"""
|
|
38
|
+
print("Returns:")
|
|
39
|
+
pretty_body = json.dumps(response, indent=4)
|
|
40
|
+
print_json(pretty_body, indent=4, sort_keys=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def print_guid_list(guids):
|
|
44
|
+
"""Print a list of guids"""
|
|
45
|
+
if guids is None:
|
|
46
|
+
print("No assets created")
|
|
47
|
+
else:
|
|
48
|
+
pretty_guids = json.dumps(guids, indent=4)
|
|
49
|
+
print_json(pretty_guids, indent=4, sort_keys=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
# OCF Common services
|
|
54
|
+
# Working with assets - this set of functions displays assets returned from the open metadata repositories.
|
|
55
|
+
#
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def body_slimmer(body: dict) -> dict:
|
|
61
|
+
"""body_slimmer is a little function to remove unused keys from a dict
|
|
62
|
+
and recursively slim embedded dicts
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
body : the dictionary that you want to slim
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
dict:
|
|
71
|
+
a slimmed body with all embedded dictionaries also slimmed
|
|
72
|
+
"""
|
|
73
|
+
if body is None:
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
slimmed = {}
|
|
77
|
+
for key, value in body.items():
|
|
78
|
+
if value and not isinstance(value, tuple):
|
|
79
|
+
if isinstance(value, dict):
|
|
80
|
+
# Recursively slim embedded dictionaries
|
|
81
|
+
slimmed_value = body_slimmer(value)
|
|
82
|
+
if slimmed_value: # Only include non-empty dictionaries
|
|
83
|
+
slimmed[key] = slimmed_value
|
|
84
|
+
else:
|
|
85
|
+
slimmed[key] = value
|
|
86
|
+
return slimmed
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def camel_to_title_case(input_string):
|
|
90
|
+
# Add a space before uppercase letters and capitalize each word
|
|
91
|
+
result = re.sub(r'([a-z])([A-Z])', r'\1 \2', input_string).title()
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def to_camel_case(input_string):
|
|
96
|
+
"""Convert an input string to camelCase, singularizing if plural.
|
|
97
|
+
|
|
98
|
+
This function takes an input string, converts it to singular form if it's plural,
|
|
99
|
+
and then transforms it to camelCase format (first word lowercase, subsequent words
|
|
100
|
+
capitalized with no spaces).
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
input_string : str
|
|
105
|
+
The string to convert to camelCase
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
str:
|
|
110
|
+
The input string converted to camelCase, after singularization if needed
|
|
111
|
+
|
|
112
|
+
Examples
|
|
113
|
+
--------
|
|
114
|
+
>>> to_camel_case("data categories")
|
|
115
|
+
'dataCategory'
|
|
116
|
+
>>> to_camel_case("business terms")
|
|
117
|
+
'businessTerm'
|
|
118
|
+
>>> to_camel_case("glossary categories")
|
|
119
|
+
'glossaryCategory'
|
|
120
|
+
"""
|
|
121
|
+
if not input_string:
|
|
122
|
+
return ""
|
|
123
|
+
|
|
124
|
+
# Convert to lowercase for consistent processing
|
|
125
|
+
lowercase_input = input_string.lower()
|
|
126
|
+
|
|
127
|
+
# First, convert to singular if plural
|
|
128
|
+
singular = lowercase_input
|
|
129
|
+
|
|
130
|
+
# Handle common plural endings
|
|
131
|
+
if singular.endswith('ies'):
|
|
132
|
+
singular = singular[:-3] + 'y'
|
|
133
|
+
elif singular.endswith('es'):
|
|
134
|
+
# Special cases like 'classes' -> 'class'
|
|
135
|
+
if singular.endswith('sses') or singular.endswith('ches') or singular.endswith('shes') or singular.endswith('xes'):
|
|
136
|
+
singular = singular[:-2]
|
|
137
|
+
else:
|
|
138
|
+
singular = singular[:-1]
|
|
139
|
+
elif singular.endswith('s') and not singular.endswith('ss'):
|
|
140
|
+
singular = singular[:-1]
|
|
141
|
+
|
|
142
|
+
# Split the string into words and convert to camelCase
|
|
143
|
+
words = singular.split()
|
|
144
|
+
if not words:
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
# First word is lowercase, rest are capitalized
|
|
148
|
+
result = words[0]
|
|
149
|
+
for word in words[1:]:
|
|
150
|
+
result += word.capitalize()
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
def to_pascal_case(input_string)->str:
|
|
155
|
+
"""
|
|
156
|
+
Convert input string to PascalCase, singularizing if plural.
|
|
157
|
+
Args:
|
|
158
|
+
input_string ():
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
transformed string
|
|
162
|
+
"""
|
|
163
|
+
result = to_camel_case(input_string)
|
|
164
|
+
output_string = result[0].upper() + result[1:]
|
|
165
|
+
return output_string
|
|
166
|
+
|
|
167
|
+
def flatten_dict_to_string(d: dict) -> str:
|
|
168
|
+
"""Flatten a dictionary into a string and replace quotes with backticks."""
|
|
169
|
+
try:
|
|
170
|
+
flat_string = ", ".join(
|
|
171
|
+
# Change replace(\"'\", '`') to replace("'", '`')
|
|
172
|
+
f"{key}=`{str(value).replace('\"', '`').replace("'", '`')}`"
|
|
173
|
+
for key, value in d.items()
|
|
174
|
+
)
|
|
175
|
+
return flat_string
|
|
176
|
+
except Exception as e:
|
|
177
|
+
# Corrected syntax for exception chaining
|
|
178
|
+
raise Exception("Error flattening dictionary") from e
|
|
179
|
+
# The decorator logic, which applies @logger.catch dynamically
|
|
180
|
+
|
|
181
|
+
def dict_to_markdown_list(data: dict, level: int = 0) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Recursively converts a dictionary into a nested markdown bullet list string.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
data (dict): The dictionary to convert.
|
|
187
|
+
level (int): The current indentation level (default is 0).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
str: The markdown formatted string.
|
|
191
|
+
"""
|
|
192
|
+
markdown_str = ""
|
|
193
|
+
# Standard markdown indent is often 2 or 4 spaces. Using 2 for compactness in recursion.
|
|
194
|
+
indent = " " * level
|
|
195
|
+
|
|
196
|
+
for key, value in data.items():
|
|
197
|
+
if isinstance(value, dict):
|
|
198
|
+
markdown_str += f"{indent}* **{key}**:\n{dict_to_markdown_list(value, level + 1)}"
|
|
199
|
+
elif isinstance(value, list):
|
|
200
|
+
markdown_str += f"{indent}* **{key}**:\n"
|
|
201
|
+
for item in value:
|
|
202
|
+
if isinstance(item, dict):
|
|
203
|
+
markdown_str += f"{indent} * \n{dict_to_markdown_list(item, level + 2)}"
|
|
204
|
+
else:
|
|
205
|
+
markdown_str += f"{indent} * {item}\n"
|
|
206
|
+
else:
|
|
207
|
+
markdown_str += f"{indent}* **{key}**: {value}\n"
|
|
208
|
+
|
|
209
|
+
return markdown_str
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
import csv
|
|
213
|
+
import io
|
|
214
|
+
from typing import Dict, Any
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def transform_json_to_tabular(json_data: Dict[str, Any], output_format: str = 'rich'):
|
|
218
|
+
"""
|
|
219
|
+
Transforms Egeria TabularDataSetReportResponse JSON to CSV, Rich table, or Markdown table (LIST).
|
|
220
|
+
|
|
221
|
+
:param json_data: The JSON data as a dictionary.
|
|
222
|
+
:param output_format: 'CSV', 'RICH-TABLE', or 'LIST'.
|
|
223
|
+
"""
|
|
224
|
+
report = json_data.json().get('tabularDataSetReport')
|
|
225
|
+
if not report:
|
|
226
|
+
print("No tabularDataSetReport found in JSON.")
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
column_descriptions = report.get('columnDescriptions', [])
|
|
230
|
+
headers = [col.get('columnName') for col in column_descriptions]
|
|
231
|
+
data_records = report.get('dataRecords', {})
|
|
232
|
+
|
|
233
|
+
# Sort keys to ensure correct order if they are numeric strings
|
|
234
|
+
sorted_record_keys = sorted(data_records.keys(), key=lambda x: int(x))
|
|
235
|
+
rows = [data_records[key] for key in sorted_record_keys]
|
|
236
|
+
|
|
237
|
+
if output_format == 'CSV':
|
|
238
|
+
output = io.StringIO()
|
|
239
|
+
writer = csv.writer(output)
|
|
240
|
+
writer.writerow(headers)
|
|
241
|
+
writer.writerows(rows)
|
|
242
|
+
print(output.getvalue())
|
|
243
|
+
elif output_format == 'RICH-TABLE':
|
|
244
|
+
try:
|
|
245
|
+
from rich.console import Console
|
|
246
|
+
from rich.table import Table
|
|
247
|
+
|
|
248
|
+
console = Console()
|
|
249
|
+
table = Table(title=report.get('tableName', 'Report'))
|
|
250
|
+
|
|
251
|
+
for header in headers:
|
|
252
|
+
table.add_column(header)
|
|
253
|
+
|
|
254
|
+
for row in rows:
|
|
255
|
+
# Convert all items to string for Rich
|
|
256
|
+
str_row = [str(item) if item is not None else "" for item in row]
|
|
257
|
+
table.add_row(*str_row)
|
|
258
|
+
|
|
259
|
+
console.print(table)
|
|
260
|
+
except ImportError:
|
|
261
|
+
print("Rich library not installed. Defaulting to simple text output.")
|
|
262
|
+
print(f"Headers: {headers}")
|
|
263
|
+
for row in rows:
|
|
264
|
+
print(row)
|
|
265
|
+
elif output_format == 'LIST':
|
|
266
|
+
# Generate a markdown table
|
|
267
|
+
markdown_output = f"### {report.get('tableName', 'Report')}\n\n"
|
|
268
|
+
markdown_output += "| " + " | ".join(headers) + " |\n"
|
|
269
|
+
markdown_output += "| " + " | ".join(["---"] * len(headers)) + " |\n"
|
|
270
|
+
for row in rows:
|
|
271
|
+
str_row = [str(item) if item is not None else "" for item in row]
|
|
272
|
+
markdown_output += "| " + " | ".join(str_row) + " |\n"
|
|
273
|
+
print(markdown_output)
|
|
274
|
+
else:
|
|
275
|
+
print(f"Unknown output format: {output_format}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# Example usage:
|
|
279
|
+
# with open('path/to/your/file.json', 'r') as f:
|
|
280
|
+
# data = json.load(f)
|
|
281
|
+
# transform_json_to_tabular(data, 'CSV') # or 'RICH-TABLE' or 'LIST'
|
|
282
|
+
|
|
283
|
+
import json
|
|
284
|
+
import re
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# def parse_to_dict(input_str: str):
|
|
288
|
+
# """
|
|
289
|
+
# Check if a string is valid JSON or a name:value list without braces and convert to a dictionary.
|
|
290
|
+
#
|
|
291
|
+
# Args:
|
|
292
|
+
# input_str: The input string to parse.
|
|
293
|
+
#
|
|
294
|
+
# Returns:
|
|
295
|
+
# dict: A dictionary converted from the input string.
|
|
296
|
+
# None: If the input is neither valid JSON nor a valid name:value list.
|
|
297
|
+
# """
|
|
298
|
+
#
|
|
299
|
+
# if input_str is None:
|
|
300
|
+
# return None
|
|
301
|
+
#
|
|
302
|
+
# # Check if the input string is valid JSON
|
|
303
|
+
# try:
|
|
304
|
+
# result = json.loads(input_str)
|
|
305
|
+
# if isinstance(result, dict): # Ensure it's a dictionary
|
|
306
|
+
# return result
|
|
307
|
+
# except json.JSONDecodeError:
|
|
308
|
+
# pass
|
|
309
|
+
#
|
|
310
|
+
# # Check if input string looks like a name:value list
|
|
311
|
+
# # Supports both comma and newline as separators
|
|
312
|
+
# pattern = r'^(\s*("[^"]+"|\'[^\']+\'|[a-zA-Z0-9_-]+)\s*:\s*("[^"]+"|\'[^\']+\'|[a-zA-Z0-9 _-]*)\s*)' \
|
|
313
|
+
# r'(\s*[,|\n]\s*("[^"]+"|\'[^\']+\'|[a-zA-Z0-9_-]+)\s*:\s*("[^"]+"|\'[^\']+\'|[a-zA-Z0-9 _-]*)\s*)*$'
|
|
314
|
+
# if re.match(pattern, input_str.strip()):
|
|
315
|
+
# try:
|
|
316
|
+
# # Split by ',' or '\n' and process key-value pairs
|
|
317
|
+
# pairs = [pair.split(":", 1) for pair in re.split(r'[,|\n]+', input_str.strip())]
|
|
318
|
+
# return {key.strip().strip('\'"'): value.strip().strip('\'"') for key, value in pairs}
|
|
319
|
+
# except Exception:
|
|
320
|
+
# return None
|
|
321
|
+
#
|
|
322
|
+
# # If neither pattern matches, return None
|
|
323
|
+
# return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def parse_to_dict(input_str: str) -> dict | None:
|
|
327
|
+
"""
|
|
328
|
+
Parse input strings into a dictionary, handling both JSON and key-value pairs.
|
|
329
|
+
Recovers from malformed JSON (e.g., where commas are missing between key-value pairs)
|
|
330
|
+
and supports multiline values.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
input_str (str): The input string to parse.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
dict: A parsed dictionary if validation is successful, or None if the string cannot be parsed.
|
|
337
|
+
"""
|
|
338
|
+
if not input_str:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
# Attempt to parse valid JSON
|
|
342
|
+
try:
|
|
343
|
+
result = json.loads(input_str)
|
|
344
|
+
if isinstance(result, dict):
|
|
345
|
+
return result
|
|
346
|
+
except json.JSONDecodeError:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
# Fix malformed JSON or attempt alternate parsing for "key: value" patterns
|
|
350
|
+
try:
|
|
351
|
+
# Step 1: Inject missing commas where they are omitted between key-value pairs
|
|
352
|
+
fixed_input = re.sub(
|
|
353
|
+
r'("\s*:[^,}\n]+)\s*("(?![:,}\n]))', # Find missing commas (key-value-value sequences)
|
|
354
|
+
r'\1,\2', # Add a comma between the values
|
|
355
|
+
input_str
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Attempt to parse the fixed string as JSON
|
|
359
|
+
try:
|
|
360
|
+
result = json.loads(fixed_input)
|
|
361
|
+
if isinstance(result, dict):
|
|
362
|
+
return result
|
|
363
|
+
except json.JSONDecodeError:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
# Step 2: Handle key-value format fallback (supports multiline strings)
|
|
367
|
+
# Matches `key: value` pairs, including multiline quoted values
|
|
368
|
+
key_value_pattern = re.compile(r'''
|
|
369
|
+
(?:"([^"]+)"|'([^']+)'|([a-zA-Z0-9_-]+)) # Key: quoted "key", 'key', or unquoted key
|
|
370
|
+
\s*:\s* # Key-value separator
|
|
371
|
+
(?:"((?:\\.|[^"\\])*?)"|'((?:\\.|[^'\\])*?)'|([^\n,]+)) # Value: quoted or unquoted
|
|
372
|
+
''', re.VERBOSE | re.DOTALL)
|
|
373
|
+
|
|
374
|
+
matches = key_value_pattern.findall(input_str)
|
|
375
|
+
|
|
376
|
+
# Build dictionary from matches
|
|
377
|
+
result_dict = {}
|
|
378
|
+
for match in matches:
|
|
379
|
+
key = next((group for group in match[:3] if group), "").strip()
|
|
380
|
+
value = next((group for group in match[3:] if group), "").strip()
|
|
381
|
+
result_dict[key] = value
|
|
382
|
+
|
|
383
|
+
if result_dict:
|
|
384
|
+
return result_dict
|
|
385
|
+
except Exception as e:
|
|
386
|
+
# Log or handle parsing exception if needed
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
# If all parsing attempts fail, return None
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def dynamic_catch(func: T) -> T:
|
|
394
|
+
if app_settings.get("enable_logger_catchh", False):
|
|
395
|
+
return logger.catch(func) # Apply the logger.catch decorator
|
|
396
|
+
else:
|
|
397
|
+
return func # Return the function unwrapped
|
|
398
|
+
|
|
399
|
+
def make_format_set_name_from_type(obj_type: str)-> str:
|
|
400
|
+
formatted_name = obj_type.replace(" ", "-")
|
|
401
|
+
return f"{formatted_name}-DrE"
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
if __name__ == "__main__":
|
|
405
|
+
print("Main-Utils")
|
|
File without changes
|
|
@@ -11,28 +11,26 @@ import asyncio
|
|
|
11
11
|
import inspect
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
14
|
-
import re
|
|
15
14
|
from datetime import datetime
|
|
16
15
|
|
|
17
16
|
import httpx
|
|
18
17
|
from httpx import AsyncClient, Response
|
|
19
18
|
|
|
20
|
-
from pyegeria.utils import body_slimmer
|
|
21
|
-
from pyegeria._exceptions import (
|
|
19
|
+
from pyegeria.core.utils import body_slimmer
|
|
20
|
+
from pyegeria.core._exceptions import (
|
|
22
21
|
InvalidParameterException,
|
|
23
22
|
OMAGCommonErrorCode,
|
|
24
23
|
PropertyServerException,
|
|
25
24
|
UserNotAuthorizedException,
|
|
26
25
|
)
|
|
27
|
-
from pyegeria._globals import enable_ssl_check, max_paging_size, NO_ELEMENTS_FOUND
|
|
28
|
-
from pyegeria._validators import (
|
|
26
|
+
from pyegeria.core._globals import enable_ssl_check, max_paging_size, NO_ELEMENTS_FOUND
|
|
27
|
+
from pyegeria.core._validators import (
|
|
29
28
|
is_json,
|
|
30
29
|
validate_name,
|
|
31
30
|
validate_server_name,
|
|
32
31
|
validate_url,
|
|
33
32
|
validate_user_id,
|
|
34
33
|
)
|
|
35
|
-
from pyegeria.output_formatter import make_preamble, make_md_attribute
|
|
36
34
|
|
|
37
35
|
...
|
|
38
36
|
|
|
@@ -181,9 +179,9 @@ class Client:
|
|
|
181
179
|
|
|
182
180
|
Raises
|
|
183
181
|
------
|
|
184
|
-
|
|
182
|
+
PyegeriaInvalidParameterException
|
|
185
183
|
If the client passes incorrect parameters on the request - such as bad URLs or invalid values
|
|
186
|
-
|
|
184
|
+
PyegeriaAPIException
|
|
187
185
|
Raised by the server when an issue arises in processing a valid request
|
|
188
186
|
NotAuthorizedException
|
|
189
187
|
The principle specified by the user_id does not have authorization for the requested action
|
|
@@ -237,9 +235,9 @@ class Client:
|
|
|
237
235
|
|
|
238
236
|
Raises
|
|
239
237
|
------
|
|
240
|
-
|
|
238
|
+
PyegeriaInvalidParameterException
|
|
241
239
|
If the client passes incorrect parameters on the request - such as bad URLs or invalid values
|
|
242
|
-
|
|
240
|
+
PyegeriaAPIException
|
|
243
241
|
Raised by the server when an issue arises in processing a valid request
|
|
244
242
|
NotAuthorizedException
|
|
245
243
|
The principle specified by the user_id does not have authorization for the requested action
|
|
@@ -263,7 +261,7 @@ class Client:
|
|
|
263
261
|
This method is used to refresh the bearer token used for authentication with Egeria. It checks if the token
|
|
264
262
|
source is 'Egeria', and if the user ID and password are valid. If all conditions are met, it calls the
|
|
265
263
|
`create_egeria_bearer_token` method to create a new bearer token. Otherwise,
|
|
266
|
-
it raises an `
|
|
264
|
+
it raises an `PyegeriaInvalidParameterException`.
|
|
267
265
|
|
|
268
266
|
Parameters:
|
|
269
267
|
|
|
@@ -271,7 +269,7 @@ class Client:
|
|
|
271
269
|
None
|
|
272
270
|
|
|
273
271
|
Raises:
|
|
274
|
-
|
|
272
|
+
PyegeriaInvalidParameterException: If the token source is invalid.
|
|
275
273
|
"""
|
|
276
274
|
if (
|
|
277
275
|
(self.token_src == "Egeria")
|
|
@@ -292,7 +290,7 @@ class Client:
|
|
|
292
290
|
This method is used to refresh the bearer token used for authentication with Egeria. It checks if the token
|
|
293
291
|
source is 'Egeria', and if the user ID and password are valid. If all conditions are met, it calls the
|
|
294
292
|
`create_egeria_bearer_token` method to create a new bearer token. Otherwise,
|
|
295
|
-
it raises an `
|
|
293
|
+
it raises an `PyegeriaInvalidParameterException`.
|
|
296
294
|
|
|
297
295
|
Parameters:
|
|
298
296
|
|
|
@@ -300,8 +298,8 @@ class Client:
|
|
|
300
298
|
None
|
|
301
299
|
|
|
302
300
|
Raises:
|
|
303
|
-
|
|
304
|
-
|
|
301
|
+
PyegeriaInvalidParameterException: If the token source is invalid.
|
|
302
|
+
PyegeriaAPIException
|
|
305
303
|
Raised by the server when an issue arises in processing a valid request
|
|
306
304
|
NotAuthorizedException
|
|
307
305
|
The principle specified by the user_id does not have authorization for the requested action
|
|
@@ -324,9 +322,9 @@ class Client:
|
|
|
324
322
|
|
|
325
323
|
Raises
|
|
326
324
|
------
|
|
327
|
-
|
|
325
|
+
PyegeriaInvalidParameterException
|
|
328
326
|
If the client passes incorrect parameters on the request - such as bad URLs or invalid values
|
|
329
|
-
|
|
327
|
+
PyegeriaAPIException
|
|
330
328
|
Raised by the server when an issue arises in processing a valid request
|
|
331
329
|
NotAuthorizedException
|
|
332
330
|
The principle specified by the user_id does not have authorization for the requested action
|
|
@@ -366,7 +364,7 @@ class Client:
|
|
|
366
364
|
) -> Response | str:
|
|
367
365
|
"""Make a request to the Egeria API - Async Version
|
|
368
366
|
Function to make an API call via the self.session Library. Raise an exception if the HTTP response code
|
|
369
|
-
is not 200/201. IF there is a REST communication exception, raise
|
|
367
|
+
is not 200/201. IF there is a REST communication exception, raise PyegeriaInvalidParameterException.
|
|
370
368
|
|
|
371
369
|
:param request_type: Type of Request.
|
|
372
370
|
Supported Values - GET, POST, (not PUT, PATCH, DELETE).
|
|
@@ -444,7 +442,7 @@ class Client:
|
|
|
444
442
|
{
|
|
445
443
|
"class": "VoidResponse",
|
|
446
444
|
"relatedHTTPCode": response.status_code,
|
|
447
|
-
"exceptionClassName": "
|
|
445
|
+
"exceptionClassName": "PyegeriaInvalidParameterException",
|
|
448
446
|
"actionDescription": caller_method,
|
|
449
447
|
"exceptionErrorMessage": msg,
|
|
450
448
|
"exceptionErrorMessageId": OMAGCommonErrorCode.CLIENT_SIDE_REST_API_ERROR.value[
|
|
@@ -487,7 +485,7 @@ class Client:
|
|
|
487
485
|
{
|
|
488
486
|
"class": "VoidResponse",
|
|
489
487
|
"relatedHTTPCode": response.status_code,
|
|
490
|
-
"exceptionClassName": "
|
|
488
|
+
"exceptionClassName": "PyegeriaInvalidParameterException",
|
|
491
489
|
"actionDescription": caller_method,
|
|
492
490
|
"exceptionErrorMessage": msg,
|
|
493
491
|
"exceptionErrorMessageId": OMAGCommonErrorCode.CLIENT_SIDE_REST_API_ERROR.value[
|
|
@@ -537,7 +535,7 @@ class Client:
|
|
|
537
535
|
{
|
|
538
536
|
"class": "VoidResponse",
|
|
539
537
|
"relatedHTTPCode": response.status_code,
|
|
540
|
-
"exceptionClassName": "
|
|
538
|
+
"exceptionClassName": "PyegeriaAPIException",
|
|
541
539
|
"actionDescription": caller_method,
|
|
542
540
|
"exceptionErrorMessage": msg,
|
|
543
541
|
"exceptionErrorMessageId": OMAGCommonErrorCode.CLIENT_SIDE_REST_API_ERROR.value[
|
|
@@ -596,7 +594,7 @@ class Client:
|
|
|
596
594
|
{
|
|
597
595
|
"class": "VoidResponse",
|
|
598
596
|
"relatedHTTPCode": 400,
|
|
599
|
-
"exceptionClassName": "
|
|
597
|
+
"exceptionClassName": "PyegeriaInvalidParameterException",
|
|
600
598
|
"actionDescription": caller_method,
|
|
601
599
|
"exceptionErrorMessage": msg,
|
|
602
600
|
"exceptionErrorMessageId": OMAGCommonErrorCode.CLIENT_SIDE_REST_API_ERROR.value[
|
|
@@ -771,13 +769,14 @@ class Client:
|
|
|
771
769
|
)
|
|
772
770
|
return result
|
|
773
771
|
|
|
774
|
-
def __create_qualified_name__(self,
|
|
772
|
+
def __create_qualified_name__(self, type_name: str, display_name: str, local_qualifier: str = None,
|
|
775
773
|
version_identifier: str = None) -> str:
|
|
776
774
|
"""Helper function to create a qualified name for a given type and display name.
|
|
777
775
|
If present, the local qualifier will be prepended to the qualified name."""
|
|
778
776
|
EGERIA_LOCAL_QUALIFIER = os.environ.get("EGERIA_LOCAL_QUALIFIER", local_qualifier)
|
|
779
777
|
# display_name = re.sub(r'\s','-',display_name.strip()) # This changes spaces between words to -; removing
|
|
780
|
-
|
|
778
|
+
if display_name is None:
|
|
779
|
+
raise InvalidParameterException("display_name is required")
|
|
781
780
|
q_name = f"{type}::{display_name.strip()}"
|
|
782
781
|
if EGERIA_LOCAL_QUALIFIER:
|
|
783
782
|
q_name = f"{EGERIA_LOCAL_QUALIFIER}::{q_name}"
|
|
@@ -786,6 +785,45 @@ class Client:
|
|
|
786
785
|
return q_name
|
|
787
786
|
|
|
788
787
|
|
|
788
|
+
async def _async_get_element_by_guid_(self, element_guid: str) -> dict | str:
|
|
789
|
+
"""
|
|
790
|
+
Simplified, internal version of get_element_by_guid found in Classification Manager.
|
|
791
|
+
Retrieve an element by its guid. Async version.
|
|
792
|
+
|
|
793
|
+
Parameters
|
|
794
|
+
----------
|
|
795
|
+
element_guid: str
|
|
796
|
+
- unique identifier for the element
|
|
797
|
+
|
|
798
|
+
Returns
|
|
799
|
+
-------
|
|
800
|
+
dict | str
|
|
801
|
+
Returns a string if no element found; otherwise a dict of the element.
|
|
802
|
+
|
|
803
|
+
Raises
|
|
804
|
+
------
|
|
805
|
+
PyegeriaInvalidParameterException
|
|
806
|
+
one of the parameters is null or invalid or
|
|
807
|
+
PyegeriaAPIException
|
|
808
|
+
There is a problem adding the element properties to the metadata repository or
|
|
809
|
+
PyegeriaUnauthorizedException
|
|
810
|
+
the requesting user is not authorized to issue this request.
|
|
811
|
+
"""
|
|
812
|
+
|
|
813
|
+
body = {
|
|
814
|
+
"class": "EffectiveTimeQueryRequestBody",
|
|
815
|
+
"effectiveTime": None,
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
url = (f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/classification-manager/elements/"
|
|
819
|
+
f"{element_guid}?forLineage=false&forDuplicateProcessing=false")
|
|
820
|
+
|
|
821
|
+
response: Response = await self._async_make_request("POST", url, body_slimmer(body))
|
|
822
|
+
|
|
823
|
+
elements = response.json().get("element", NO_ELEMENTS_FOUND)
|
|
824
|
+
|
|
825
|
+
return elements
|
|
826
|
+
|
|
789
827
|
|
|
790
828
|
if __name__ == "__main__":
|
|
791
829
|
print("Main-__client")
|