osbot-utils 1.89.0__py3-none-any.whl → 1.91.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 +20 -7
- osbot_utils/graphs/mermaid/Mermaid.py +7 -7
- osbot_utils/helpers/Zip_Bytes.py +2 -2
- osbot_utils/helpers/python_compatibility/__init__.py +0 -0
- osbot_utils/helpers/python_compatibility/python_3_8.py +8 -0
- osbot_utils/helpers/trace/Trace_Call__Handler.py +21 -21
- osbot_utils/helpers/type_safe/Type_Safe__Validator.py +14 -0
- osbot_utils/helpers/type_safe/__init__.py +0 -0
- osbot_utils/helpers/type_safe/validators/Validator__Max.py +28 -0
- osbot_utils/helpers/type_safe/validators/Validator__Min.py +38 -0
- osbot_utils/helpers/type_safe/validators/Validator__One_Of.py +18 -0
- osbot_utils/helpers/type_safe/validators/Validator__Regex.py +26 -0
- osbot_utils/helpers/type_safe/validators/__init__.py +0 -0
- osbot_utils/helpers/xml/Xml__Attribute.py +7 -0
- osbot_utils/helpers/xml/Xml__Element.py +15 -0
- osbot_utils/helpers/xml/Xml__File.py +9 -0
- osbot_utils/helpers/xml/Xml__File__Load.py +88 -0
- osbot_utils/helpers/xml/Xml__File__To_Dict.py +41 -0
- osbot_utils/helpers/xml/Xml__File__To_Xml.py +72 -0
- osbot_utils/helpers/xml/__init__.py +0 -0
- osbot_utils/helpers/xml/rss/RSS__Channel.py +17 -0
- osbot_utils/helpers/xml/rss/RSS__Enclosure.py +7 -0
- osbot_utils/helpers/xml/rss/RSS__Feed.py +11 -0
- osbot_utils/helpers/xml/rss/RSS__Feed__Parser.py +93 -0
- osbot_utils/helpers/xml/rss/RSS__Image.py +8 -0
- osbot_utils/helpers/xml/rss/RSS__Item.py +17 -0
- osbot_utils/utils/Objects.py +46 -10
- osbot_utils/utils/Regex.py +1 -1
- osbot_utils/version +1 -1
- {osbot_utils-1.89.0.dist-info → osbot_utils-1.91.0.dist-info}/METADATA +2 -2
- {osbot_utils-1.89.0.dist-info → osbot_utils-1.91.0.dist-info}/RECORD +33 -12
- osbot_utils/helpers/Xml_To_Dict.py +0 -87
- {osbot_utils-1.89.0.dist-info → osbot_utils-1.91.0.dist-info}/LICENSE +0 -0
- {osbot_utils-1.89.0.dist-info → osbot_utils-1.91.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
|
@@ -24,6 +24,7 @@ if sys.version_info < (3, 8): # pragma
|
|
24
24
|
return ()
|
25
25
|
else:
|
26
26
|
from typing import get_origin, get_args, ForwardRef
|
27
|
+
from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
|
27
28
|
|
28
29
|
if sys.version_info >= (3, 10):
|
29
30
|
NoneType = types.NoneType
|
@@ -44,7 +45,6 @@ class Type_Safe:
|
|
44
45
|
|
45
46
|
for (key, value) in self.__cls_kwargs__().items(): # assign all default values to self
|
46
47
|
if value is not None: # when the value is explicitly set to None on the class static vars, we can't check for type safety
|
47
|
-
|
48
48
|
raise_exception_on_obj_type_annotation_mismatch(self, key, value)
|
49
49
|
if hasattr(self, key):
|
50
50
|
existing_value = getattr(self, key)
|
@@ -65,10 +65,11 @@ class Type_Safe:
|
|
65
65
|
def __exit__(self, exc_type, exc_val, exc_tb): pass
|
66
66
|
|
67
67
|
def __setattr__(self, name, value):
|
68
|
-
from osbot_utils.utils.Objects
|
69
|
-
from osbot_utils.utils.Objects
|
70
|
-
from osbot_utils.utils.Objects
|
71
|
-
from osbot_utils.utils.Objects
|
68
|
+
from osbot_utils.utils.Objects import convert_dict_to_value_from_obj_annotation
|
69
|
+
from osbot_utils.utils.Objects import convert_to_value_from_obj_annotation
|
70
|
+
from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_attr
|
71
|
+
from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_union_and_annotated
|
72
|
+
from osbot_utils.helpers.type_safe.Type_Safe__Validator import Type_Safe__Validator
|
72
73
|
|
73
74
|
if not hasattr(self, '__annotations__'): # can't do type safety checks if the class does not have annotations
|
74
75
|
return super().__setattr__(name, value)
|
@@ -79,7 +80,7 @@ class Type_Safe:
|
|
79
80
|
if 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)
|
80
81
|
value = convert_to_value_from_obj_annotation (self, name, value)
|
81
82
|
check_1 = value_type_matches_obj_annotation_for_attr (self, name, value)
|
82
|
-
check_2 =
|
83
|
+
check_2 = value_type_matches_obj_annotation_for_union_and_annotated(self, name, value)
|
83
84
|
if (check_1 is False and check_2 is None or
|
84
85
|
check_1 is None and check_2 is False or
|
85
86
|
check_1 is False and check_2 is False ): # fix for type safety assigment on Union vars
|
@@ -89,6 +90,16 @@ class Type_Safe:
|
|
89
90
|
if getattr(self, name) is not None: # unless it is already set to None
|
90
91
|
raise ValueError(f"Can't set None, to a variable that is already set. Invalid type for attribute '{name}'. Expected '{self.__annotations__.get(name)}' but got '{type(value)}'")
|
91
92
|
|
93
|
+
# todo: refactor this to separate method
|
94
|
+
if hasattr(self.__annotations__, 'get'):
|
95
|
+
annotation = self.__annotations__.get(name)
|
96
|
+
if annotation and get_origin(annotation) is Annotated:
|
97
|
+
annotation_args = get_args(annotation)
|
98
|
+
target_type = annotation_args[0]
|
99
|
+
for attribute in annotation_args[1:]:
|
100
|
+
if isinstance(attribute, Type_Safe__Validator):
|
101
|
+
attribute.validate(value=value, field_name=name, target_type=target_type)
|
102
|
+
|
92
103
|
super().__setattr__(name, value)
|
93
104
|
|
94
105
|
def __attr_names__(self):
|
@@ -131,6 +142,8 @@ class Type_Safe:
|
|
131
142
|
else:
|
132
143
|
var_value = getattr(base_cls, var_name)
|
133
144
|
if var_value is not None: # allow None assignments on ctor since that is a valid use case
|
145
|
+
if get_origin(var_type) is Annotated:
|
146
|
+
continue
|
134
147
|
if var_type and not isinstance(var_value, var_type): # check type
|
135
148
|
exception_message = f"variable '{var_name}' is defined as type '{var_type}' but has value '{var_value}' of type '{type(var_value)}'"
|
136
149
|
raise ValueError(exception_message)
|
@@ -1,10 +1,10 @@
|
|
1
|
-
from osbot_utils.graphs.mermaid.Mermaid__Renderer
|
2
|
-
from osbot_utils.graphs.mermaid.Mermaid__Edge
|
3
|
-
from osbot_utils.graphs.mermaid.Mermaid__Graph
|
4
|
-
from osbot_utils.graphs.mermaid.models.Mermaid__Diagram_Direction
|
5
|
-
from osbot_utils.graphs.mermaid.models.Mermaid__Diagram__Type
|
6
|
-
from osbot_utils.utils.Python_Logger
|
7
|
-
from osbot_utils.base_classes.Kwargs_To_Self
|
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
8
|
|
9
9
|
class Mermaid(Kwargs_To_Self):
|
10
10
|
graph : Mermaid__Graph
|
osbot_utils/helpers/Zip_Bytes.py
CHANGED
@@ -2,10 +2,10 @@ from osbot_utils.base_classes.Type_Safe import Type_Safe
|
|
2
2
|
from osbot_utils.utils.Dev import pprint
|
3
3
|
from osbot_utils.utils.Files import files_list, file_create_from_bytes, temp_file, parent_folder, parent_folder_create
|
4
4
|
from osbot_utils.utils.Misc import random_text
|
5
|
-
from osbot_utils.utils.Regex import
|
5
|
+
from osbot_utils.utils.Regex import list__match_regexes
|
6
6
|
from osbot_utils.utils.Zip import zip_bytes_empty, zip_bytes__files, zip_bytes__add_file, zip_bytes__add_files, \
|
7
7
|
zip_bytes__replace_files, zip_bytes__replace_file, zip_bytes__file_list, zip_bytes__file, \
|
8
|
-
zip_bytes__add_file__from_disk, zip_bytes__add_files__from_disk,
|
8
|
+
zip_bytes__add_file__from_disk, zip_bytes__add_files__from_disk, zip_file__files, zip_bytes__remove_files
|
9
9
|
|
10
10
|
|
11
11
|
class Zip_Bytes(Type_Safe):
|
File without changes
|
@@ -8,28 +8,28 @@ from osbot_utils.helpers.trace.Trace_Call__Stack_Node import Trace_Call__Stack
|
|
8
8
|
from osbot_utils.helpers.trace.Trace_Call__Stats import Trace_Call__Stats
|
9
9
|
|
10
10
|
DEFAULT_ROOT_NODE_NODE_TITLE = 'Trace Session'
|
11
|
-
GLOBAL_FUNCTIONS_TO_IGNORE = ['value_type_matches_obj_annotation_for_attr'
|
12
|
-
'
|
13
|
-
'are_types_compatible_for_assigment'
|
14
|
-
'obj_attribute_annotation'
|
15
|
-
'get_origin'
|
16
|
-
'getmro'
|
17
|
-
'default_value'
|
18
|
-
'raise_exception_on_obj_type_annotation_mismatch'
|
19
|
-
'__cls_kwargs__'
|
20
|
-
'__default__value__'
|
21
|
-
'__setattr__'
|
11
|
+
GLOBAL_FUNCTIONS_TO_IGNORE = ['value_type_matches_obj_annotation_for_attr' , # these are type safety functions which introduce quite a lot of noise in the traces (and unless one is debugging type safety, they will not be needed)
|
12
|
+
'value_type_matches_obj_annotation_for_union_and_annotated' , # todo: map out and document why exactly these methods are ignore (and what is the side effect)
|
13
|
+
'are_types_compatible_for_assigment' ,
|
14
|
+
'obj_attribute_annotation' ,
|
15
|
+
'get_origin' ,
|
16
|
+
'getmro' ,
|
17
|
+
'default_value' ,
|
18
|
+
'raise_exception_on_obj_type_annotation_mismatch' ,
|
19
|
+
'__cls_kwargs__' ,
|
20
|
+
'__default__value__' ,
|
21
|
+
'__setattr__' ,
|
22
22
|
'<module>']
|
23
|
-
GLOBAL_MODULES_TO_IGNORE = ['osbot_utils.helpers.trace.Trace_Call'
|
24
|
-
'osbot_utils.helpers.trace.Trace_Call__Config'
|
25
|
-
'osbot_utils.helpers.trace.Trace_Call__View_Model'
|
26
|
-
'osbot_utils.helpers.trace.Trace_Call__Print_Traces'
|
27
|
-
'osbot_utils.helpers.trace.Trace_Call__Stack'
|
28
|
-
'osbot_utils.base_classes.Type_Safe'
|
29
|
-
'osbot_utils.helpers.CPrint'
|
30
|
-
'osbot_utils.helpers.Print_Table'
|
31
|
-
'osbot_utils.decorators.methods.cache_on_self'
|
32
|
-
'codecs'
|
23
|
+
GLOBAL_MODULES_TO_IGNORE = ['osbot_utils.helpers.trace.Trace_Call' , # todo: map out and document why exactly these modules are ignore (and what is the side effect)
|
24
|
+
'osbot_utils.helpers.trace.Trace_Call__Config' ,
|
25
|
+
'osbot_utils.helpers.trace.Trace_Call__View_Model' ,
|
26
|
+
'osbot_utils.helpers.trace.Trace_Call__Print_Traces' ,
|
27
|
+
'osbot_utils.helpers.trace.Trace_Call__Stack' ,
|
28
|
+
'osbot_utils.base_classes.Type_Safe' ,
|
29
|
+
'osbot_utils.helpers.CPrint' , # also see if this should be done here or at the print/view stage
|
30
|
+
'osbot_utils.helpers.Print_Table' ,
|
31
|
+
'osbot_utils.decorators.methods.cache_on_self' ,
|
32
|
+
'codecs' ]
|
33
33
|
|
34
34
|
#GLOBAL_MODULES_TO_IGNORE = []
|
35
35
|
#GLOBAL_FUNCTIONS_TO_IGNORE = []
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
class Type_Safe__Validator: # Base class for all Type_Safe validators.
|
5
|
+
|
6
|
+
def validate(self, value: Any, field_name: str, target_type: type) -> None: # Validate a value against this validator's rules.
|
7
|
+
if value and type(value) != target_type:
|
8
|
+
raise ValueError(f"{field_name} must be of type {target_type}, got {type(value)}")
|
9
|
+
|
10
|
+
def describe(self) -> str: # Return a human-readable description of this validator's rules.
|
11
|
+
pass
|
12
|
+
|
13
|
+
|
14
|
+
Validate = Annotated
|
File without changes
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from typing import Any
|
2
|
+
from osbot_utils.helpers.type_safe.Type_Safe__Validator import Type_Safe__Validator
|
3
|
+
|
4
|
+
|
5
|
+
class Validator__Max(Type_Safe__Validator): # Validates that a numeric value is at most the specified maximum."""
|
6
|
+
max_value: float
|
7
|
+
|
8
|
+
def __init__(self, max_value):
|
9
|
+
self.max_value = max_value
|
10
|
+
|
11
|
+
def validate(self, value: Any, field_name: str, target_type: type) -> None:
|
12
|
+
super().validate(value=value, field_name=field_name, target_type=target_type)
|
13
|
+
if value is None:
|
14
|
+
return
|
15
|
+
compare_value = value if isinstance(value, (int, float)) else len(value)
|
16
|
+
if compare_value > self.max_value:
|
17
|
+
if not isinstance(value, (int, float)):
|
18
|
+
msg = f"{field_name} must be at most {self.max_value}, got {compare_value}"
|
19
|
+
elif isinstance(value, (list, tuple)):
|
20
|
+
msg = f"{field_name} must be at most {self.max_value}, got length {compare_value}"
|
21
|
+
else:
|
22
|
+
msg = f"{field_name} must be at most {self.max_value}, got size {compare_value}"
|
23
|
+
raise ValueError(msg)
|
24
|
+
|
25
|
+
def describe(self) -> str:
|
26
|
+
return f"maximum value: {self.max_value}"
|
27
|
+
|
28
|
+
Max = Validator__Max
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from typing import Any
|
2
|
+
from osbot_utils.helpers.type_safe.Type_Safe__Validator import Type_Safe__Validator
|
3
|
+
|
4
|
+
class Validator__Min(Type_Safe__Validator): # Validates that a value is at least the specified minimum. Works with any type that supports the < operator (numbers, strings, lists, etc.)
|
5
|
+
min_value: Any
|
6
|
+
|
7
|
+
def __init__(self, min_value):
|
8
|
+
super().__init__()
|
9
|
+
self.min_value = min_value
|
10
|
+
|
11
|
+
def validate(self, value: Any, field_name: str, target_type: type) -> None:
|
12
|
+
super().validate(value=value, field_name=field_name, target_type=target_type)
|
13
|
+
if value is None: # can't compare if value has been set to None
|
14
|
+
return
|
15
|
+
|
16
|
+
try:
|
17
|
+
compare_value = value if isinstance(value, (int, float)) else len(value)
|
18
|
+
|
19
|
+
if compare_value < self.min_value:
|
20
|
+
if isinstance(value, (int, float)):
|
21
|
+
msg = f"{field_name} must be at least {self.min_value}, got {compare_value}"
|
22
|
+
elif isinstance(value, str):
|
23
|
+
msg = f"{field_name} must have length at least {self.min_value}, got length {compare_value}"
|
24
|
+
elif isinstance(value, (list, tuple)):
|
25
|
+
msg = f"{field_name} must have length at least {self.min_value}, got length {compare_value}"
|
26
|
+
else:
|
27
|
+
msg = f"{field_name} must have size at least {self.min_value}, got size {compare_value}"
|
28
|
+
raise ValueError(msg)
|
29
|
+
except TypeError:
|
30
|
+
raise ValueError(f"Cannot compare {field_name} of type {type(value)} with minimum value of type {type(self.min_value)}")
|
31
|
+
|
32
|
+
def describe(self) -> str:
|
33
|
+
if isinstance(self.min_value, (int, float)):
|
34
|
+
return f"minimum value: {self.min_value}"
|
35
|
+
else:
|
36
|
+
return f"minimum length: {len(self.min_value)}"
|
37
|
+
|
38
|
+
Min = Validator__Min
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import Any
|
2
|
+
from osbot_utils.helpers.type_safe.Type_Safe__Validator import Type_Safe__Validator
|
3
|
+
|
4
|
+
|
5
|
+
class Validator__One_Of(Type_Safe__Validator): # Validates that a value is one of a set of allowed values."""
|
6
|
+
allowed: list
|
7
|
+
|
8
|
+
def __init__(self, allowed):
|
9
|
+
self.allowed = allowed
|
10
|
+
|
11
|
+
def validate(self, value: Any, field_name: str, target_type:type) -> None:
|
12
|
+
if value not in self.allowed:
|
13
|
+
raise ValueError(f"{field_name} must be one of {self.allowed}, got {value}")
|
14
|
+
|
15
|
+
def describe(self) -> str:
|
16
|
+
return f"must be one of: {self.allowed}"
|
17
|
+
|
18
|
+
One_Of = Validator__One_Of
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from typing import Any
|
2
|
+
from osbot_utils.helpers.type_safe.Type_Safe__Validator import Type_Safe__Validator
|
3
|
+
|
4
|
+
class Validator__Regex(Type_Safe__Validator): # Validates that a string matches the specified regex pattern.
|
5
|
+
pattern : str
|
6
|
+
description: str
|
7
|
+
|
8
|
+
def __init__(self, pattern, description=None):
|
9
|
+
self.pattern = pattern
|
10
|
+
self.description = description
|
11
|
+
|
12
|
+
def validate(self, value: Any, field_name: str, target_type:type) -> None:
|
13
|
+
import re
|
14
|
+
if value is None:
|
15
|
+
return
|
16
|
+
if not isinstance(value, str):
|
17
|
+
raise ValueError(f"{field_name} must be a string, got {type(value)}")
|
18
|
+
if not re.match(self.pattern, value):
|
19
|
+
raise ValueError(f"{field_name} must match pattern {self.pattern}")
|
20
|
+
|
21
|
+
def describe(self) -> str:
|
22
|
+
if self.description:
|
23
|
+
return self.description
|
24
|
+
return f"must match pattern: {self.pattern}"
|
25
|
+
|
26
|
+
Regex = Validator__Regex
|
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from typing import Dict, List, Union
|
2
|
+
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
3
|
+
from osbot_utils.helpers.xml.Xml__Attribute import Xml__Attribute
|
4
|
+
|
5
|
+
class XML__Element(Type_Safe):
|
6
|
+
tag : str # Element's local name
|
7
|
+
namespace : str # Element's namespace URI
|
8
|
+
namespace_prefix: str # Element's namespace prefix
|
9
|
+
attributes : Dict[str, Xml__Attribute] # Element attributes
|
10
|
+
children : List[Union[str, 'XML__Element']] # Child elements/text
|
11
|
+
|
12
|
+
# def qualified_name(self) -> str: # Get fully qualified name with prefix
|
13
|
+
# if self.namespace_prefix:
|
14
|
+
# return f"{self.namespace_prefix}:{self.tag}"
|
15
|
+
# return self.tag
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from typing import Dict
|
2
|
+
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
3
|
+
from osbot_utils.helpers.xml.Xml__Element import XML__Element
|
4
|
+
|
5
|
+
class Xml__File(Type_Safe):
|
6
|
+
xml_data : str # Raw XML content
|
7
|
+
root_element: XML__Element # Parsed root element
|
8
|
+
namespaces : Dict[str, str] # XML namespace mappings
|
9
|
+
|
@@ -0,0 +1,88 @@
|
|
1
|
+
from io import StringIO
|
2
|
+
from typing import List, Union, Dict
|
3
|
+
from xml.etree.ElementTree import iterparse, Element, fromstring, ParseError
|
4
|
+
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
5
|
+
from osbot_utils.helpers.xml.Xml__Attribute import Xml__Attribute
|
6
|
+
from osbot_utils.helpers.xml.Xml__Element import XML__Element
|
7
|
+
from osbot_utils.helpers.xml.Xml__File import Xml__File
|
8
|
+
|
9
|
+
|
10
|
+
class Xml__File__Load(Type_Safe):
|
11
|
+
|
12
|
+
def load_from_string(self, xml_data: str) -> Xml__File: # Create Xml__File from string
|
13
|
+
xml_file = Xml__File(xml_data=xml_data)
|
14
|
+
self.load_namespaces(xml_file)
|
15
|
+
self.parse_xml (xml_file)
|
16
|
+
return xml_file
|
17
|
+
|
18
|
+
def load_namespaces(self, xml_file: Xml__File): # Extract namespaces from XML
|
19
|
+
if not xml_file.xml_data:
|
20
|
+
raise ValueError("XML data cannot be empty")
|
21
|
+
|
22
|
+
for event, elem in iterparse(StringIO(xml_file.xml_data), events=("start-ns",)):
|
23
|
+
prefix, uri = elem
|
24
|
+
xml_file.namespaces[prefix] = uri
|
25
|
+
|
26
|
+
def parse_xml(self, xml_file: Xml__File): # Parse XML into type-safe structure
|
27
|
+
if not xml_file.xml_data:
|
28
|
+
raise ValueError("XML data cannot be empty")
|
29
|
+
|
30
|
+
try:
|
31
|
+
root = fromstring(xml_file.xml_data)
|
32
|
+
namespaces = xml_file.namespaces
|
33
|
+
xml_file.root_element = self.convert_element(namespaces,root)
|
34
|
+
except ParseError as error:
|
35
|
+
raise ValueError(f"Invalid XML: {str(error)}")
|
36
|
+
|
37
|
+
def convert_element(self, namespaces: Dict[str,str], element: Element) -> XML__Element:
|
38
|
+
attributes = self.convert_attributes(element)
|
39
|
+
children: List[Union[str, XML__Element]] = []
|
40
|
+
|
41
|
+
tag = element.tag
|
42
|
+
namespace = ''
|
43
|
+
namespace_prefix = ''
|
44
|
+
|
45
|
+
if '}' in tag:
|
46
|
+
namespace, tag = tag.split('}', 1) # Split namespace and tag
|
47
|
+
namespace = namespace[1:] # Remove the '{' prefix
|
48
|
+
|
49
|
+
for prefix, uri in namespaces.items(): # Find prefix for this namespace
|
50
|
+
if uri == namespace:
|
51
|
+
namespace_prefix = prefix
|
52
|
+
break
|
53
|
+
|
54
|
+
# Handle text content
|
55
|
+
if element.text and element.text.strip():
|
56
|
+
children.append(element.text.strip())
|
57
|
+
|
58
|
+
# Process child elements
|
59
|
+
for child in element:
|
60
|
+
child_element = self.convert_element(namespaces, child)
|
61
|
+
children.append(child_element)
|
62
|
+
|
63
|
+
if child.tail and child.tail.strip():
|
64
|
+
children.append(child.tail.strip())
|
65
|
+
|
66
|
+
return XML__Element(tag=tag,
|
67
|
+
namespace=namespace,
|
68
|
+
namespace_prefix=namespace_prefix,
|
69
|
+
attributes=attributes,
|
70
|
+
children=children)
|
71
|
+
|
72
|
+
def convert_attributes(self, element: Element) -> Dict[str, Xml__Attribute]: # Convert element attributes
|
73
|
+
attributes = {}
|
74
|
+
for key, value in element.attrib.items():
|
75
|
+
if '}' in key: # Handle namespaced attributes
|
76
|
+
namespace, name = key.split('}', 1)
|
77
|
+
namespace = namespace[1:] # Remove the '{' prefix
|
78
|
+
else:
|
79
|
+
namespace = ''
|
80
|
+
name = key
|
81
|
+
|
82
|
+
attribute = Xml__Attribute(
|
83
|
+
name=name,
|
84
|
+
value=value,
|
85
|
+
namespace=namespace
|
86
|
+
)
|
87
|
+
attributes[key] = attribute
|
88
|
+
return attributes
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from typing import Dict, Any
|
2
|
+
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
3
|
+
from osbot_utils.helpers.xml.Xml__Element import XML__Element
|
4
|
+
from osbot_utils.helpers.xml.Xml__File import Xml__File
|
5
|
+
|
6
|
+
|
7
|
+
class Xml__File__To_Dict(Type_Safe):
|
8
|
+
def to_dict(self, xml_file: Xml__File) -> Dict[str, Any]: # Convert Xml__File to dictionary
|
9
|
+
if not xml_file.root_element:
|
10
|
+
return {}
|
11
|
+
return self.element_to_dict(xml_file.root_element)
|
12
|
+
|
13
|
+
def element_to_dict(self, element: XML__Element) -> Dict[str, Any]: # Convert XML__Element to dictionary
|
14
|
+
result = {}
|
15
|
+
|
16
|
+
for key, attr in element.attributes.items(): # Convert attributes first
|
17
|
+
result[key] = attr.value
|
18
|
+
|
19
|
+
|
20
|
+
child_nodes: Dict[str, Any] = {} # Process children and collect text content
|
21
|
+
text_content = []
|
22
|
+
for child in element.children:
|
23
|
+
if isinstance(child, str):
|
24
|
+
text_content.append(child)
|
25
|
+
else:
|
26
|
+
if child.tag in child_nodes: # Handle child elements
|
27
|
+
if not isinstance(child_nodes[child.tag], list):
|
28
|
+
child_nodes[child.tag] = [child_nodes[child.tag]]
|
29
|
+
child_nodes[child.tag].append(self.element_to_dict(child))
|
30
|
+
else:
|
31
|
+
child_nodes[child.tag] = self.element_to_dict(child)
|
32
|
+
|
33
|
+
if text_content: # Handle text content
|
34
|
+
text_value = ' '.join(text_content)
|
35
|
+
if child_nodes or result: # If we have attributes or child nodes
|
36
|
+
result['#text'] = text_value # Add text as a special key
|
37
|
+
else:
|
38
|
+
return text_value # Return just the text if no attributes/children
|
39
|
+
|
40
|
+
result.update(child_nodes) # Merge child nodes into result
|
41
|
+
return result
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from xml.etree.ElementTree import Element, SubElement, tostring
|
3
|
+
from xml.dom import minidom
|
4
|
+
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
5
|
+
from osbot_utils.helpers.xml.Xml__Element import XML__Element
|
6
|
+
from osbot_utils.helpers.xml.Xml__File import Xml__File
|
7
|
+
|
8
|
+
class Xml__File__To_Xml(Type_Safe):
|
9
|
+
|
10
|
+
def convert_to_xml(self, xml_file: Xml__File, pretty_print: bool = True) -> str:
|
11
|
+
if not xml_file.root_element:
|
12
|
+
raise ValueError("XML file must have a root element")
|
13
|
+
|
14
|
+
# Create XML element tree
|
15
|
+
root = self.create_element(xml_file.root_element, xml_file.namespaces)
|
16
|
+
|
17
|
+
# Add all namespace declarations to root element
|
18
|
+
for prefix, uri in xml_file.namespaces.items():
|
19
|
+
if prefix == '':
|
20
|
+
root.set('xmlns', uri)
|
21
|
+
else:
|
22
|
+
root.set(f'xmlns:{prefix}', uri)
|
23
|
+
|
24
|
+
# Convert to string
|
25
|
+
xml_string = tostring(root, encoding='unicode', method='xml')
|
26
|
+
|
27
|
+
# Pretty print if requested
|
28
|
+
if pretty_print:
|
29
|
+
return self.pretty_print(xml_string)
|
30
|
+
return xml_string
|
31
|
+
|
32
|
+
def create_element(self, xml_element: XML__Element, namespaces: Optional[dict] = None, parent: Optional[Element] = None) -> Element:
|
33
|
+
# Create tag with namespace if applicable
|
34
|
+
tag = xml_element.tag
|
35
|
+
if xml_element.namespace_prefix:
|
36
|
+
tag = f"{xml_element.namespace_prefix}:{tag}"
|
37
|
+
# Don't add namespace URI for default namespace - let it be inherited
|
38
|
+
|
39
|
+
# Create new element or sub-element
|
40
|
+
if parent is not None:
|
41
|
+
element = SubElement(parent, tag)
|
42
|
+
else:
|
43
|
+
element = Element(tag)
|
44
|
+
|
45
|
+
# Add attributes including namespace declarations
|
46
|
+
for attr_key, attr in xml_element.attributes.items():
|
47
|
+
if attr.namespace:
|
48
|
+
attr_name = f"{{{attr.namespace}}}{attr.name}"
|
49
|
+
else:
|
50
|
+
attr_name = attr.name
|
51
|
+
element.set(attr_name, attr.value)
|
52
|
+
|
53
|
+
# Process children
|
54
|
+
text_parts = []
|
55
|
+
for child in xml_element.children:
|
56
|
+
if isinstance(child, str):
|
57
|
+
text_parts.append(child)
|
58
|
+
elif isinstance(child, XML__Element):
|
59
|
+
self.create_element(child, namespaces, element)
|
60
|
+
|
61
|
+
# Set text content if any
|
62
|
+
if text_parts:
|
63
|
+
element.text = ''.join(text_parts)
|
64
|
+
|
65
|
+
return element
|
66
|
+
|
67
|
+
def pretty_print(self, xml_string: str) -> str:
|
68
|
+
"""Format XML string with proper indentation."""
|
69
|
+
parsed = minidom.parseString(xml_string)
|
70
|
+
pretty_xml = parsed.toprettyxml(indent=' ').rstrip() + '\n'
|
71
|
+
# Remove empty lines (common issue with toprettyxml)
|
72
|
+
return '\n'.join(line for line in pretty_xml.split('\n') if line.strip())
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from typing import Any, Dict, List
|
2
|
+
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
3
|
+
from osbot_utils.helpers.xml.rss.RSS__Image import RSS__Image
|
4
|
+
from osbot_utils.helpers.xml.rss.RSS__Item import RSS__Item
|
5
|
+
|
6
|
+
|
7
|
+
class RSS__Channel(Type_Safe):
|
8
|
+
description : str
|
9
|
+
extensions : Dict[str, Any]
|
10
|
+
image : RSS__Image = None
|
11
|
+
items : List[RSS__Item]
|
12
|
+
language : str
|
13
|
+
last_build_date : str
|
14
|
+
link : str
|
15
|
+
title : str
|
16
|
+
update_frequency: str
|
17
|
+
update_period : str
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from typing import Dict, Any
|
2
|
+
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
3
|
+
from osbot_utils.helpers.xml.rss.RSS__Channel import RSS__Channel
|
4
|
+
|
5
|
+
DEFAULT__RSS_FEED__VERSION = "2.0"
|
6
|
+
|
7
|
+
class RSS__Feed(Type_Safe):
|
8
|
+
version : str = DEFAULT__RSS_FEED__VERSION
|
9
|
+
channel : RSS__Channel = None
|
10
|
+
namespaces : Dict[str, str]
|
11
|
+
extensions : Dict[str, Any]
|
@@ -0,0 +1,93 @@
|
|
1
|
+
from typing import Dict, Any
|
2
|
+
from osbot_utils.helpers.Guid import Guid
|
3
|
+
from osbot_utils.helpers.xml.rss.RSS__Channel import RSS__Channel
|
4
|
+
from osbot_utils.helpers.xml.rss.RSS__Feed import RSS__Feed
|
5
|
+
from osbot_utils.helpers.xml.rss.RSS__Image import RSS__Image
|
6
|
+
from osbot_utils.helpers.xml.rss.RSS__Item import RSS__Item
|
7
|
+
|
8
|
+
|
9
|
+
class RSS__Feed__Parser:
|
10
|
+
|
11
|
+
def from_dict(self, data: Dict[str, Any]) -> RSS__Feed: # Convert a dictionary (from XML) into an RSS__Feed object
|
12
|
+
if 'channel' not in data:
|
13
|
+
raise ValueError("Invalid RSS feed: no channel element found")
|
14
|
+
|
15
|
+
channel_data = data['channel']
|
16
|
+
rss_items = []
|
17
|
+
items = channel_data.get('item', []) # get raw items data
|
18
|
+
if type(items) is not list: # handle case when only one item is present
|
19
|
+
items = [items] # by converting it to a list
|
20
|
+
for item_data in items: # Process items
|
21
|
+
|
22
|
+
title = self.element_text(item_data.get('title' ))
|
23
|
+
link = self.element_text(item_data.get('link' ))
|
24
|
+
description = self.element_text(item_data.get('description'))
|
25
|
+
guid = self.extract_guid(item_data.get('guid' ))
|
26
|
+
pubDate = self.element_text(item_data.get('pubDate' ))
|
27
|
+
creator = self.element_text(item_data.get('creator' ))
|
28
|
+
rss_item = RSS__Item(title = title ,
|
29
|
+
link = link ,
|
30
|
+
description = description ,
|
31
|
+
guid = guid ,
|
32
|
+
pubDate = pubDate ,
|
33
|
+
creator = creator ,
|
34
|
+
categories = item_data.get('category' , []),
|
35
|
+
content = item_data.get('content' , {}),
|
36
|
+
thumbnail = item_data.get('thumbnail' , {}))
|
37
|
+
|
38
|
+
|
39
|
+
known_fields = {'title', 'link', 'description', 'guid', 'pubDate', # Move non-standard elements to extensions
|
40
|
+
'creator', 'category', 'content', 'thumbnail'}
|
41
|
+
rss_item.extensions = {k: v for k, v in item_data.items()
|
42
|
+
if k not in known_fields}
|
43
|
+
rss_items.append(rss_item)
|
44
|
+
|
45
|
+
# Create channel
|
46
|
+
link = self.element_text(channel_data.get('link'))
|
47
|
+
channel = RSS__Channel( title = channel_data.get('title' , ''),
|
48
|
+
link = link ,
|
49
|
+
description = channel_data.get('description' , ''),
|
50
|
+
language = channel_data.get('language' , ''),
|
51
|
+
last_build_date = channel_data.get('lastBuildDate' ),
|
52
|
+
items = rss_items ,
|
53
|
+
update_frequency = channel_data.get('updateFrequency', ''),
|
54
|
+
update_period = channel_data.get('updatePeriod' , ''))
|
55
|
+
|
56
|
+
|
57
|
+
# Process channel image if present
|
58
|
+
if 'image' in channel_data:
|
59
|
+
img_data = channel_data['image']
|
60
|
+
channel.image = RSS__Image( url = img_data.get('url' , '' ),
|
61
|
+
title = img_data.get('title' , '' ),
|
62
|
+
link = img_data.get('link' , '' ),
|
63
|
+
width = int(img_data.get('width' , 0 )),
|
64
|
+
height = int(img_data.get('height', 0 )))
|
65
|
+
|
66
|
+
known_channel_fields = {'title', 'link', 'description', 'language', # Move non-standard channel elements to extensions
|
67
|
+
'lastBuildDate', 'image', 'item', 'updateFrequency', 'updatePeriod'}
|
68
|
+
channel.extensions = {k: v for k, v in channel_data.items()
|
69
|
+
if k not in known_channel_fields}
|
70
|
+
|
71
|
+
rss_feed = RSS__Feed(channel = channel)
|
72
|
+
return rss_feed
|
73
|
+
|
74
|
+
def extract_guid(self, target):
|
75
|
+
return Guid(self.extract_text(target))
|
76
|
+
|
77
|
+
def element_text(self, target):
|
78
|
+
if isinstance(target, list):
|
79
|
+
for item in target:
|
80
|
+
value = self.extract_text(item)
|
81
|
+
if value:
|
82
|
+
return value
|
83
|
+
value = self.extract_text(target)
|
84
|
+
return value or f'{target}'
|
85
|
+
|
86
|
+
def extract_text(self, target):
|
87
|
+
if not target:
|
88
|
+
return ''
|
89
|
+
if isinstance(target, str):
|
90
|
+
return target
|
91
|
+
if isinstance(target, dict):
|
92
|
+
return target.get('#text', '')
|
93
|
+
return ''
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from typing import Dict, List, Any
|
2
|
+
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
3
|
+
from osbot_utils.helpers.Guid import Guid
|
4
|
+
from osbot_utils.helpers.xml.rss.RSS__Enclosure import RSS__Enclosure
|
5
|
+
|
6
|
+
class RSS__Item(Type_Safe):
|
7
|
+
title : str
|
8
|
+
link : str
|
9
|
+
description : str
|
10
|
+
guid : Guid
|
11
|
+
pubDate : str
|
12
|
+
creator : str
|
13
|
+
categories : List[str]
|
14
|
+
enclosure : RSS__Enclosure = None
|
15
|
+
content : Dict[str, Any]
|
16
|
+
thumbnail : Dict[str, Any]
|
17
|
+
extensions : Dict[str, Any]
|
osbot_utils/utils/Objects.py
CHANGED
@@ -23,7 +23,7 @@ if sys.version_info < (3, 8):
|
|
23
23
|
else:
|
24
24
|
return ()
|
25
25
|
else:
|
26
|
-
from typing import get_origin, get_args
|
26
|
+
from typing import get_origin, get_args, List, Tuple, Dict
|
27
27
|
|
28
28
|
|
29
29
|
def are_types_compatible_for_assigment(source_type, target_type):
|
@@ -343,11 +343,10 @@ def obj_values(target=None):
|
|
343
343
|
return list(obj_dict(target).values())
|
344
344
|
|
345
345
|
def raise_exception_on_obj_type_annotation_mismatch(target, attr_name, value):
|
346
|
-
# todo : check if this is is not causing the type safety issues
|
347
346
|
if value_type_matches_obj_annotation_for_attr(target, attr_name, value) is False: # handle case with normal types
|
348
|
-
if
|
347
|
+
if value_type_matches_obj_annotation_for_union_and_annotated(target, attr_name, value) is True: # handle union cases
|
349
348
|
return # this is done like this because value_type_matches_obj_annotation_for_union_attr will return None when there is no Union objects
|
350
|
-
raise
|
349
|
+
raise TypeError(f"Invalid type for attribute '{attr_name}'. Expected '{target.__annotations__.get(attr_name)}' but got '{type(value)}'")
|
351
350
|
|
352
351
|
def obj_attribute_annotation(target, attr_name):
|
353
352
|
if target is not None and attr_name is not None:
|
@@ -382,15 +381,52 @@ def obj_is_type_union_compatible(var_type, compatible_types):
|
|
382
381
|
return True # If all args are compatible, return True
|
383
382
|
return var_type in compatible_types or var_type is type(None) # Check for direct compatibility or type(None) for non-Union types
|
384
383
|
|
385
|
-
|
386
|
-
|
384
|
+
|
385
|
+
def value_type_matches_obj_annotation_for_union_and_annotated(target, attr_name, value):
|
386
|
+
from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
|
387
|
+
from typing import Union, get_origin, get_args
|
388
|
+
|
387
389
|
value_type = type(value)
|
388
|
-
attribute_annotation = obj_attribute_annotation(target,attr_name)
|
390
|
+
attribute_annotation = obj_attribute_annotation(target, attr_name)
|
389
391
|
origin = get_origin(attribute_annotation)
|
390
|
-
|
391
|
-
|
392
|
+
|
393
|
+
if origin is Union: # Handle Union types (including Optional)
|
394
|
+
args = get_args(attribute_annotation)
|
392
395
|
return value_type in args
|
393
|
-
|
396
|
+
|
397
|
+
# todo: refactor the logic below to a separate method (and check for duplicate code with other get_origin usage)
|
398
|
+
if origin is Annotated: # Handle Annotated types
|
399
|
+
args = get_args(attribute_annotation)
|
400
|
+
base_type = args[0] # First argument is the base type
|
401
|
+
base_origin = get_origin(base_type)
|
402
|
+
|
403
|
+
if base_origin is None: # Non-container types
|
404
|
+
return isinstance(value, base_type)
|
405
|
+
|
406
|
+
if base_origin in (list, List): # Handle List types
|
407
|
+
if not isinstance(value, list):
|
408
|
+
return False
|
409
|
+
item_type = get_args(base_type)[0]
|
410
|
+
return all(isinstance(item, item_type) for item in value)
|
411
|
+
|
412
|
+
if base_origin in (tuple, Tuple): # Handle Tuple types
|
413
|
+
if not isinstance(value, tuple):
|
414
|
+
return False
|
415
|
+
item_types = get_args(base_type)
|
416
|
+
return len(value) == len(item_types) and all(
|
417
|
+
isinstance(item, item_type)
|
418
|
+
for item, item_type in zip(value, item_types)
|
419
|
+
)
|
420
|
+
|
421
|
+
if base_origin in (dict, Dict): # Handle Dict types
|
422
|
+
if not isinstance(value, dict):
|
423
|
+
return False
|
424
|
+
key_type, value_type = get_args(base_type)
|
425
|
+
return all(isinstance(k, key_type) and isinstance(v, value_type)
|
426
|
+
for k, v in value.items())
|
427
|
+
|
428
|
+
# todo: add support for for other typing constructs
|
429
|
+
return None # if it is not a Union or Annotated types just return None (to give an indication to the caller that the comparison was not made)
|
394
430
|
|
395
431
|
|
396
432
|
def pickle_save_to_bytes(target: object) -> bytes:
|
osbot_utils/utils/Regex.py
CHANGED
osbot_utils/version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
v1.
|
1
|
+
v1.91.0
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: osbot_utils
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.91.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=JT_lzMkdt7zmLFAPProbgk30J3iBjLgpavGOZ7XwvXY,25611
|
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
|
@@ -38,7 +38,7 @@ osbot_utils/fluent/Fluent_Dict.py,sha256=nZ2z91s39sU2a-TLYpBirRoWgDXHrs0tQ9Bi_Zd
|
|
38
38
|
osbot_utils/fluent/Fluent_List.py,sha256=PfDDC9sm16CFnNQ8gkhCEsmKcZp8iyQ0YBpSHvYssG8,1089
|
39
39
|
osbot_utils/fluent/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
40
40
|
osbot_utils/graphs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
|
-
osbot_utils/graphs/mermaid/Mermaid.py,sha256=
|
41
|
+
osbot_utils/graphs/mermaid/Mermaid.py,sha256=G7--iIKm2C1z-tEB1qLNopwoW3_w4oR7Oq7-yA460mM,3164
|
42
42
|
osbot_utils/graphs/mermaid/Mermaid__Edge.py,sha256=jwHxHJEAA49aO28T8nnJFxOfpWAZZaWKNT_krG1fwkQ,1893
|
43
43
|
osbot_utils/graphs/mermaid/Mermaid__Graph.py,sha256=FRw17efZrdcKyXDKsyb1C8nswIAmljiUAyiF0FHIL4M,2854
|
44
44
|
osbot_utils/graphs/mermaid/Mermaid__Node.py,sha256=j_AVfR3hnKAJH2Z3d17djvU7MfQP8B70Lh7Jv6y0tTs,3322
|
@@ -77,8 +77,7 @@ osbot_utils/helpers/Str_ASCII.py,sha256=PRqyu449XnKrLn6b9Miii1Hv-GO5OAa1UhhgvlRc
|
|
77
77
|
osbot_utils/helpers/Timestamp_Now.py,sha256=k3-SUGYx2jLTXvgZYeECqPRJhVxqWPmW7co1l6r12jk,438
|
78
78
|
osbot_utils/helpers/Type_Registry.py,sha256=Ajk3SyMSKDi2g9SJYUtTgg7PZkAgydaHcpbGuEN3S94,311
|
79
79
|
osbot_utils/helpers/Type_Safe_Method.py,sha256=8E88of__An9_ZhJKz6Kp22C1mb9WLED0jWNLOII3fJs,10489
|
80
|
-
osbot_utils/helpers/
|
81
|
-
osbot_utils/helpers/Zip_Bytes.py,sha256=KB5zWfCf6ET4alNfqNrSp5DxZ3Jp9oDHpc6tK2iO_qg,4320
|
80
|
+
osbot_utils/helpers/Zip_Bytes.py,sha256=6i6uD1TIcMZZRM8BYMseSnihRxUI2vJiai3UnPHrGrY,4290
|
82
81
|
osbot_utils/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
83
82
|
osbot_utils/helpers/ast/Ast.py,sha256=lcPQOSxXI6zgmMnIVF9WM6ISqViWX-sq4d_UC0CDG8s,1155
|
84
83
|
osbot_utils/helpers/ast/Ast_Base.py,sha256=5rHMupBlN_n6lOC31UnSW_lWqxqxaE31v0gn-t32OgQ,3708
|
@@ -211,6 +210,8 @@ osbot_utils/helpers/pubsub/schemas/Schema__Event__Leave_Room.py,sha256=cNugRz-k_
|
|
211
210
|
osbot_utils/helpers/pubsub/schemas/Schema__Event__Message.py,sha256=rt8W-DGitmR-SvmunSG8kbTH_mubE2PKMh2cJ3eJOyo,244
|
212
211
|
osbot_utils/helpers/pubsub/schemas/Schema__PubSub__Client.py,sha256=yOQSn4o1bIsEoyhnQJYen372so89Ben1wMWUO12G4h8,239
|
213
212
|
osbot_utils/helpers/pubsub/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
213
|
+
osbot_utils/helpers/python_compatibility/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
214
|
+
osbot_utils/helpers/python_compatibility/python_3_8.py,sha256=kh846vs3ir8xD0RSARJBOL0xufnt3L_Td3K45lDfqng,161
|
214
215
|
osbot_utils/helpers/sqlite/Capture_Sqlite_Error.py,sha256=GSuRYgs1yKQjxMszPoaI7fsfMfuUqhb64AaIysRE6Cs,1747
|
215
216
|
osbot_utils/helpers/sqlite/Sqlite__Cursor.py,sha256=k5G9Tkk3nx6nHoSanLmpuJG_TceAmN7uRBCt0bo6sIc,3364
|
216
217
|
osbot_utils/helpers/sqlite/Sqlite__Database.py,sha256=YSuppFFiR-RcGiAXxhS-Oygw5PvdbV94wm4ybQ7cOF8,5616
|
@@ -261,7 +262,7 @@ osbot_utils/helpers/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
261
262
|
osbot_utils/helpers/trace/Trace_Call.py,sha256=O_y6cncgneYrj3ARDMz-o9Yi1LjsESibUqkGFAg0Jk0,8886
|
262
263
|
osbot_utils/helpers/trace/Trace_Call__Config.py,sha256=UAjdsDEqsPBBsu-h4_QKYL4UMukJmQYBBWGYTmSKS40,3361
|
263
264
|
osbot_utils/helpers/trace/Trace_Call__Graph.py,sha256=HCrXRKQI42DIQxxyFLcaosWiOcUyoITbeV17ICdXcXM,1156
|
264
|
-
osbot_utils/helpers/trace/Trace_Call__Handler.py,sha256=
|
265
|
+
osbot_utils/helpers/trace/Trace_Call__Handler.py,sha256=9EQyIiGB6r3UURyNgEXHPOCmZNOejM0UJNW9c_MjpNU,12650
|
265
266
|
osbot_utils/helpers/trace/Trace_Call__Print_Lines.py,sha256=cy7zLv0_JNxdOIQPfZk6J9bv6AkIW6O643w0ykClXbw,4820
|
266
267
|
osbot_utils/helpers/trace/Trace_Call__Print_Traces.py,sha256=2LGeWMGP1uhSojGMmJmL3bH2B5LFIlfYEqEPNqoyKJw,8628
|
267
268
|
osbot_utils/helpers/trace/Trace_Call__Stack.py,sha256=pIvZ2yP4tymOQraUR2N5R-qlmg5QijyLxt85zmMajUs,7462
|
@@ -270,6 +271,26 @@ osbot_utils/helpers/trace/Trace_Call__Stats.py,sha256=gmiotIrOXe2ssxodzQQ56t8eGT
|
|
270
271
|
osbot_utils/helpers/trace/Trace_Call__View_Model.py,sha256=a40nn6agCEMd2ecsJ93n8vXij0omh0D69QilqwmN_ao,4545
|
271
272
|
osbot_utils/helpers/trace/Trace_Files.py,sha256=SNpAmuBlSUS9NyVocgZ5vevzqVaIqoh622yZge3a53A,978
|
272
273
|
osbot_utils/helpers/trace/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
274
|
+
osbot_utils/helpers/type_safe/Type_Safe__Validator.py,sha256=cJIPSBarjV716SZUOLvz7Mthjk-aUYKUQtRDtKUBmN4,779
|
275
|
+
osbot_utils/helpers/type_safe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
276
|
+
osbot_utils/helpers/type_safe/validators/Validator__Max.py,sha256=UxrS-tDS5RqfV5aCSbgoGcKHC9VPGqeS_ACB1dqNpQw,1272
|
277
|
+
osbot_utils/helpers/type_safe/validators/Validator__Min.py,sha256=krNnQjt8XCS5smESvGsFHw2dJWmS9VKn6WXcY1xatkY,1978
|
278
|
+
osbot_utils/helpers/type_safe/validators/Validator__One_Of.py,sha256=AEknqmurAilHMOvbL6EBxAho2pz4llKPNeX0FHBPru8,678
|
279
|
+
osbot_utils/helpers/type_safe/validators/Validator__Regex.py,sha256=k6mZSDM1lISwCK-Atnuw5TTe1qx6DIf-DbzT4hL1Pu8,1005
|
280
|
+
osbot_utils/helpers/type_safe/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
281
|
+
osbot_utils/helpers/xml/Xml__Attribute.py,sha256=r29x8ehRug27KuHQKQEGl6Thz_MNaNHy6X3cPRgH4ho,148
|
282
|
+
osbot_utils/helpers/xml/Xml__Element.py,sha256=J0hMaJPIBNmZf4wo0xrtZBZd5vWie5FoX9XqtAXQToM,871
|
283
|
+
osbot_utils/helpers/xml/Xml__File.py,sha256=c3axXx07rIoK5tGzpLwH-X1B7nusSe1-SZAxJNZJ0KI,420
|
284
|
+
osbot_utils/helpers/xml/Xml__File__Load.py,sha256=iAH21HjrqFoZlU5-Kg2RE53RBdNs0mDaASETUTnbhRo,3625
|
285
|
+
osbot_utils/helpers/xml/Xml__File__To_Dict.py,sha256=1KGswrpMpUPu4JDYUbkP8GTOQfHGcGONnDYsi-_aqv0,2061
|
286
|
+
osbot_utils/helpers/xml/Xml__File__To_Xml.py,sha256=HvUbYOfLz6i160_to90dP3b1uy3Z85nnj7qaGN8LEBI,2884
|
287
|
+
osbot_utils/helpers/xml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
288
|
+
osbot_utils/helpers/xml/rss/RSS__Channel.py,sha256=HPLsGRNIaPh0_8GYE2a53bSV5Bb4E6j6tKGuy4bxg4Y,605
|
289
|
+
osbot_utils/helpers/xml/rss/RSS__Enclosure.py,sha256=f75U7nXXJ7hLs2zPDui0WsFAmMJaeaverjlxD4M-Otg,142
|
290
|
+
osbot_utils/helpers/xml/rss/RSS__Feed.py,sha256=lhFoeBMWdH1Dp8QnagCGj9bfZHKmB_HkE56hPVZNaM0,425
|
291
|
+
osbot_utils/helpers/xml/rss/RSS__Feed__Parser.py,sha256=qG4FbUexMmU6_pTgcxDBmBeZvh9d1dmFZGzQOwVlggk,5088
|
292
|
+
osbot_utils/helpers/xml/rss/RSS__Image.py,sha256=4uI0jd17pqb8FJ8HQcERXvn3WjGbiOVI8u1tv-IN59U,171
|
293
|
+
osbot_utils/helpers/xml/rss/RSS__Item.py,sha256=y-QI2WBfd9FEsVWc_eNirvZUUslpb2z27hxcm-RVHJQ,611
|
273
294
|
osbot_utils/testing/Catch.py,sha256=HdNoKnrPBjvVj87XYN-Wa1zpo5z3oByURT6TKbd5QpQ,2229
|
274
295
|
osbot_utils/testing/Custom_Handler_For_Http_Tests.py,sha256=LKscFEcuwTQQ9xl4q71PR5FA8U-q8OtfTkCJoIgQIoQ,5358
|
275
296
|
osbot_utils/testing/Duration.py,sha256=iBrczAuw6j3jXtG7ZPraT0PXbCILEcCplJbqei96deA,2217
|
@@ -306,11 +327,11 @@ osbot_utils/utils/Json.py,sha256=0DZGlCU7Nqte5n0r7ctPXFybqA5MRfSrTz5zuK_6UFk,709
|
|
306
327
|
osbot_utils/utils/Json_Cache.py,sha256=mLPkkDZN-3ZVJiDvV1KBJXILtKkTZ4OepzOsDoBPhWg,2006
|
307
328
|
osbot_utils/utils/Lists.py,sha256=tPz5x5s3sRO97WZ_nsxREBPC5cwaHrhgaYBhsrffTT8,5599
|
308
329
|
osbot_utils/utils/Misc.py,sha256=H_xexJgiTxB3jDeDiW8efGQbO0Zuy8MM0iQ7qXC92JI,17363
|
309
|
-
osbot_utils/utils/Objects.py,sha256=
|
330
|
+
osbot_utils/utils/Objects.py,sha256=FVX0CyVY2g1iXIL4UA1dFzgbsA-GYrJ_cYlxosGxTdo,21713
|
310
331
|
osbot_utils/utils/Png.py,sha256=V1juGp6wkpPigMJ8HcxrPDIP4bSwu51oNkLI8YqP76Y,1172
|
311
332
|
osbot_utils/utils/Process.py,sha256=lr3CTiEkN3EiBx3ZmzYmTKlQoPdkgZBRjPulMxG-zdo,2357
|
312
333
|
osbot_utils/utils/Python_Logger.py,sha256=tx8N6wRKL3RDHboDRKZn8SirSJdSAE9cACyJkxrThZ8,12792
|
313
|
-
osbot_utils/utils/Regex.py,sha256=
|
334
|
+
osbot_utils/utils/Regex.py,sha256=MtHhk69ax7Nwu4CQZK7y4KXHZ6VREwEpIchuioB168c,960
|
314
335
|
osbot_utils/utils/Status.py,sha256=Yq4s0TelXgn0i2QjCP9V8mP30GabXp_UL-jjM6Iwiw4,4305
|
315
336
|
osbot_utils/utils/Str.py,sha256=Y05F46m6s3_H7KoPdeasc1LRaU7R4YifIbsHNQYDEeg,3275
|
316
337
|
osbot_utils/utils/Threads.py,sha256=lnh4doZWYUIoWBZRU_780QPeAIKGDh7INuqmU8Fzmdc,3042
|
@@ -318,8 +339,8 @@ osbot_utils/utils/Toml.py,sha256=Rxl8gx7mni5CvBAK-Ai02EKw-GwtJdd3yeHT2kMloik,166
|
|
318
339
|
osbot_utils/utils/Version.py,sha256=Ww6ChwTxqp1QAcxOnztkTicShlcx6fbNsWX5xausHrg,422
|
319
340
|
osbot_utils/utils/Zip.py,sha256=pR6sKliUY0KZXmqNzKY2frfW-YVQEVbLKiyqQX_lc-8,14052
|
320
341
|
osbot_utils/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
321
|
-
osbot_utils/version,sha256=
|
322
|
-
osbot_utils-1.
|
323
|
-
osbot_utils-1.
|
324
|
-
osbot_utils-1.
|
325
|
-
osbot_utils-1.
|
342
|
+
osbot_utils/version,sha256=mY1aZ-j7A_cOD1nfZEVe-d0NfwVf728QcJbLk0BIvkI,8
|
343
|
+
osbot_utils-1.91.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
344
|
+
osbot_utils-1.91.0.dist-info/METADATA,sha256=luU4ieNWlaNSKRUyf9jTUyGb2tKGwRPlZtgZ5cZCTPc,1317
|
345
|
+
osbot_utils-1.91.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
346
|
+
osbot_utils-1.91.0.dist-info/RECORD,,
|
@@ -1,87 +0,0 @@
|
|
1
|
-
from typing import Dict, Any, Union
|
2
|
-
from xml.etree.ElementTree import Element
|
3
|
-
|
4
|
-
from osbot_utils.base_classes.Type_Safe import Type_Safe
|
5
|
-
|
6
|
-
class XML_Attribute(Type_Safe):
|
7
|
-
name : str
|
8
|
-
value : str
|
9
|
-
namespace: str
|
10
|
-
|
11
|
-
class XML_Element(Type_Safe):
|
12
|
-
attributes: Dict[str, XML_Attribute]
|
13
|
-
children : Dict[str, Union[str, 'XML_Element']]
|
14
|
-
|
15
|
-
|
16
|
-
class Xml_To_Dict(Type_Safe):
|
17
|
-
xml_data : str = None # Input XML string
|
18
|
-
root : Element = None # Root ElementTree.Element
|
19
|
-
namespaces : Dict[str, str] # XML namespaces
|
20
|
-
xml_dict : Dict[str, Any] # Parsed XML as dictionary
|
21
|
-
|
22
|
-
def setup(self) :
|
23
|
-
from xml.etree.ElementTree import ParseError
|
24
|
-
try:
|
25
|
-
self.load_namespaces()
|
26
|
-
self.load_root()
|
27
|
-
|
28
|
-
except ParseError as e:
|
29
|
-
raise ValueError(f"Invalid XML: {str(e)}")
|
30
|
-
return self
|
31
|
-
|
32
|
-
|
33
|
-
def load_namespaces(self):
|
34
|
-
from xml.etree.ElementTree import iterparse
|
35
|
-
from io import StringIO
|
36
|
-
|
37
|
-
for event, elem in iterparse(StringIO(self.xml_data), events=("start-ns",)):
|
38
|
-
self.namespaces[elem[0]] = elem[1]
|
39
|
-
|
40
|
-
def load_root(self):
|
41
|
-
from xml.etree.ElementTree import fromstring
|
42
|
-
|
43
|
-
self.root = fromstring(self.xml_data)
|
44
|
-
|
45
|
-
def element_to_dict(self, element: Element) -> Union[Dict[str, Any], str]:
|
46
|
-
"""Convert an ElementTree.Element to a dictionary"""
|
47
|
-
result: Dict[str, Any] = {}
|
48
|
-
|
49
|
-
|
50
|
-
if element.attrib: # Handle attributes
|
51
|
-
result.update(element.attrib)
|
52
|
-
|
53
|
-
# Handle child elements
|
54
|
-
child_nodes: Dict[str, Any] = {}
|
55
|
-
for child in element:
|
56
|
-
tag = child.tag # Remove namespace prefix if present
|
57
|
-
if '}' in tag:
|
58
|
-
tag = tag.split('}', 1)[1]
|
59
|
-
|
60
|
-
child_data = self.element_to_dict(child)
|
61
|
-
|
62
|
-
if tag in child_nodes:
|
63
|
-
if not isinstance(child_nodes[tag], list):
|
64
|
-
child_nodes[tag] = [child_nodes[tag]]
|
65
|
-
child_nodes[tag].append(child_data)
|
66
|
-
else:
|
67
|
-
child_nodes[tag] = child_data
|
68
|
-
|
69
|
-
# Handle text content
|
70
|
-
text = element.text.strip() if element.text else ''
|
71
|
-
if text:
|
72
|
-
if child_nodes or result:
|
73
|
-
result['_text'] = text
|
74
|
-
else:
|
75
|
-
return text
|
76
|
-
elif not child_nodes and not result: # Make sure we return text content even for empty nodes
|
77
|
-
return text
|
78
|
-
|
79
|
-
# Combine results
|
80
|
-
if child_nodes:
|
81
|
-
result.update(child_nodes)
|
82
|
-
|
83
|
-
return result
|
84
|
-
|
85
|
-
def parse(self) -> Dict[str, Any]: # Convert parsed XML to dictionary
|
86
|
-
self.xml_dict = self.element_to_dict(self.root)
|
87
|
-
return self
|
File without changes
|
File without changes
|