osbot-utils 1.92.0__py3-none-any.whl → 1.94.0__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.
- osbot_utils/base_classes/Type_Safe.py +28 -7
- osbot_utils/helpers/Safe_Id.py +3 -1
- osbot_utils/utils/Json.py +1 -0
- osbot_utils/utils/Objects.py +0 -2
- osbot_utils/utils/Str.py +5 -5
- osbot_utils/version +1 -1
- {osbot_utils-1.92.0.dist-info → osbot_utils-1.94.0.dist-info}/METADATA +2 -2
- {osbot_utils-1.92.0.dist-info → osbot_utils-1.94.0.dist-info}/RECORD +10 -33
- osbot_utils/graphs/__init__.py +0 -0
- osbot_utils/graphs/mermaid/Mermaid.py +0 -75
- osbot_utils/graphs/mermaid/Mermaid__Edge.py +0 -49
- osbot_utils/graphs/mermaid/Mermaid__Graph.py +0 -95
- osbot_utils/graphs/mermaid/Mermaid__Node.py +0 -69
- osbot_utils/graphs/mermaid/Mermaid__Renderer.py +0 -56
- osbot_utils/graphs/mermaid/configs/Mermaid__Edge__Config.py +0 -7
- osbot_utils/graphs/mermaid/configs/Mermaid__Node__Config.py +0 -9
- osbot_utils/graphs/mermaid/configs/Mermaid__Render__Config.py +0 -7
- osbot_utils/graphs/mermaid/examples/Mermaid_Examples__FlowChart.py +0 -98
- osbot_utils/graphs/mermaid/models/Mermaid__Diagram_Direction.py +0 -9
- osbot_utils/graphs/mermaid/models/Mermaid__Diagram__Type.py +0 -16
- osbot_utils/graphs/mermaid/models/Mermaid__Node__Shape.py +0 -30
- osbot_utils/graphs/mgraph/MGraph.py +0 -55
- osbot_utils/graphs/mgraph/MGraph__Config.py +0 -7
- osbot_utils/graphs/mgraph/MGraph__Data.py +0 -139
- osbot_utils/graphs/mgraph/MGraph__Edge.py +0 -24
- osbot_utils/graphs/mgraph/MGraph__Node.py +0 -32
- osbot_utils/graphs/mgraph/MGraph__Random_Graphs.py +0 -27
- osbot_utils/graphs/mgraph/MGraph__Serializer.py +0 -42
- osbot_utils/graphs/mgraph/MGraphs.py +0 -12
- osbot_utils/graphs/mgraph/__init__.py +0 -0
- osbot_utils/helpers/trace/Trace_Call__Graph.py +0 -26
- {osbot_utils-1.92.0.dist-info → osbot_utils-1.94.0.dist-info}/LICENSE +0 -0
- {osbot_utils-1.92.0.dist-info → osbot_utils-1.94.0.dist-info}/WHEEL +0 -0
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
import sys
|
5
5
|
import types
|
6
|
-
from osbot_utils.utils.Objects
|
6
|
+
from osbot_utils.utils.Objects import default_value # todo: remove test mocking requirement for this to be here (instead of on the respective method)
|
7
7
|
|
8
8
|
# Backport implementations of get_origin and get_args for Python 3.7
|
9
9
|
if sys.version_info < (3, 8): # pragma: no cover
|
@@ -95,8 +95,12 @@ class Type_Safe:
|
|
95
95
|
if value is not None:
|
96
96
|
if type(value) is dict:
|
97
97
|
value = convert_dict_to_value_from_obj_annotation(self, name, value)
|
98
|
-
|
98
|
+
elif type(value) in [int, str]: # for now only a small number of str and int classes are supported (until we understand the full implications of this)
|
99
99
|
value = convert_to_value_from_obj_annotation (self, name, value)
|
100
|
+
else:
|
101
|
+
origin = get_origin(value)
|
102
|
+
if origin is not None:
|
103
|
+
value = origin
|
100
104
|
check_1 = value_type_matches_obj_annotation_for_attr (self, name, value)
|
101
105
|
check_2 = value_type_matches_obj_annotation_for_union_and_annotated(self, name, value)
|
102
106
|
if (check_1 is False and check_2 is None or
|
@@ -169,7 +173,7 @@ class Type_Safe:
|
|
169
173
|
#todo: fix type safety bug that I believe is caused here
|
170
174
|
if obj_is_type_union_compatible(var_type, IMMUTABLE_TYPES) is False: # if var_type is not something like Optional[Union[int, str]]
|
171
175
|
if type(var_type) not in IMMUTABLE_TYPES:
|
172
|
-
exception_message = f"variable '{var_name}' is defined as type '{var_type}' which is not supported by
|
176
|
+
exception_message = f"variable '{var_name}' is defined as type '{var_type}' which is not supported by Type_Safe, with only the following immutable types being supported: '{IMMUTABLE_TYPES}'"
|
173
177
|
raise ValueError(exception_message)
|
174
178
|
if include_base_classes is False:
|
175
179
|
break
|
@@ -304,6 +308,19 @@ class Type_Safe:
|
|
304
308
|
setattr(self, key, value)
|
305
309
|
return self
|
306
310
|
|
311
|
+
def deserialize_type__using_value(self, value):
|
312
|
+
if value:
|
313
|
+
try:
|
314
|
+
module_name, type_name = value.rsplit('.', 1)
|
315
|
+
if module_name == 'builtins' and type_name == 'NoneType': # Special case for NoneType (which serialises as builtins.* , but it actually in types.* )
|
316
|
+
value = types.NoneType
|
317
|
+
else:
|
318
|
+
module = __import__(module_name, fromlist=[type_name])
|
319
|
+
value = getattr(module, type_name)
|
320
|
+
except (ValueError, ImportError, AttributeError) as e:
|
321
|
+
raise ValueError(f"Could not reconstruct type from '{value}': {str(e)}")
|
322
|
+
return value
|
323
|
+
|
307
324
|
def deserialize_dict__using_key_value_annotations(self, key, value):
|
308
325
|
from osbot_utils.base_classes.Type_Safe__Dict import Type_Safe__Dict
|
309
326
|
|
@@ -353,7 +370,9 @@ class Type_Safe:
|
|
353
370
|
raise ValueError(f"Attribute '{key}' not found in '{self.__class__.__name__}'")
|
354
371
|
else:
|
355
372
|
continue
|
356
|
-
if
|
373
|
+
if obj_attribute_annotation(self, key) == type: # Handle type objects
|
374
|
+
value = self.deserialize_type__using_value(value)
|
375
|
+
elif obj_is_attribute_annotation_of_type(self, key, dict): # handle the case when the value is a dict
|
357
376
|
value = self.deserialize_dict__using_key_value_annotations(key, value)
|
358
377
|
elif obj_is_attribute_annotation_of_type(self, key, list): # handle the case when the value is a list
|
359
378
|
attribute_annotation = obj_attribute_annotation(self, key) # get the annotation for this variable
|
@@ -423,15 +442,17 @@ def serialize_to_dict(obj):
|
|
423
442
|
return obj
|
424
443
|
elif isinstance(obj, Enum):
|
425
444
|
return obj.name
|
445
|
+
elif isinstance(obj, type):
|
446
|
+
return f"{obj.__module__}.{obj.__name__}" # save the full type name
|
426
447
|
elif isinstance(obj, list) or isinstance(obj, List):
|
427
448
|
return [serialize_to_dict(item) for item in obj]
|
428
449
|
elif isinstance(obj, dict):
|
429
450
|
return {key: serialize_to_dict(value) for key, value in obj.items()}
|
430
451
|
elif hasattr(obj, "__dict__"):
|
431
|
-
data = {}
|
452
|
+
data = {} # todo: look at a more advanced version which saved the type of the object, for example with {'__type__': type(obj).__name__}
|
432
453
|
for key, value in obj.__dict__.items():
|
433
|
-
if key.startswith('__') is False:
|
434
|
-
data[key] = serialize_to_dict(value)
|
454
|
+
if key.startswith('__') is False: # don't process internal variables (for example the ones set by @cache_on_self)
|
455
|
+
data[key] = serialize_to_dict(value) # Recursive call for complex types
|
435
456
|
return data
|
436
457
|
else:
|
437
458
|
raise TypeError(f"Type {type(obj)} not serializable")
|
osbot_utils/helpers/Safe_Id.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
from osbot_utils.utils.Misc import random_id_short
|
2
2
|
from osbot_utils.utils.Str import safe_id
|
3
3
|
|
4
|
+
SAFE_ID__MAX_LENGTH = 512
|
5
|
+
|
4
6
|
class Safe_Id(str):
|
5
|
-
def __new__(cls, value=None, max_length=
|
7
|
+
def __new__(cls, value=None, max_length=SAFE_ID__MAX_LENGTH):
|
6
8
|
if value is None:
|
7
9
|
value = safe_id(random_id_short('safe-id'))
|
8
10
|
sanitized_value = safe_id(value, max_length=max_length)
|
osbot_utils/utils/Json.py
CHANGED
@@ -193,6 +193,7 @@ json_save_file_gz = Json.save_file_gz
|
|
193
193
|
json_save_file_pretty_gz = Json.save_file_pretty_gz
|
194
194
|
json_save_tmp_file = Json.json_save_tmp_file
|
195
195
|
str_to_json = Json.loads
|
196
|
+
str_from_json = json_dumps
|
196
197
|
|
197
198
|
load_file_json = json_load_file
|
198
199
|
load_file_json_gz = json_load_file_gz
|
osbot_utils/utils/Objects.py
CHANGED
@@ -359,8 +359,6 @@ def obj_attribute_annotation(target, attr_name):
|
|
359
359
|
|
360
360
|
def obj_is_attribute_annotation_of_type(target, attr_name, expected_type):
|
361
361
|
attribute_annotation = obj_attribute_annotation(target, attr_name)
|
362
|
-
#attribute_type = type(attribute_annotation)
|
363
|
-
#return attribute_type is expected_type
|
364
362
|
if expected_type is attribute_annotation:
|
365
363
|
return True
|
366
364
|
if expected_type is type(attribute_annotation):
|
osbot_utils/utils/Str.py
CHANGED
@@ -9,17 +9,17 @@ REGEX__ANSI_ESCAPE_PATTERN = re.compile(r'\x1b\[[0-9;]*m')
|
|
9
9
|
REGEX__SAFE_ID_REGEX = re.compile(r'[^a-zA-Z0-9_-]')
|
10
10
|
|
11
11
|
def ansi_text_visible_length(ansi_text):
|
12
|
-
if
|
12
|
+
if isinstance(ansi_text, str):
|
13
13
|
ansi_escape = re.compile(REGEX__ANSI_ESCAPE_PATTERN) # This regex matches the escape sequences used for text formatting
|
14
14
|
visible_text = ansi_escape.sub('', ansi_text) # Remove the escape sequences
|
15
15
|
return len(visible_text) # Return the length of the remaining text
|
16
16
|
|
17
17
|
def ansi_to_text(ansi_text: str):
|
18
|
-
if
|
18
|
+
if isinstance(ansi_text, str):
|
19
19
|
return REGEX__ANSI_ESCAPE_PATTERN.sub('', ansi_text)
|
20
20
|
|
21
21
|
def ansis_to_texts(ansis_texts: list): # todo: find a better name for this method :)
|
22
|
-
if
|
22
|
+
if isinstance(ansis_texts, list):
|
23
23
|
return [ansi_to_text(ansi_text) for ansi_text in ansis_texts]
|
24
24
|
return []
|
25
25
|
|
@@ -42,7 +42,7 @@ def safe_id(value, max_length=36):
|
|
42
42
|
value = str(value)
|
43
43
|
|
44
44
|
if len(value) > max_length:
|
45
|
-
raise ValueError(f"Invalid ID: The ID must not exceed
|
45
|
+
raise ValueError(f"Invalid ID: The ID must not exceed {max_length} characters (was {len(value)}).")
|
46
46
|
|
47
47
|
sanitized_value = REGEX__SAFE_ID_REGEX.sub('_', value)
|
48
48
|
|
@@ -98,7 +98,7 @@ def str_cap_snake_case(snake_str):
|
|
98
98
|
|
99
99
|
|
100
100
|
def trim(target):
|
101
|
-
if
|
101
|
+
if isinstance(target, str):
|
102
102
|
return target.strip()
|
103
103
|
return ""
|
104
104
|
|
osbot_utils/version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
v1.
|
1
|
+
v1.94.0
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: osbot_utils
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.94.0
|
4
4
|
Summary: OWASP Security Bot - Utils
|
5
5
|
Home-page: https://github.com/owasp-sbot/OSBot-Utils
|
6
6
|
License: MIT
|
@@ -23,7 +23,7 @@ Description-Content-Type: text/markdown
|
|
23
23
|
|
24
24
|
Powerful Python util methods and classes that simplify common apis and tasks.
|
25
25
|
|
26
|
-

|
27
27
|
[](https://codecov.io/gh/owasp-sbot/OSBot-Utils)
|
28
28
|
|
29
29
|
|
@@ -2,7 +2,7 @@ osbot_utils/__init__.py,sha256=DdJDmQc9zbQUlPVyTJOww6Ixrn9n4bD3ami5ItQfzJI,16
|
|
2
2
|
osbot_utils/base_classes/Cache_Pickle.py,sha256=kPCwrgUbf_dEdxUz7vW1GuvIPwlNXxuRhb-H3AbSpII,5884
|
3
3
|
osbot_utils/base_classes/Kwargs_To_Disk.py,sha256=HHoy05NC_w35WcT-OnSKoSIV_cLqaU9rdjH0_KNTM0E,1096
|
4
4
|
osbot_utils/base_classes/Kwargs_To_Self.py,sha256=weFNsBfBNV9W_qBkN-IdBD4yYcJV_zgTxBRO-ZlcPS4,141
|
5
|
-
osbot_utils/base_classes/Type_Safe.py,sha256=
|
5
|
+
osbot_utils/base_classes/Type_Safe.py,sha256=XzJd1tOEFFT67ycg5QaiXv5pFeWD8Fq4YDVQGGVYAjc,28990
|
6
6
|
osbot_utils/base_classes/Type_Safe__Base.py,sha256=CFPYe8_i5vvTLyc7s8CXbY4n_dY6sqVfBY8w9Vo77ZA,5468
|
7
7
|
osbot_utils/base_classes/Type_Safe__Dict.py,sha256=sfZcukhXUd9TS0PQpAk-gGLfZUJSC6BtMh6jF4Fn8Jw,1107
|
8
8
|
osbot_utils/base_classes/Type_Safe__List.py,sha256=pXDzJJttpEQQ9oTdsw7BykMB4VIX2rZzi1ZrnCzMZ8M,650
|
@@ -37,28 +37,6 @@ osbot_utils/decorators/methods/type_safe.py,sha256=WtoDTRPcFV-EgSaNEawMj2plVq-zR
|
|
37
37
|
osbot_utils/fluent/Fluent_Dict.py,sha256=nZ2z91s39sU2a-TLYpBirRoWgDXHrs0tQ9Bi_ZdKeW0,481
|
38
38
|
osbot_utils/fluent/Fluent_List.py,sha256=PfDDC9sm16CFnNQ8gkhCEsmKcZp8iyQ0YBpSHvYssG8,1089
|
39
39
|
osbot_utils/fluent/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
40
|
-
osbot_utils/graphs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
|
-
osbot_utils/graphs/mermaid/Mermaid.py,sha256=G7--iIKm2C1z-tEB1qLNopwoW3_w4oR7Oq7-yA460mM,3164
|
42
|
-
osbot_utils/graphs/mermaid/Mermaid__Edge.py,sha256=jwHxHJEAA49aO28T8nnJFxOfpWAZZaWKNT_krG1fwkQ,1893
|
43
|
-
osbot_utils/graphs/mermaid/Mermaid__Graph.py,sha256=FRw17efZrdcKyXDKsyb1C8nswIAmljiUAyiF0FHIL4M,2854
|
44
|
-
osbot_utils/graphs/mermaid/Mermaid__Node.py,sha256=j_AVfR3hnKAJH2Z3d17djvU7MfQP8B70Lh7Jv6y0tTs,3322
|
45
|
-
osbot_utils/graphs/mermaid/Mermaid__Renderer.py,sha256=-5h_Xkaq3boKVWzPYPG_kUoe0SOlR0Tm4zVEXJ70wUk,2289
|
46
|
-
osbot_utils/graphs/mermaid/configs/Mermaid__Edge__Config.py,sha256=PaypnkaDQQhNBIxZqnAB5bxuJAZWh3SQtmHUPfIcDCQ,212
|
47
|
-
osbot_utils/graphs/mermaid/configs/Mermaid__Node__Config.py,sha256=KZWd_uiIm-QIz_wPSDfXfWAWU_A2yxzMeq-h5wldVjQ,465
|
48
|
-
osbot_utils/graphs/mermaid/configs/Mermaid__Render__Config.py,sha256=JGQR6kdiTQh45zFZYvqdEDMghFIWLnVYMZsdukYtGyw,216
|
49
|
-
osbot_utils/graphs/mermaid/examples/Mermaid_Examples__FlowChart.py,sha256=BFlQ1C-DkmVxSWJKUQC8lFVxu26znHvgaXy50QkYVZY,2220
|
50
|
-
osbot_utils/graphs/mermaid/models/Mermaid__Diagram_Direction.py,sha256=m52zJ3bkHuF-Fq29CK3yAlX9RFi5_VQwqQGKaT1XRR4,141
|
51
|
-
osbot_utils/graphs/mermaid/models/Mermaid__Diagram__Type.py,sha256=QWPjpZ8Y77K3l8v481TK_UzbkxmSkIHDfOxc-P405TM,799
|
52
|
-
osbot_utils/graphs/mermaid/models/Mermaid__Node__Shape.py,sha256=_su5S8nYqg5qpXmWvKFZ05ZT5zLjTZP3sVIhc2hNpgg,1813
|
53
|
-
osbot_utils/graphs/mgraph/MGraph.py,sha256=1Iu2CM9IHxoJxjmABsBqDvi8l09LINLDE9_b53Dz4kM,2218
|
54
|
-
osbot_utils/graphs/mgraph/MGraph__Config.py,sha256=oFTj9eP92HolaOSyk-7tpsPVFZLMIcTH-O-mx93nTiA,204
|
55
|
-
osbot_utils/graphs/mgraph/MGraph__Data.py,sha256=oLaKmxUOXoQbhdUxa_VW_QZA0tAETgRMzP3pvslppHw,5746
|
56
|
-
osbot_utils/graphs/mgraph/MGraph__Edge.py,sha256=sS9LojdWZ8AISD0d8gdY6HgcyPM956roHmEkzhM_TQg,785
|
57
|
-
osbot_utils/graphs/mgraph/MGraph__Node.py,sha256=KnUnD918hu5ifjbW_v7AVAAzxjHi5iuA4rluvDGOaPg,893
|
58
|
-
osbot_utils/graphs/mgraph/MGraph__Random_Graphs.py,sha256=MDCmnrv7QcGXJeZDYc4y5Wth6ysB0hHzn1DqfocxtIs,964
|
59
|
-
osbot_utils/graphs/mgraph/MGraph__Serializer.py,sha256=pLgKsJuo1x8S22m6TCLd3bvFLrBo6sTrq1AkknkOA18,1475
|
60
|
-
osbot_utils/graphs/mgraph/MGraphs.py,sha256=TIv1CEsUiPIVxll3AuGLhEVBY4YRzZu_rpEz3Tzrph8,532
|
61
|
-
osbot_utils/graphs/mgraph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
62
40
|
osbot_utils/helpers/CFormat.py,sha256=1_XvqGwgU6qC97MbzcKF0o7s9mCXpU5Kq9Yf-1ixUwY,6808
|
63
41
|
osbot_utils/helpers/CPrint.py,sha256=ztKPNmT8BGxeyPXSQKRs63PqqbgxKDz_BiZmzFMup9g,1413
|
64
42
|
osbot_utils/helpers/Dependency_Manager.py,sha256=79YRYnVfchewq8iSMJ5dzwW2D5u8chWcIqYE-G9YrSo,1337
|
@@ -72,7 +50,7 @@ osbot_utils/helpers/Python_Audit.py,sha256=shpZlluJwqJBAlad6xN01FkgC1TsQ48RLvR5Z
|
|
72
50
|
osbot_utils/helpers/Random_Guid.py,sha256=COu9hcP51vzjk-ErECTFFaOWuOmW0eGJyMu8HXhaRXQ,382
|
73
51
|
osbot_utils/helpers/Random_Guid_Short.py,sha256=YP_k5OLuYvXWGU2OEnQHk_OGViBQofTWKm3pUdQaJao,404
|
74
52
|
osbot_utils/helpers/Random_Seed.py,sha256=14btja8LDN9cMGWaz4fCNcMRU_eyx49gas-_PQvHgy4,634
|
75
|
-
osbot_utils/helpers/Safe_Id.py,sha256=
|
53
|
+
osbot_utils/helpers/Safe_Id.py,sha256=0wPGd9eLzaOCYTSg9yEOal1kPsc8OI9khHkqEw9VQOM,446
|
76
54
|
osbot_utils/helpers/Str_ASCII.py,sha256=PRqyu449XnKrLn6b9Miii1Hv-GO5OAa1UhhgvlRcC2M,704
|
77
55
|
osbot_utils/helpers/Timestamp_Now.py,sha256=k3-SUGYx2jLTXvgZYeECqPRJhVxqWPmW7co1l6r12jk,438
|
78
56
|
osbot_utils/helpers/Type_Registry.py,sha256=Ajk3SyMSKDi2g9SJYUtTgg7PZkAgydaHcpbGuEN3S94,311
|
@@ -261,7 +239,6 @@ osbot_utils/helpers/ssh/TestCase__SSH.py,sha256=MD8sq0_kI4f6pEmEO0cLq2mQOhIqbP45
|
|
261
239
|
osbot_utils/helpers/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
262
240
|
osbot_utils/helpers/trace/Trace_Call.py,sha256=O_y6cncgneYrj3ARDMz-o9Yi1LjsESibUqkGFAg0Jk0,8886
|
263
241
|
osbot_utils/helpers/trace/Trace_Call__Config.py,sha256=UAjdsDEqsPBBsu-h4_QKYL4UMukJmQYBBWGYTmSKS40,3361
|
264
|
-
osbot_utils/helpers/trace/Trace_Call__Graph.py,sha256=HCrXRKQI42DIQxxyFLcaosWiOcUyoITbeV17ICdXcXM,1156
|
265
242
|
osbot_utils/helpers/trace/Trace_Call__Handler.py,sha256=9EQyIiGB6r3UURyNgEXHPOCmZNOejM0UJNW9c_MjpNU,12650
|
266
243
|
osbot_utils/helpers/trace/Trace_Call__Print_Lines.py,sha256=cy7zLv0_JNxdOIQPfZk6J9bv6AkIW6O643w0ykClXbw,4820
|
267
244
|
osbot_utils/helpers/trace/Trace_Call__Print_Traces.py,sha256=2LGeWMGP1uhSojGMmJmL3bH2B5LFIlfYEqEPNqoyKJw,8628
|
@@ -323,24 +300,24 @@ osbot_utils/utils/Files.py,sha256=yxteAcyhDcUy1_r9Eihx80V16lV_UAE6cvoOe2Dc7DU,23
|
|
323
300
|
osbot_utils/utils/Functions.py,sha256=0E6alPJ0fJpBiJgFOWooCOi265wSRyxxXAJ5CELBnso,3498
|
324
301
|
osbot_utils/utils/Http.py,sha256=Cm_-b2EgxKoQJ47ThZp-dgHCdeGv4UcCNLfTOH94-7s,7790
|
325
302
|
osbot_utils/utils/Int.py,sha256=PmlUdU4lSwf4gJdmTVdqclulkEp7KPCVUDO6AcISMF4,116
|
326
|
-
osbot_utils/utils/Json.py,sha256=
|
303
|
+
osbot_utils/utils/Json.py,sha256=0t7Hwefx8bg4JiZVr-xIbWP3BAk6_ZsnY7iV5pnRLDQ,7137
|
327
304
|
osbot_utils/utils/Json_Cache.py,sha256=mLPkkDZN-3ZVJiDvV1KBJXILtKkTZ4OepzOsDoBPhWg,2006
|
328
305
|
osbot_utils/utils/Lists.py,sha256=tPz5x5s3sRO97WZ_nsxREBPC5cwaHrhgaYBhsrffTT8,5599
|
329
306
|
osbot_utils/utils/Misc.py,sha256=H_xexJgiTxB3jDeDiW8efGQbO0Zuy8MM0iQ7qXC92JI,17363
|
330
|
-
osbot_utils/utils/Objects.py,sha256=
|
307
|
+
osbot_utils/utils/Objects.py,sha256=nsheXk2t4sRQVJStsKYDgCj8wF3knQ8X1AtLO1M4SlM,21614
|
331
308
|
osbot_utils/utils/Png.py,sha256=V1juGp6wkpPigMJ8HcxrPDIP4bSwu51oNkLI8YqP76Y,1172
|
332
309
|
osbot_utils/utils/Process.py,sha256=lr3CTiEkN3EiBx3ZmzYmTKlQoPdkgZBRjPulMxG-zdo,2357
|
333
310
|
osbot_utils/utils/Python_Logger.py,sha256=tx8N6wRKL3RDHboDRKZn8SirSJdSAE9cACyJkxrThZ8,12792
|
334
311
|
osbot_utils/utils/Regex.py,sha256=MtHhk69ax7Nwu4CQZK7y4KXHZ6VREwEpIchuioB168c,960
|
335
312
|
osbot_utils/utils/Status.py,sha256=Yq4s0TelXgn0i2QjCP9V8mP30GabXp_UL-jjM6Iwiw4,4305
|
336
|
-
osbot_utils/utils/Str.py,sha256=
|
313
|
+
osbot_utils/utils/Str.py,sha256=KQVfh0o3BxJKVm24yhAhgIGH5QYfzpP1G-siVv2zQws,3301
|
337
314
|
osbot_utils/utils/Threads.py,sha256=lnh4doZWYUIoWBZRU_780QPeAIKGDh7INuqmU8Fzmdc,3042
|
338
315
|
osbot_utils/utils/Toml.py,sha256=Rxl8gx7mni5CvBAK-Ai02EKw-GwtJdd3yeHT2kMloik,1667
|
339
316
|
osbot_utils/utils/Version.py,sha256=Ww6ChwTxqp1QAcxOnztkTicShlcx6fbNsWX5xausHrg,422
|
340
317
|
osbot_utils/utils/Zip.py,sha256=pR6sKliUY0KZXmqNzKY2frfW-YVQEVbLKiyqQX_lc-8,14052
|
341
318
|
osbot_utils/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
342
|
-
osbot_utils/version,sha256=
|
343
|
-
osbot_utils-1.
|
344
|
-
osbot_utils-1.
|
345
|
-
osbot_utils-1.
|
346
|
-
osbot_utils-1.
|
319
|
+
osbot_utils/version,sha256=_0FgKn2Budr8baj8RE7pf68-C8Yvew0utOg1014PVtc,8
|
320
|
+
osbot_utils-1.94.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
321
|
+
osbot_utils-1.94.0.dist-info/METADATA,sha256=v3Ivesk54q4PsxGFrjpS-7clJ6b6tkX_-vhJkZ-ZZp4,1317
|
322
|
+
osbot_utils-1.94.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
323
|
+
osbot_utils-1.94.0.dist-info/RECORD,,
|
osbot_utils/graphs/__init__.py
DELETED
File without changes
|
@@ -1,75 +0,0 @@
|
|
1
|
-
from osbot_utils.graphs.mermaid.Mermaid__Renderer import Mermaid__Renderer
|
2
|
-
from osbot_utils.graphs.mermaid.Mermaid__Edge import Mermaid__Edge
|
3
|
-
from osbot_utils.graphs.mermaid.Mermaid__Graph import Mermaid__Graph
|
4
|
-
from osbot_utils.graphs.mermaid.models.Mermaid__Diagram_Direction import Diagram__Direction
|
5
|
-
from osbot_utils.graphs.mermaid.models.Mermaid__Diagram__Type import Diagram__Type
|
6
|
-
from osbot_utils.utils.Python_Logger import Python_Logger
|
7
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
8
|
-
|
9
|
-
class Mermaid(Kwargs_To_Self):
|
10
|
-
graph : Mermaid__Graph
|
11
|
-
renderer : Mermaid__Renderer
|
12
|
-
logger : Python_Logger
|
13
|
-
|
14
|
-
# todo add support for storing the data in sqlite so that the search for existing nodes is efficient
|
15
|
-
def add_edge(self, from_node_key, to_node_key, label=None,attributes=None):
|
16
|
-
nodes_by_id = self.graph.data().nodes__by_key()
|
17
|
-
from_node = nodes_by_id.get(from_node_key)
|
18
|
-
to_node = nodes_by_id.get(to_node_key)
|
19
|
-
if not from_node:
|
20
|
-
from_node = self.add_node(key=from_node_key)
|
21
|
-
if not to_node:
|
22
|
-
to_node = self.add_node(key=to_node_key)
|
23
|
-
|
24
|
-
# todo: add back the protection/detection that we get from MGraph class of allow_circle_edges and allow_duplicate_edges
|
25
|
-
mermaid_edge = Mermaid__Edge(from_node=from_node, to_node=to_node, label=label, attributes=attributes)
|
26
|
-
self.graph.edges.append(mermaid_edge)
|
27
|
-
return mermaid_edge
|
28
|
-
|
29
|
-
def add_directive(self, directive):
|
30
|
-
self.renderer.config.directives.append(directive)
|
31
|
-
return self
|
32
|
-
|
33
|
-
def add_node(self, **kwargs):
|
34
|
-
return self.graph.add_node(**kwargs)
|
35
|
-
|
36
|
-
def code(self):
|
37
|
-
return self.renderer.code(self.nodes(), self.edges())
|
38
|
-
|
39
|
-
def code_markdown(self):
|
40
|
-
#self.code_create()
|
41
|
-
self.code()
|
42
|
-
rendered_lines = self.renderer.mermaid_code
|
43
|
-
markdown = ['#### Mermaid Graph',
|
44
|
-
"```mermaid" ,
|
45
|
-
*rendered_lines ,
|
46
|
-
"```" ]
|
47
|
-
|
48
|
-
return '\n'.join(markdown)
|
49
|
-
|
50
|
-
def edges(self):
|
51
|
-
return self.graph.edges
|
52
|
-
|
53
|
-
def print_code(self):
|
54
|
-
print(self.code())
|
55
|
-
|
56
|
-
def nodes(self):
|
57
|
-
return self.graph.nodes
|
58
|
-
|
59
|
-
def set_direction(self, direction):
|
60
|
-
if isinstance(direction, Diagram__Direction):
|
61
|
-
self.renderer.diagram_direction = direction
|
62
|
-
elif isinstance(direction, str) and direction in Diagram__Direction.__members__:
|
63
|
-
self.renderer.diagram_direction = Diagram__Direction[direction]
|
64
|
-
return self # If the value can't be set (not a valid name), do nothing
|
65
|
-
|
66
|
-
def set_diagram_type(self, diagram_type):
|
67
|
-
if isinstance(diagram_type, Diagram__Type):
|
68
|
-
self.renderer.diagram_type = diagram_type
|
69
|
-
|
70
|
-
def save(self, target_file=None):
|
71
|
-
file_path = target_file or '/tmp/mermaid.md'
|
72
|
-
|
73
|
-
with open(file_path, 'w') as file:
|
74
|
-
file.write(self.code_markdown())
|
75
|
-
return file_path
|
@@ -1,49 +0,0 @@
|
|
1
|
-
from osbot_utils.graphs.mermaid.Mermaid__Node import LINE_PADDING, Mermaid__Node
|
2
|
-
from osbot_utils.graphs.mermaid.configs.Mermaid__Edge__Config import Mermaid__Edge__Config
|
3
|
-
from osbot_utils.graphs.mgraph.MGraph__Edge import MGraph__Edge
|
4
|
-
#from osbot_utils.graphs.mgraph.views.mermaid.Mermaid__Node import Mermaid__Node
|
5
|
-
from osbot_utils.utils.Str import safe_str
|
6
|
-
|
7
|
-
|
8
|
-
class Mermaid__Edge(MGraph__Edge):
|
9
|
-
config : Mermaid__Edge__Config
|
10
|
-
from_node : Mermaid__Node
|
11
|
-
to_node : Mermaid__Node
|
12
|
-
|
13
|
-
# def __init__(self, **kwargs):
|
14
|
-
# super().__init__(**kwargs)
|
15
|
-
|
16
|
-
# def __init__(self, **kwargs):
|
17
|
-
# super().__init__(**kwargs)
|
18
|
-
# self.convert_nodes()
|
19
|
-
#
|
20
|
-
def edge_mode(self, edge_mode):
|
21
|
-
self.config.edge_mode = edge_mode
|
22
|
-
return self
|
23
|
-
|
24
|
-
def edge_mode__lr_using_pipe(self):
|
25
|
-
return self.edge_mode('lr_using_pipe')
|
26
|
-
|
27
|
-
def output_node_from(self, value=True):
|
28
|
-
self.config.output_node_from = value
|
29
|
-
return self
|
30
|
-
|
31
|
-
def output_node_to(self, value=True):
|
32
|
-
self.config.output_node_to = value
|
33
|
-
return self
|
34
|
-
|
35
|
-
def render_edge(self):
|
36
|
-
from_node_key = safe_str(self.from_node.key)
|
37
|
-
to_node_key = safe_str(self.to_node .key)
|
38
|
-
if self.config.output_node_from:
|
39
|
-
from_node_key = self.from_node.render_node(include_padding=False) #f'{edge.from_node.key}["{edge.from_node.label}"]'
|
40
|
-
if self.config.output_node_to:
|
41
|
-
to_node_key = self.to_node.render_node(include_padding=False ) #f'{edge.to_node .key}["{edge.to_node .label}"]'
|
42
|
-
if self.config.edge_mode == 'lr_using_pipe':
|
43
|
-
link_code = f'-->|{self.label}|'
|
44
|
-
elif self.label:
|
45
|
-
link_code = f'--"{self.label}"-->'
|
46
|
-
else:
|
47
|
-
link_code = '-->'
|
48
|
-
edge_code = f'{LINE_PADDING}{from_node_key} {link_code} {to_node_key}'
|
49
|
-
return edge_code
|
@@ -1,95 +0,0 @@
|
|
1
|
-
from typing import List
|
2
|
-
|
3
|
-
from osbot_utils.graphs.mgraph.MGraph__Edge import MGraph__Edge
|
4
|
-
from osbot_utils.graphs.mgraph.MGraph__Node import MGraph__Node
|
5
|
-
from osbot_utils.graphs.mermaid.Mermaid__Edge import Mermaid__Edge
|
6
|
-
from osbot_utils.graphs.mermaid.Mermaid__Node import Mermaid__Node
|
7
|
-
from osbot_utils.graphs.mgraph.MGraph import MGraph
|
8
|
-
|
9
|
-
|
10
|
-
class Mermaid__Graph(MGraph):
|
11
|
-
edges : List[Mermaid__Edge]
|
12
|
-
mermaid_code : List
|
13
|
-
nodes : List[Mermaid__Node]
|
14
|
-
|
15
|
-
# def __init__(self, mgraph=None):
|
16
|
-
# super().__init__()
|
17
|
-
# if mgraph is None:
|
18
|
-
# mgraph = MGraph()
|
19
|
-
# self.__dict__ = mgraph.__dict__
|
20
|
-
# self.convert_nodes().convert_edges()
|
21
|
-
|
22
|
-
# def cast(self, source):
|
23
|
-
# self.__dict__ = source.__dict__
|
24
|
-
# return self
|
25
|
-
|
26
|
-
# def add_edge(self, **kwargs):
|
27
|
-
# #new_edge = super().add_edge(*args, **kwargs)
|
28
|
-
# mermaid_edge = Mermaid__Edge(**kwargs)
|
29
|
-
# self.edges.append(mermaid_edge)
|
30
|
-
# return mermaid_edge
|
31
|
-
#
|
32
|
-
def add_node(self, **kwargs):
|
33
|
-
new_node = MGraph__Node(**kwargs)
|
34
|
-
mermaid_node = Mermaid__Node().merge_with(new_node)
|
35
|
-
self.nodes.append(mermaid_node)
|
36
|
-
return mermaid_node
|
37
|
-
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# def add_line(self, line):
|
41
|
-
# self.mermaid_code.append(line)
|
42
|
-
# return line
|
43
|
-
#
|
44
|
-
# def code(self):
|
45
|
-
# self.code_create()
|
46
|
-
# return '\n'.join(self.mermaid_code)
|
47
|
-
#
|
48
|
-
# def code_create(self):
|
49
|
-
# with self as _:
|
50
|
-
# _.reset_code()
|
51
|
-
# _.add_line('graph LR')
|
52
|
-
# for node in _.nodes:
|
53
|
-
# _.add_line(node.code())
|
54
|
-
# _.add_line('')
|
55
|
-
# for edge in _.edges:
|
56
|
-
# _.add_line(edge.code())
|
57
|
-
# return self
|
58
|
-
#
|
59
|
-
# def code_markdown(self):
|
60
|
-
# self.code_create()
|
61
|
-
# markdown = ['# Mermaid Graph',
|
62
|
-
# "```mermaid" ,
|
63
|
-
# *self.mermaid_code,
|
64
|
-
# "```"]
|
65
|
-
#
|
66
|
-
# return '\n'.join(markdown)
|
67
|
-
#
|
68
|
-
# def convert_edges(self):
|
69
|
-
# new_edges = []
|
70
|
-
# for edge in self.edges:
|
71
|
-
# new_edges.append(Mermaid__Edge().cast(edge))
|
72
|
-
# self.edges = new_edges
|
73
|
-
# return self
|
74
|
-
#
|
75
|
-
# def convert_nodes(self):
|
76
|
-
# new_nodes = []
|
77
|
-
# for node in self.nodes:
|
78
|
-
# mermaid_node = Mermaid__Node().cast(node)
|
79
|
-
# new_nodes.append(mermaid_node)
|
80
|
-
# self.nodes = new_nodes
|
81
|
-
# return self
|
82
|
-
#
|
83
|
-
# def reset_code(self):
|
84
|
-
# self.mermaid_code = []
|
85
|
-
# return self
|
86
|
-
#
|
87
|
-
# def save(self, target_file=None):
|
88
|
-
# file_path = target_file or '/tmp/mermaid.md'
|
89
|
-
#
|
90
|
-
# with open(file_path, 'w') as file:
|
91
|
-
# file.write(self.code_markdown())
|
92
|
-
# return file_path
|
93
|
-
#
|
94
|
-
# def print_code(self):
|
95
|
-
# print(self.code())
|
@@ -1,69 +0,0 @@
|
|
1
|
-
from enum import Enum
|
2
|
-
|
3
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
4
|
-
from osbot_utils.graphs.mermaid.configs.Mermaid__Node__Config import Mermaid__Node__Config
|
5
|
-
from osbot_utils.graphs.mermaid.models.Mermaid__Node__Shape import Mermaid__Node__Shape
|
6
|
-
from osbot_utils.graphs.mgraph.MGraph__Node import MGraph__Node
|
7
|
-
from osbot_utils.utils.Str import safe_str
|
8
|
-
|
9
|
-
LINE_PADDING = ' '
|
10
|
-
|
11
|
-
class Mermaid__Node(MGraph__Node):
|
12
|
-
|
13
|
-
config : Mermaid__Node__Config
|
14
|
-
|
15
|
-
def render_node(self, include_padding=True):
|
16
|
-
left_char, right_char = self.config.node_shape.value
|
17
|
-
|
18
|
-
if self.config.markdown:
|
19
|
-
label = f'`{self.label}`'
|
20
|
-
else:
|
21
|
-
label = self.label
|
22
|
-
|
23
|
-
|
24
|
-
if self.config.show_label is False:
|
25
|
-
node_code = f'{self.key}'
|
26
|
-
else:
|
27
|
-
if self.config.wrap_with_quotes is False:
|
28
|
-
node_code = f'{self.key}{left_char}{label}{right_char}'
|
29
|
-
else:
|
30
|
-
node_code = f'{self.key}{left_char}"{label}"{right_char}'
|
31
|
-
|
32
|
-
if include_padding:
|
33
|
-
node_code = f'{LINE_PADDING}{node_code}'
|
34
|
-
return node_code
|
35
|
-
|
36
|
-
def markdown(self, value=True):
|
37
|
-
self.config.markdown = value
|
38
|
-
return self
|
39
|
-
|
40
|
-
def shape(self, shape=None):
|
41
|
-
self.config.node_shape = Mermaid__Node__Shape.get_shape(shape)
|
42
|
-
return self
|
43
|
-
|
44
|
-
|
45
|
-
def shape_asymmetric (self): self.config.node_shape = Mermaid__Node__Shape.asymmetric ; return self
|
46
|
-
def shape_circle (self): self.config.node_shape = Mermaid__Node__Shape.circle ; return self
|
47
|
-
def shape_cylindrical (self): self.config.node_shape = Mermaid__Node__Shape.cylindrical ; return self
|
48
|
-
def shape_default (self): self.config.node_shape = Mermaid__Node__Shape.default ; return self
|
49
|
-
def shape_double_circle (self): self.config.node_shape = Mermaid__Node__Shape.double_circle ; return self
|
50
|
-
def shape_hexagon (self): self.config.node_shape = Mermaid__Node__Shape.hexagon ; return self
|
51
|
-
def shape_parallelogram (self): self.config.node_shape = Mermaid__Node__Shape.parallelogram ; return self
|
52
|
-
def shape_parallelogram_alt (self): self.config.node_shape = Mermaid__Node__Shape.parallelogram_alt ; return self
|
53
|
-
def shape_stadium (self): self.config.node_shape = Mermaid__Node__Shape.stadium ; return self
|
54
|
-
def shape_subroutine (self): self.config.node_shape = Mermaid__Node__Shape.subroutine ; return self
|
55
|
-
def shape_rectangle (self): self.config.node_shape = Mermaid__Node__Shape.rectangle ; return self
|
56
|
-
def shape_rhombus (self): self.config.node_shape = Mermaid__Node__Shape.rhombus ; return self
|
57
|
-
def shape_round_edges (self): self.config.node_shape = Mermaid__Node__Shape.round_edges ; return self
|
58
|
-
def shape_trapezoid (self): self.config.node_shape = Mermaid__Node__Shape.trapezoid ; return self
|
59
|
-
def shape_trapezoid_alt (self): self.config.node_shape = Mermaid__Node__Shape.trapezoid_alt ; return self
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
def wrap_with_quotes(self, value=True):
|
64
|
-
self.config.wrap_with_quotes = value
|
65
|
-
return self
|
66
|
-
|
67
|
-
def show_label(self, value=True):
|
68
|
-
self.config.show_label = value
|
69
|
-
return self
|
@@ -1,56 +0,0 @@
|
|
1
|
-
from typing import List
|
2
|
-
|
3
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
4
|
-
from osbot_utils.graphs.mermaid.configs.Mermaid__Render__Config import Mermaid__Render__Config
|
5
|
-
from osbot_utils.graphs.mermaid.models.Mermaid__Diagram_Direction import Diagram__Direction
|
6
|
-
from osbot_utils.graphs.mermaid.models.Mermaid__Diagram__Type import Diagram__Type
|
7
|
-
|
8
|
-
|
9
|
-
class Mermaid__Renderer(Kwargs_To_Self):
|
10
|
-
config : Mermaid__Render__Config
|
11
|
-
mermaid_code : List
|
12
|
-
diagram_direction : Diagram__Direction = Diagram__Direction.LR
|
13
|
-
diagram_type : Diagram__Type = Diagram__Type.graph
|
14
|
-
|
15
|
-
|
16
|
-
def add_line(self, line):
|
17
|
-
self.mermaid_code.append(line)
|
18
|
-
return line
|
19
|
-
|
20
|
-
def code(self, nodes, edges):
|
21
|
-
self.code_create(nodes, edges)
|
22
|
-
return '\n'.join(self.mermaid_code)
|
23
|
-
|
24
|
-
def code_create(self, nodes, edges, recreate=False):
|
25
|
-
with self as _:
|
26
|
-
if recreate: # if recreate is True, reset the code
|
27
|
-
_.reset_code()
|
28
|
-
elif self.mermaid_code: # if the code has already been created, don't create it
|
29
|
-
return self # todo: find a better way to do this, namely around the concept of auto detecting (on change) when the recreation needs to be done (vs being able to use the previously calculated data)
|
30
|
-
for directive in _.config.directives:
|
31
|
-
_.add_line(f'%%{{{directive}}}%%')
|
32
|
-
_.add_line(self.graph_header())
|
33
|
-
if self.config.add_nodes:
|
34
|
-
for node in nodes:
|
35
|
-
node_code = node.render_node()
|
36
|
-
_.add_line(node_code)
|
37
|
-
if self.config.line_before_edges:
|
38
|
-
_.add_line('')
|
39
|
-
for edge in edges:
|
40
|
-
edge_code = edge.render_edge()
|
41
|
-
_.add_line(edge_code)
|
42
|
-
return self
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
def graph_header(self):
|
47
|
-
# if type(self.diagram_type.value) is str:
|
48
|
-
# value = self.diagram_type.value
|
49
|
-
# else:
|
50
|
-
# value = self.diagram_type.name
|
51
|
-
value = self.diagram_type.name
|
52
|
-
return f'{value} {self.diagram_direction.name}'
|
53
|
-
|
54
|
-
def reset_code(self):
|
55
|
-
self.mermaid_code = []
|
56
|
-
return self
|
@@ -1,9 +0,0 @@
|
|
1
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
2
|
-
from osbot_utils.graphs.mermaid.models.Mermaid__Node__Shape import Mermaid__Node__Shape
|
3
|
-
|
4
|
-
|
5
|
-
class Mermaid__Node__Config(Kwargs_To_Self):
|
6
|
-
markdown : bool
|
7
|
-
node_shape : Mermaid__Node__Shape = Mermaid__Node__Shape.default
|
8
|
-
show_label : bool = True
|
9
|
-
wrap_with_quotes : bool = True # todo: add support for only using quotes when needed
|
@@ -1,98 +0,0 @@
|
|
1
|
-
# from https://mermaid.js.org/syntax/flowchart.html
|
2
|
-
class Mermain_Examples__FlowChart:
|
3
|
-
example_1__a_node_default = """
|
4
|
-
flowchart LR
|
5
|
-
id
|
6
|
-
"""
|
7
|
-
example_2__a_node_with_text = """
|
8
|
-
flowchart LR
|
9
|
-
id1[This is the text in the box]
|
10
|
-
"""
|
11
|
-
|
12
|
-
example_3__a_node_with_unicode_text = """
|
13
|
-
flowchart LR
|
14
|
-
id["This ❤ Unicode"]
|
15
|
-
"""
|
16
|
-
example_4__a_node_with_markdown_formatting = """
|
17
|
-
%%{init: {"flowchart": {"htmlLabels": false}} }%%
|
18
|
-
flowchart LR
|
19
|
-
markdown["`This **is** _Markdown_`"]
|
20
|
-
newLines["`Line1
|
21
|
-
Line 2
|
22
|
-
Line 3`"]
|
23
|
-
markdown --> newLines
|
24
|
-
"""
|
25
|
-
|
26
|
-
example_5__direction__from_top_to_bottom = """
|
27
|
-
flowchart TD
|
28
|
-
Start --> Stop
|
29
|
-
"""
|
30
|
-
|
31
|
-
example_6__direction__from_left_to_right = """
|
32
|
-
flowchart LR
|
33
|
-
Start --> Stop
|
34
|
-
"""
|
35
|
-
example_7__node_shapes_a_node_with_round_edges = """
|
36
|
-
flowchart LR
|
37
|
-
id1(This is the text in the box)
|
38
|
-
"""
|
39
|
-
|
40
|
-
example_8__node_shapes_a_stadium_shaped_node = """
|
41
|
-
flowchart LR
|
42
|
-
id1([This is the text in the box])
|
43
|
-
"""
|
44
|
-
|
45
|
-
example_9__node_shapes_a_node_in_a_subroutine ="""
|
46
|
-
flowchart LR
|
47
|
-
id1[[This is the text in the box]]
|
48
|
-
"""
|
49
|
-
|
50
|
-
example_10__node_shapes_a_node_in_a_cylindrical_shape ="""
|
51
|
-
flowchart LR
|
52
|
-
id1[(Database)]
|
53
|
-
"""
|
54
|
-
|
55
|
-
example_11__node_shapes_a_node_in_the_form_of_a_circle = """
|
56
|
-
flowchart LR
|
57
|
-
id1((This is the text in the circle))
|
58
|
-
"""
|
59
|
-
|
60
|
-
example_12__node_shapes_a_node_in_an_asymmetric_shape = """
|
61
|
-
flowchart LR
|
62
|
-
id1>This is the text in the box]
|
63
|
-
"""
|
64
|
-
|
65
|
-
example_13__node_shapes_a_node_rhombus = """
|
66
|
-
flowchart LR
|
67
|
-
id1{This is the text in the box}
|
68
|
-
"""
|
69
|
-
|
70
|
-
example_14__node_shapes_a_hexagon_node = """
|
71
|
-
flowchart LR
|
72
|
-
id1{{This is the text in the box}}
|
73
|
-
"""
|
74
|
-
|
75
|
-
example_15__node_shapes_parallelogram = """
|
76
|
-
flowchart TD
|
77
|
-
id1[/This is the text in the box/]
|
78
|
-
"""
|
79
|
-
|
80
|
-
example_16__node_shapes_parallelogram_alt = r"""
|
81
|
-
flowchart TD
|
82
|
-
id1[\This is the text in the box\]
|
83
|
-
"""
|
84
|
-
|
85
|
-
example_17__node_shapes_trapezoid = r"""
|
86
|
-
flowchart TD
|
87
|
-
A[/Christmas\]
|
88
|
-
"""
|
89
|
-
|
90
|
-
example_18__node_shapes_trapezoid_alt = r"""
|
91
|
-
flowchart TD
|
92
|
-
B[\Go shopping/]
|
93
|
-
"""
|
94
|
-
|
95
|
-
example_19__node_shapes_double_circle = """
|
96
|
-
flowchart TD
|
97
|
-
id1(((This is the text in the circle)))
|
98
|
-
"""
|
@@ -1,16 +0,0 @@
|
|
1
|
-
from enum import Enum
|
2
|
-
|
3
|
-
class Diagram__Type(Enum):
|
4
|
-
class_diagram = "class_diagram"
|
5
|
-
entity_relationship_diagram = "entity_relationship_diagram"
|
6
|
-
flowchart = "flowchart"
|
7
|
-
gantt = "gantt"
|
8
|
-
git_graph = "git_graph"
|
9
|
-
graph = "graph"
|
10
|
-
mermaid_map = "mermaid_map"
|
11
|
-
mindmap = "mindmap"
|
12
|
-
pie_chart = "pie_chart"
|
13
|
-
requirement_diagram = "requirement_diagram"
|
14
|
-
sequence_diagram = "sequenceDiagram" # these are different from the others
|
15
|
-
state_diagram = "stateDiagram-v2" # these are different from the others
|
16
|
-
user_journey = "user_journey"
|
@@ -1,30 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from enum import Enum
|
3
|
-
|
4
|
-
|
5
|
-
class Mermaid__Node__Shape(Enum):
|
6
|
-
asymmetric = ('>' , ']' ) # Asymmetric
|
7
|
-
circle = ('((' , '))' ) # Circle, used for endpoints or start/end points
|
8
|
-
cylindrical = ('[(' , ')]' ) # Cylindrical
|
9
|
-
default = ('[' , ']' ) # Rectangle
|
10
|
-
double_circle = ('(((', ')))') # Double Circle
|
11
|
-
round_edges = ('(' , ')' ) # Stadium shape, used for processes or operations
|
12
|
-
rhombus = ('{' , '}' ) # Rhombus, often synonymous with diamond in diagramming contexts
|
13
|
-
hexagon = ('{{' , '}}' ) # Hexagon, used for preparation or complex processing
|
14
|
-
parallelogram = ('[/' , '/]' ) # Parallelogram, used for input/output
|
15
|
-
parallelogram_alt = ('[\\', '\\]') # Alternative parallelogram, also used for input/output
|
16
|
-
rectangle = ('[' , ']' ) # Rectangle, used for process
|
17
|
-
stadium = ('([' , '])' ) # Stadium
|
18
|
-
subroutine = ('[[', ']]' ) # Subroutine
|
19
|
-
trapezoid = ('[/' , r'\]' ) # Trapezoid, used for manual operations # todo: figure out why this line is throwing the compile error of: SyntaxWarning: invalid escape sequence '\]'
|
20
|
-
trapezoid_alt = ('[\\', '/]' ) # Inverted trapezoid, also used for manual operations
|
21
|
-
|
22
|
-
@staticmethod
|
23
|
-
def get_shape(value = None) -> Mermaid__Node__Shape:
|
24
|
-
if isinstance(value, Mermaid__Node__Shape):
|
25
|
-
return value
|
26
|
-
if type(value) is str:
|
27
|
-
for shape in Mermaid__Node__Shape:
|
28
|
-
if value == shape.name:
|
29
|
-
return shape
|
30
|
-
return Mermaid__Node__Shape.default
|
@@ -1,55 +0,0 @@
|
|
1
|
-
from typing import List
|
2
|
-
|
3
|
-
from osbot_utils.utils.Misc import random_text, lower
|
4
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
5
|
-
from osbot_utils.graphs.mgraph.MGraph__Config import MGraph__Config
|
6
|
-
from osbot_utils.graphs.mgraph.MGraph__Edge import MGraph__Edge
|
7
|
-
from osbot_utils.graphs.mgraph.MGraph__Node import MGraph__Node
|
8
|
-
|
9
|
-
|
10
|
-
# todo add support for storing the data in sqlite so that we get the ability to search nodes and edges
|
11
|
-
class MGraph(Kwargs_To_Self):
|
12
|
-
config : MGraph__Config
|
13
|
-
edges : List[MGraph__Edge]
|
14
|
-
key : str
|
15
|
-
nodes : List[MGraph__Node]
|
16
|
-
|
17
|
-
|
18
|
-
def __init__(self, **kwargs):
|
19
|
-
super().__init__(**kwargs)
|
20
|
-
if not self.key:
|
21
|
-
self.key = random_text("mgraph", lowercase=True) # make sure there is always a key
|
22
|
-
|
23
|
-
def add_edge(self, from_node, to_node, label=None,attributes=None):
|
24
|
-
if self.config.allow_circle_edges is False:
|
25
|
-
if from_node == to_node:
|
26
|
-
return None
|
27
|
-
if self.config.allow_duplicate_edges is False: # todo: figure out if there is a more efficient way to do this
|
28
|
-
for edge in self.edges:
|
29
|
-
if edge.from_node == from_node and edge.to_node == to_node:
|
30
|
-
return None
|
31
|
-
new_edge = MGraph__Edge(from_node=from_node, to_node=to_node, label=label, attributes=attributes)
|
32
|
-
self.edges.append(new_edge)
|
33
|
-
return new_edge
|
34
|
-
|
35
|
-
def add_node(self, key=None, label=None, attributes=None):
|
36
|
-
new_node = MGraph__Node(key=key, label=label, attributes=attributes)
|
37
|
-
self.nodes.append(new_node)
|
38
|
-
return new_node
|
39
|
-
|
40
|
-
def data(self):
|
41
|
-
from osbot_utils.graphs.mgraph.MGraph__Data import MGraph__Data
|
42
|
-
return MGraph__Data(mgraph=self)
|
43
|
-
|
44
|
-
# def save(self, format='pickle'):
|
45
|
-
# if format == 'pickle':
|
46
|
-
# return pickle_save_to_file(self)
|
47
|
-
|
48
|
-
#todo: add save that return saved object
|
49
|
-
# def save(self):
|
50
|
-
# from osbot_utils.graphs.mgraph.MGraph__Serializer import MGraph__Serializer # due to circular dependency
|
51
|
-
# return MGraph__Serializer(mgraph=self).save()
|
52
|
-
|
53
|
-
def print(self):
|
54
|
-
print()
|
55
|
-
return self.data().print()
|
@@ -1,139 +0,0 @@
|
|
1
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
2
|
-
from osbot_utils.graphs.mgraph.MGraph import MGraph
|
3
|
-
from osbot_utils.helpers.Print_Table import Print_Table
|
4
|
-
|
5
|
-
|
6
|
-
class MGraph__Data(Kwargs_To_Self):
|
7
|
-
|
8
|
-
mgraph : MGraph
|
9
|
-
|
10
|
-
def graph_data(self):
|
11
|
-
nodes_data = self.nodes_data()
|
12
|
-
edges_data = self.edges_data()
|
13
|
-
graph_data = {'nodes': nodes_data, 'edges': edges_data}
|
14
|
-
return graph_data
|
15
|
-
|
16
|
-
def edges(self):
|
17
|
-
return self.mgraph.edges
|
18
|
-
|
19
|
-
def edges_data(self):
|
20
|
-
edges_data = []
|
21
|
-
for edge in self.edges():
|
22
|
-
edges_data.append(edge.data())
|
23
|
-
return edges_data
|
24
|
-
|
25
|
-
def nodes(self):
|
26
|
-
return self.mgraph.nodes
|
27
|
-
|
28
|
-
def nodes_data(self):
|
29
|
-
nodes_data = []
|
30
|
-
for node in self.nodes():
|
31
|
-
nodes_data.append(node.data())
|
32
|
-
return nodes_data
|
33
|
-
|
34
|
-
|
35
|
-
def nodes__by_key(self):
|
36
|
-
by_key = {}
|
37
|
-
for node in self.nodes():
|
38
|
-
by_key[node.key] = node
|
39
|
-
return by_key
|
40
|
-
|
41
|
-
def nodes__keys(self):
|
42
|
-
return [node.key for node in self.nodes()]
|
43
|
-
|
44
|
-
def nodes_edges(self):
|
45
|
-
nodes__edges = {}
|
46
|
-
for node in self.nodes():
|
47
|
-
nodes__edges[node.key] = []
|
48
|
-
for edge in self.edges():
|
49
|
-
from_key = edge.from_node.key
|
50
|
-
if from_key in nodes__edges: # todo: add a better way to handle this, which is a weird situation, look also at a better way to do this assigment
|
51
|
-
nodes__edges[from_key].append(edge.to_node.key)
|
52
|
-
for node_key, edges_keys in nodes__edges.items():
|
53
|
-
nodes__edges[node_key] = sorted(edges_keys)
|
54
|
-
return nodes__edges
|
55
|
-
|
56
|
-
# def map_paths(self, key, paths, all_paths, nodes_edges):
|
57
|
-
# key_edges = nodes_edges[key]
|
58
|
-
# new_paths = []
|
59
|
-
#
|
60
|
-
# for edge_key in key_edges:
|
61
|
-
# for path in paths:
|
62
|
-
# if edge_key in path:
|
63
|
-
# if path not in all_paths:
|
64
|
-
# all_paths.append(path)
|
65
|
-
# else:
|
66
|
-
# new_path = [*path, edge_key]
|
67
|
-
# new_paths.append(new_path)
|
68
|
-
# self.map_paths(edge_key, new_paths, all_paths, nodes_edges)
|
69
|
-
# if new_path not in all_paths:
|
70
|
-
# all_paths.append(new_path)
|
71
|
-
# if new_paths:
|
72
|
-
# return new_paths
|
73
|
-
|
74
|
-
# for edge_key in key_edges:
|
75
|
-
# self.map_paths(edge_key, paths, nodes_edges)
|
76
|
-
return paths
|
77
|
-
|
78
|
-
# def nodes__find_all_paths(self):
|
79
|
-
# key = self.nodes__keys()[0]
|
80
|
-
# nodes_edges = self.nodes_edges()
|
81
|
-
# #for key in self.nodes__keys():
|
82
|
-
# all_paths = []
|
83
|
-
# paths = [[key]]
|
84
|
-
# self.map_paths(key, paths,all_paths, nodes_edges)
|
85
|
-
# pprint(all_paths)
|
86
|
-
|
87
|
-
def print(self):
|
88
|
-
with Print_Table() as _:
|
89
|
-
_.set_title(self.mgraph.config.graph_title)
|
90
|
-
for node_key, edges_keys in self.nodes_edges().items():
|
91
|
-
row = {'key': node_key, 'edges': edges_keys}
|
92
|
-
_.add_data(row)
|
93
|
-
_.set_order('key', 'edges')
|
94
|
-
_.print()
|
95
|
-
|
96
|
-
def print_adjacency_matrix(self):
|
97
|
-
adjacency_matrix = self.nodes_edges__adjacency_matrix()
|
98
|
-
node_keys = sorted(self.nodes__keys())
|
99
|
-
with Print_Table() as _:
|
100
|
-
for row in adjacency_matrix:
|
101
|
-
_.add_data(row)
|
102
|
-
_.set_order('key', *node_keys)
|
103
|
-
_.print()
|
104
|
-
|
105
|
-
|
106
|
-
def node_edges__to_from(self):
|
107
|
-
# Initialize a dictionary to hold the edges to and from for each node
|
108
|
-
node_connections = { node_key: {'edges_to': [], 'edges_from': []} for node_key in self.nodes_edges().keys() }
|
109
|
-
|
110
|
-
|
111
|
-
for node_key, edges_keys in self.nodes_edges().items(): # Fill 'edges_to' and 'edges_from' for each node
|
112
|
-
node_connections[node_key]['edges_from'].extend(edges_keys) # 'edges_from' are the outgoing edges from 'node_key'
|
113
|
-
|
114
|
-
for edge_key in edges_keys: # 'edges_to' are the incoming edges to the nodes in 'edges_keys'
|
115
|
-
if edge_key in node_connections: # Check if the edge_key represents a valid node
|
116
|
-
node_connections[edge_key]['edges_to'].append(node_key)
|
117
|
-
|
118
|
-
return node_connections
|
119
|
-
|
120
|
-
def nodes_edges__adjacency_matrix(self):
|
121
|
-
nodes_edges = self.nodes_edges() # Retrieve the nodes and their edges
|
122
|
-
node_keys = sorted(nodes_edges.keys()) # Get a sorted list of unique node keys
|
123
|
-
node_indices = {node_key: index for index, node_key in enumerate(node_keys)} # Create a mapping of node keys to their respective indices
|
124
|
-
size = len(node_keys) # Initialize a square matrix with empty strings
|
125
|
-
matrix = [['' for _ in range(size)] for _ in range(size)]
|
126
|
-
|
127
|
-
for node_key, edges_keys in nodes_edges.items(): # Fill the matrix with 'X' if there is an edge between two nodes
|
128
|
-
for edge_key in edges_keys: # Find the matrix positions based on node indices
|
129
|
-
row_index = node_indices[node_key]
|
130
|
-
col_index = node_indices[edge_key]
|
131
|
-
matrix[row_index][col_index] = 'X'
|
132
|
-
|
133
|
-
table_data = []
|
134
|
-
for i, row in enumerate(matrix):
|
135
|
-
row_data = {'key': node_keys[i]}
|
136
|
-
row_data.update({node_keys[j]: row[j] for j in range(size)})
|
137
|
-
table_data.append(row_data)
|
138
|
-
return table_data
|
139
|
-
|
@@ -1,24 +0,0 @@
|
|
1
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
2
|
-
from osbot_utils.graphs.mgraph.MGraph__Node import MGraph__Node
|
3
|
-
|
4
|
-
class MGraph__Edge(Kwargs_To_Self):
|
5
|
-
attributes : dict
|
6
|
-
from_node : MGraph__Node
|
7
|
-
label : str
|
8
|
-
to_node : MGraph__Node
|
9
|
-
|
10
|
-
def __init__(self, **kwargs):
|
11
|
-
super().__init__(**kwargs)
|
12
|
-
|
13
|
-
# def __repr__(self):
|
14
|
-
# return self.__str__()
|
15
|
-
|
16
|
-
def __str__(self):
|
17
|
-
return f'[Graph Edge] from "{self.from_node.key}" to "{self.to_node.key}" '
|
18
|
-
|
19
|
-
# def cast(self, source):
|
20
|
-
# self.__dict__ = source.__dict__
|
21
|
-
# return self
|
22
|
-
|
23
|
-
def data(self):
|
24
|
-
return self.__locals__() # todo: see if there is a better way to do this (specialy as the edge objects gets more features and attributes)
|
@@ -1,32 +0,0 @@
|
|
1
|
-
from osbot_utils.utils.Misc import random_id
|
2
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
3
|
-
|
4
|
-
|
5
|
-
class MGraph__Node(Kwargs_To_Self):
|
6
|
-
attributes : dict
|
7
|
-
key : str
|
8
|
-
label : str
|
9
|
-
|
10
|
-
def __init__(self, **kwargs):
|
11
|
-
super().__init__(**kwargs)
|
12
|
-
if not self.key:
|
13
|
-
self.key = random_id()
|
14
|
-
if not self.label:
|
15
|
-
self.label = self.key
|
16
|
-
|
17
|
-
def __repr__(self):
|
18
|
-
return self.__str__()
|
19
|
-
|
20
|
-
def __str__(self):
|
21
|
-
return f'[Graph Node] {self.key}'
|
22
|
-
|
23
|
-
def data(self):
|
24
|
-
return self.__locals__() # todo: see if there is a better way to do this (specialy as the node objects gets more features and attributes)
|
25
|
-
|
26
|
-
def set_key(self, value: str):
|
27
|
-
self.key = value
|
28
|
-
return self
|
29
|
-
|
30
|
-
def set_label(self, value: str):
|
31
|
-
self.label = value
|
32
|
-
return self
|
@@ -1,27 +0,0 @@
|
|
1
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
2
|
-
|
3
|
-
from osbot_utils.graphs.mgraph.MGraph import MGraph
|
4
|
-
from osbot_utils.graphs.mgraph.MGraph__Config import MGraph__Config
|
5
|
-
from osbot_utils.utils.Misc import random_int
|
6
|
-
|
7
|
-
|
8
|
-
class MGraph__Random_Graphs(Kwargs_To_Self):
|
9
|
-
config : MGraph__Config
|
10
|
-
graph_key : str
|
11
|
-
|
12
|
-
def new_graph(self):
|
13
|
-
return MGraph(config=self.config, key=self.graph_key)
|
14
|
-
|
15
|
-
def with_x_nodes_and_y_edges(self, x=10, y=20):
|
16
|
-
MGraph = self.new_graph()
|
17
|
-
if x >0 and y > 0 :
|
18
|
-
for i in range(x):
|
19
|
-
MGraph.add_node(label=f'node_{i}')
|
20
|
-
for i in range(y):
|
21
|
-
from_node_id = random_int(max=x) - 1
|
22
|
-
to_node_id = random_int(max=x) - 1
|
23
|
-
from_node = MGraph.nodes[from_node_id]
|
24
|
-
to_node = MGraph.nodes[to_node_id ]
|
25
|
-
MGraph.add_edge(from_node=from_node, to_node=to_node)
|
26
|
-
|
27
|
-
return MGraph
|
@@ -1,42 +0,0 @@
|
|
1
|
-
from enum import Enum, auto
|
2
|
-
from osbot_utils.utils.Str import safe_str
|
3
|
-
from osbot_utils.helpers.Local_Cache import Local_Cache
|
4
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
5
|
-
from osbot_utils.graphs.mgraph.MGraph import MGraph
|
6
|
-
|
7
|
-
|
8
|
-
class Serialization_Mode(Enum):
|
9
|
-
JSON = auto()
|
10
|
-
PICKLE = auto()
|
11
|
-
|
12
|
-
class MGraph__Serializer(Kwargs_To_Self):
|
13
|
-
|
14
|
-
caches_name : str = 'mgraph_tests'
|
15
|
-
mode : Serialization_Mode = Serialization_Mode.PICKLE
|
16
|
-
|
17
|
-
local_cache : Local_Cache # todo, refactor this into an MGraph__Storage__Disk class
|
18
|
-
key : str
|
19
|
-
mgraph : MGraph
|
20
|
-
|
21
|
-
def __init__(self, **kwargs):
|
22
|
-
super().__init__(**kwargs)
|
23
|
-
self.key = safe_str(f'serialiser_for__{self.mgraph.key}')
|
24
|
-
|
25
|
-
self.local_cache = Local_Cache(cache_name=self.key, caches_name=self.caches_name)
|
26
|
-
|
27
|
-
|
28
|
-
def save(self):
|
29
|
-
if self.mode == Serialization_Mode.JSON:
|
30
|
-
return self.save_to_json()
|
31
|
-
if self.mode == Serialization_Mode.PICKLE:
|
32
|
-
return self.save_to_pickle()
|
33
|
-
|
34
|
-
def save_to_json(self):
|
35
|
-
graph_data = self.mgraph.data().graph_data()
|
36
|
-
#pprint(graph_data)
|
37
|
-
self.local_cache.set('graph_data', graph_data)
|
38
|
-
return True
|
39
|
-
|
40
|
-
def save_to_pickle(self):
|
41
|
-
#obj_info(self.local_cache)
|
42
|
-
return '...pickle save - to be implemented...'
|
@@ -1,12 +0,0 @@
|
|
1
|
-
from osbot_utils.graphs.mgraph.MGraph__Random_Graphs import MGraph__Random_Graphs
|
2
|
-
|
3
|
-
class MGraphs:
|
4
|
-
|
5
|
-
def new__random(self, config=None, graph_key=None, x_nodes=10, y_edges=20):
|
6
|
-
return MGraph__Random_Graphs(config=config, graph_key=graph_key).with_x_nodes_and_y_edges(x=x_nodes, y=y_edges)
|
7
|
-
|
8
|
-
# todo : implement based on multiple save a load methods
|
9
|
-
# def load(self, file_path):
|
10
|
-
# if file_exists(file_path):
|
11
|
-
# if file_extension(file_path):
|
12
|
-
# return pickle_load_from_file(file_path)
|
File without changes
|
@@ -1,26 +0,0 @@
|
|
1
|
-
from osbot_utils.graphs.mermaid.Mermaid__Graph import Mermaid__Graph
|
2
|
-
from osbot_utils.utils.Dev import pprint
|
3
|
-
|
4
|
-
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
|
5
|
-
from osbot_utils.helpers.trace.Trace_Call import Trace_Call
|
6
|
-
|
7
|
-
# todo: reimplement this class when Mermaid__Graph has been updated to new version
|
8
|
-
class Trace_Call__Graph(Trace_Call):
|
9
|
-
|
10
|
-
def create(self):
|
11
|
-
mermaid_graph = Mermaid__Graph()
|
12
|
-
self.trace_call_handler.stack.root_node.func_name = 'trace_root'
|
13
|
-
for trace in self.trace_call_handler.traces():
|
14
|
-
node_key = trace.func_name
|
15
|
-
class_name = trace.module.split('.')[-1]
|
16
|
-
node_label = f'`**{trace.func_name}**\n*{class_name}*`'
|
17
|
-
mermaid_graph.add_node(key=node_key, label=node_label)
|
18
|
-
|
19
|
-
nodes__by_key = mermaid_graph.data().nodes__by_key()
|
20
|
-
|
21
|
-
for trace in self.trace_call_handler.traces():
|
22
|
-
from_node = nodes__by_key[trace.func_name]
|
23
|
-
for child in trace.children:
|
24
|
-
to_node = nodes__by_key[child.func_name]
|
25
|
-
mermaid_graph.add_edge(from_node=from_node, to_node=to_node)
|
26
|
-
return mermaid_graph
|
File without changes
|
File without changes
|