osbot-utils 1.88.0__py3-none-any.whl → 1.90.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.
@@ -3,7 +3,7 @@
3
3
 
4
4
  import sys
5
5
  import types
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)
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
@@ -23,7 +23,7 @@ if sys.version_info < (3, 8): # pragma
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, ForwardRef
27
27
 
28
28
  if sys.version_info >= (3, 10):
29
29
  NoneType = types.NoneType
@@ -148,6 +148,7 @@ class Type_Safe:
148
148
  def __default__value__(cls, var_type):
149
149
  import typing
150
150
  from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List
151
+ from osbot_utils.base_classes.Type_Safe__Dict import Type_Safe__Dict
151
152
 
152
153
  if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
153
154
  return set()
@@ -156,13 +157,28 @@ class Type_Safe:
156
157
 
157
158
  if var_type is typing.Dict:
158
159
  return {}
159
- if get_origin(var_type) is dict:
160
- return {} # todo: add Type_Safe__Dict
160
+
161
+ if get_origin(var_type) is dict: # e.g. Dict[key_type, value_type]
162
+ key_type, value_type = get_args(var_type)
163
+ if isinstance(key_type, ForwardRef): # Handle forward references on key_type ---
164
+ forward_name = key_type.__forward_arg__
165
+ if forward_name == cls.__name__:
166
+ key_type = cls
167
+ if isinstance(value_type, ForwardRef): # Handle forward references on value_type ---
168
+ forward_name = value_type.__forward_arg__
169
+ if forward_name == cls.__name__:
170
+ value_type = cls
171
+ return Type_Safe__Dict(expected_key_type=key_type, expected_value_type=value_type)
161
172
 
162
173
  if var_type is typing.List:
163
- return [] # handle case when List was used with no type information provided
174
+ return [] # handle case when List was used with no type information provided
175
+
164
176
  if get_origin(var_type) is list: # if we have list defined as list[type]
165
177
  item_type = get_args(var_type)[0] # get the type that was defined
178
+ if isinstance(item_type, ForwardRef): # handle the case when the type is a forward reference
179
+ forward_name = item_type.__forward_arg__
180
+ if forward_name == cls.__name__: # if the forward reference is to the current class (simple name check)
181
+ item_type = cls # set the item_type to the current class
166
182
  return Type_Safe__List(expected_type=item_type) # and used it as expected_type in Type_Safe__List
167
183
  else:
168
184
  return default_value(var_type) # for all other cases call default_value, which will try to create a default instance
@@ -258,16 +274,16 @@ class Type_Safe:
258
274
  return self
259
275
 
260
276
  def deserialize_dict__using_key_value_annotations(self, key, value):
277
+ from osbot_utils.base_classes.Type_Safe__Dict import Type_Safe__Dict
278
+
261
279
  dict_annotations_tuple = get_args(self.__annotations__[key])
262
280
  if not dict_annotations_tuple: # happens when the value is a dict/Dict with no annotations
263
281
  return value
264
282
  if not type(value) is dict:
265
283
  return value
266
- #key_class = get_args(self.__annotations__[key])[0]
267
- #value_class = get_args(self.__annotations__[key])[1]
268
284
  key_class = dict_annotations_tuple[0]
269
285
  value_class = dict_annotations_tuple[1]
270
- new_value = {}
286
+ new_value = Type_Safe__Dict(expected_key_type=key_class, expected_value_type=value_class)
271
287
 
272
288
  for dict_key, dict_value in value.items():
273
289
  if issubclass(key_class, Type_Safe):
@@ -0,0 +1,117 @@
1
+ from typing import get_origin, get_args, Union, Optional, Any, ForwardRef
2
+
3
+ EXACT_TYPE_MATCH = (int, float, str, bytes, bool, complex)
4
+
5
+ class Type_Safe__Base:
6
+ def is_instance_of_type(self, item, expected_type):
7
+ if expected_type is Any:
8
+ return True
9
+ if isinstance(expected_type, ForwardRef): # todo: add support for ForwardRef
10
+ return True
11
+ origin = get_origin(expected_type)
12
+ args = get_args(expected_type)
13
+ if origin is None:
14
+ if expected_type in EXACT_TYPE_MATCH:
15
+ if type(item) is expected_type:
16
+ return True
17
+ else:
18
+ expected_type_name = type_str(expected_type)
19
+ actual_type_name = type_str(type(item))
20
+ raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
21
+ else:
22
+ if isinstance(item, expected_type): # Non-parameterized type
23
+ return True
24
+ else:
25
+ expected_type_name = type_str(expected_type)
26
+ actual_type_name = type_str(type(item))
27
+ raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
28
+
29
+ elif origin is list and args: # Expected type is List[...]
30
+ (item_type,) = args
31
+ if not isinstance(item, list):
32
+ expected_type_name = type_str(expected_type)
33
+ actual_type_name = type_str(type(item))
34
+ raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
35
+ for idx, elem in enumerate(item):
36
+ try:
37
+ self.is_instance_of_type(elem, item_type)
38
+ except TypeError as e:
39
+ raise TypeError(f"In list at index {idx}: {e}")
40
+ return True
41
+ elif origin is dict and args: # Expected type is Dict[...]
42
+ key_type, value_type = args
43
+ if not isinstance(item, dict):
44
+ expected_type_name = type_str(expected_type)
45
+ actual_type_name = type_str(type(item))
46
+ raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
47
+ for k, v in item.items():
48
+ try:
49
+ self.is_instance_of_type(k, key_type)
50
+ except TypeError as e:
51
+ raise TypeError(f"In dict key '{k}': {e}")
52
+ try:
53
+ self.is_instance_of_type(v, value_type)
54
+ except TypeError as e:
55
+ raise TypeError(f"In dict value for key '{k}': {e}")
56
+ return True
57
+ elif origin is tuple:
58
+ if not isinstance(item, tuple):
59
+ expected_type_name = type_str(expected_type)
60
+ actual_type_name = type_str(type(item))
61
+ raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
62
+ if len(args) != len(item):
63
+ raise TypeError(f"Expected tuple of length {len(args)}, but got {len(item)}")
64
+ for idx, (elem, elem_type) in enumerate(zip(item, args)):
65
+ try:
66
+ self.is_instance_of_type(elem, elem_type)
67
+ except TypeError as e:
68
+ raise TypeError(f"In tuple at index {idx}: {e}")
69
+ return True
70
+ elif origin is Union or expected_type is Optional: # Expected type is Union[...]
71
+ for arg in args:
72
+ try:
73
+ self.is_instance_of_type(item, arg)
74
+ return True
75
+ except TypeError:
76
+ continue
77
+ expected_type_name = type_str(expected_type)
78
+ actual_type_name = type_str(type(item))
79
+ raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
80
+ else:
81
+ if isinstance(item, origin):
82
+ return True
83
+ else:
84
+ expected_type_name = type_str(expected_type)
85
+ actual_type_name = type_str(type(item))
86
+ raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
87
+
88
+ # todo: see if we should/can move this to the Objects.py file
89
+ def type_str(tp):
90
+ origin = get_origin(tp)
91
+ if origin is None:
92
+ if hasattr(tp, '__name__'):
93
+ return tp.__name__
94
+ else:
95
+ return str(tp)
96
+ else:
97
+ args = get_args(tp)
98
+ args_str = ', '.join(type_str(arg) for arg in args)
99
+ return f"{origin.__name__}[{args_str}]"
100
+
101
+ def get_object_type_str(obj):
102
+ if isinstance(obj, dict):
103
+ if not obj:
104
+ return "Dict[Empty]"
105
+ key_types = set(type(k).__name__ for k in obj.keys())
106
+ value_types = set(type(v).__name__ for v in obj.values())
107
+ key_type_str = ', '.join(sorted(key_types))
108
+ value_type_str = ', '.join(sorted(value_types))
109
+ return f"Dict[{key_type_str}, {value_type_str}]"
110
+ elif isinstance(obj, list):
111
+ if not obj:
112
+ return "List[Empty]"
113
+ elem_types = set(type(e).__name__ for e in obj)
114
+ elem_type_str = ', '.join(sorted(elem_types))
115
+ return f"List[{elem_type_str}]"
116
+ else:
117
+ return type(obj).__name__
@@ -0,0 +1,22 @@
1
+ from osbot_utils.base_classes.Type_Safe__Base import type_str, Type_Safe__Base
2
+
3
+ class Type_Safe__Dict(Type_Safe__Base, dict):
4
+ def __init__(self, expected_key_type, expected_value_type, *args, **kwargs):
5
+ super().__init__(*args, **kwargs)
6
+
7
+ self.expected_key_type = expected_key_type
8
+ self.expected_value_type = expected_value_type
9
+
10
+ for k, v in self.items(): # check type-safety of ctor arguments
11
+ self.is_instance_of_type(k, self.expected_key_type )
12
+ self.is_instance_of_type(v, self.expected_value_type)
13
+
14
+ def __setitem__(self, key, value): # Check type-safety before allowing assignment.
15
+ self.is_instance_of_type(key, self.expected_key_type)
16
+ self.is_instance_of_type(value, self.expected_value_type)
17
+ super().__setitem__(key, value)
18
+
19
+ def __repr__(self):
20
+ key_type_name = type_str(self.expected_key_type)
21
+ value_type_name = type_str(self.expected_value_type)
22
+ return f"dict[{key_type_name}, {value_type_name}] with {len(self)} entries"
@@ -1,8 +1,7 @@
1
- from typing import get_origin, get_args, Union, Optional, Any, ForwardRef
1
+ from osbot_utils.base_classes.Type_Safe__Base import Type_Safe__Base, type_str
2
2
 
3
- EXACT_TYPE_MATCH = (int, float, str, bytes, bool, complex)
4
3
 
5
- class Type_Safe__List(list):
4
+ class Type_Safe__List(Type_Safe__Base, list):
6
5
 
7
6
  def __init__(self, expected_type, *args):
8
7
  super().__init__(*args)
@@ -19,115 +18,5 @@ class Type_Safe__List(list):
19
18
  raise TypeError(f"In Type_Safe__List: Invalid type for item: {e}")
20
19
  super().append(item)
21
20
 
22
- def is_instance_of_type(self, item, expected_type):
23
- if expected_type is Any:
24
- return True
25
- if isinstance(expected_type, ForwardRef): # todo: add support for ForwardRef
26
- return True
27
- origin = get_origin(expected_type)
28
- args = get_args(expected_type)
29
- if origin is None:
30
- if expected_type in EXACT_TYPE_MATCH:
31
- if type(item) is expected_type:
32
- return True
33
- else:
34
- expected_type_name = type_str(expected_type)
35
- actual_type_name = type_str(type(item))
36
- raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
37
- else:
38
- if isinstance(item, expected_type): # Non-parameterized type
39
- return True
40
- else:
41
- expected_type_name = type_str(expected_type)
42
- actual_type_name = type_str(type(item))
43
- raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
44
21
 
45
- elif origin is list and args: # Expected type is List[...]
46
- (item_type,) = args
47
- if not isinstance(item, list):
48
- expected_type_name = type_str(expected_type)
49
- actual_type_name = type_str(type(item))
50
- raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
51
- for idx, elem in enumerate(item):
52
- try:
53
- self.is_instance_of_type(elem, item_type)
54
- except TypeError as e:
55
- raise TypeError(f"In list at index {idx}: {e}")
56
- return True
57
- elif origin is dict and args: # Expected type is Dict[...]
58
- key_type, value_type = args
59
- if not isinstance(item, dict):
60
- expected_type_name = type_str(expected_type)
61
- actual_type_name = type_str(type(item))
62
- raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
63
- for k, v in item.items():
64
- try:
65
- self.is_instance_of_type(k, key_type)
66
- except TypeError as e:
67
- raise TypeError(f"In dict key '{k}': {e}")
68
- try:
69
- self.is_instance_of_type(v, value_type)
70
- except TypeError as e:
71
- raise TypeError(f"In dict value for key '{k}': {e}")
72
- return True
73
- elif origin is tuple:
74
- if not isinstance(item, tuple):
75
- expected_type_name = type_str(expected_type)
76
- actual_type_name = type_str(type(item))
77
- raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
78
- if len(args) != len(item):
79
- raise TypeError(f"Expected tuple of length {len(args)}, but got {len(item)}")
80
- for idx, (elem, elem_type) in enumerate(zip(item, args)):
81
- try:
82
- self.is_instance_of_type(elem, elem_type)
83
- except TypeError as e:
84
- raise TypeError(f"In tuple at index {idx}: {e}")
85
- return True
86
- elif origin is Union or expected_type is Optional: # Expected type is Union[...]
87
- for arg in args:
88
- try:
89
- self.is_instance_of_type(item, arg)
90
- return True
91
- except TypeError:
92
- continue
93
- expected_type_name = type_str(expected_type)
94
- actual_type_name = type_str(type(item))
95
- raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
96
- else:
97
- if isinstance(item, origin):
98
- return True
99
- else:
100
- expected_type_name = type_str(expected_type)
101
- actual_type_name = type_str(type(item))
102
- raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
103
22
 
104
- # todo: see if we should/can move this to the Objects.py file
105
- def type_str(tp):
106
- origin = get_origin(tp)
107
- if origin is None:
108
- if hasattr(tp, '__name__'):
109
- return tp.__name__
110
- else:
111
- return str(tp)
112
- else:
113
- args = get_args(tp)
114
- args_str = ', '.join(type_str(arg) for arg in args)
115
- return f"{origin.__name__}[{args_str}]"
116
-
117
- def get_object_type_str(obj):
118
- if isinstance(obj, dict):
119
- if not obj:
120
- return "Dict[Empty]"
121
- key_types = set(type(k).__name__ for k in obj.keys())
122
- value_types = set(type(v).__name__ for v in obj.values())
123
- key_type_str = ', '.join(sorted(key_types))
124
- value_type_str = ', '.join(sorted(value_types))
125
- return f"Dict[{key_type_str}, {value_type_str}]"
126
- elif isinstance(obj, list):
127
- if not obj:
128
- return "List[Empty]"
129
- elem_types = set(type(e).__name__ for e in obj)
130
- elem_type_str = ', '.join(sorted(elem_types))
131
- return f"List[{elem_type_str}]"
132
- else:
133
- return type(obj).__name__
@@ -0,0 +1,7 @@
1
+ from osbot_utils.base_classes.Type_Safe import Type_Safe
2
+
3
+
4
+ class Xml__Attribute(Type_Safe):
5
+ name : str
6
+ value : str
7
+ namespace: str
@@ -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,7 @@
1
+ from osbot_utils.base_classes.Type_Safe import Type_Safe
2
+
3
+
4
+ class RSS__Enclosure(Type_Safe):
5
+ url : str
6
+ type : str
7
+ length : int
@@ -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,8 @@
1
+ from osbot_utils.base_classes.Type_Safe import Type_Safe
2
+
3
+ class RSS__Image(Type_Safe):
4
+ url : str
5
+ title : str
6
+ link : str
7
+ width : int
8
+ height : int
@@ -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]
@@ -30,14 +30,17 @@ def are_types_compatible_for_assigment(source_type, target_type):
30
30
  import types
31
31
  import typing
32
32
 
33
+ if isinstance(target_type, str): # If the "target_type" is a forward reference (string), handle it here.
34
+ if target_type == source_type.__name__: # Simple check: does the string match the actual class name
35
+ return True
33
36
  if source_type is target_type:
34
37
  return True
35
38
  if source_type is int and target_type is float:
36
39
  return True
37
- if target_type in source_type.__mro__: # this means that the source_type has the target_type has of its base types
40
+ if target_type in source_type.__mro__: # this means that the source_type has the target_type has of its base types
38
41
  return True
39
- if target_type is callable: # handle case where callable was used as the target type
40
- if source_type is types.MethodType: # and a method or function was used as the source type
42
+ if target_type is callable: # handle case where callable was used as the target type
43
+ if source_type is types.MethodType: # and a method or function was used as the source type
41
44
  return True
42
45
  if source_type is types.FunctionType:
43
46
  return True
@@ -413,7 +416,7 @@ def value_type_matches_obj_annotation_for_attr(target, attr_name, value):
413
416
  origin_attr_type = get_origin(attr_type) # to handle when type definion contains an generic
414
417
  if origin_attr_type is typing.Union:
415
418
  args = get_args(attr_type)
416
- if len(args)==2 and args[1] is type(None): # todo: find a better way to do this, since this is handling an edge case when origin_attr_type is Optional (whcih is an shorthand for Union[X, None] )
419
+ if len(args)==2 and args[1] is type(None): # todo: find a better way to do this, since this is handling an edge case when origin_attr_type is Optional (which is an shorthand for Union[X, None] )
417
420
  attr_type = args[0]
418
421
  origin_attr_type = get_origin(attr_type)
419
422
 
osbot_utils/version CHANGED
@@ -1 +1 @@
1
- v1.88.0
1
+ v1.90.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: osbot_utils
3
- Version: 1.88.0
3
+ Version: 1.90.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
- ![Current Release](https://img.shields.io/badge/release-v1.88.0-blue)
26
+ ![Current Release](https://img.shields.io/badge/release-v1.90.0-blue)
27
27
  [![codecov](https://codecov.io/gh/owasp-sbot/OSBot-Utils/graph/badge.svg?token=GNVW0COX1N)](https://codecov.io/gh/owasp-sbot/OSBot-Utils)
28
28
 
29
29
 
@@ -2,8 +2,10 @@ 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=VeNkr6BD9iA9LyQ2FiduHIMM8nS0iaALpwYC2Y-KWy4,23445
6
- osbot_utils/base_classes/Type_Safe__List.py,sha256=iWyoc2xjHkTJrZTVnPse9Rljte2tF67oNq8yA7jnAhY,5996
5
+ osbot_utils/base_classes/Type_Safe.py,sha256=oEwxNPa1pcq6ccJUyWPUuuMXK6CRzVIzwQNL2lcc6E4,24663
6
+ osbot_utils/base_classes/Type_Safe__Base.py,sha256=CFPYe8_i5vvTLyc7s8CXbY4n_dY6sqVfBY8w9Vo77ZA,5468
7
+ osbot_utils/base_classes/Type_Safe__Dict.py,sha256=sfZcukhXUd9TS0PQpAk-gGLfZUJSC6BtMh6jF4Fn8Jw,1107
8
+ osbot_utils/base_classes/Type_Safe__List.py,sha256=pXDzJJttpEQQ9oTdsw7BykMB4VIX2rZzi1ZrnCzMZ8M,650
7
9
  osbot_utils/base_classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
10
  osbot_utils/context_managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
11
  osbot_utils/context_managers/async_invoke.py,sha256=-ja3K8orLy8Of54CIYSK-zn443pOIDY2hnFBjVELrXc,829
@@ -267,6 +269,19 @@ osbot_utils/helpers/trace/Trace_Call__Stats.py,sha256=gmiotIrOXe2ssxodzQQ56t8eGT
267
269
  osbot_utils/helpers/trace/Trace_Call__View_Model.py,sha256=a40nn6agCEMd2ecsJ93n8vXij0omh0D69QilqwmN_ao,4545
268
270
  osbot_utils/helpers/trace/Trace_Files.py,sha256=SNpAmuBlSUS9NyVocgZ5vevzqVaIqoh622yZge3a53A,978
269
271
  osbot_utils/helpers/trace/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
272
+ osbot_utils/helpers/xml/Xml__Attribute.py,sha256=r29x8ehRug27KuHQKQEGl6Thz_MNaNHy6X3cPRgH4ho,148
273
+ osbot_utils/helpers/xml/Xml__Element.py,sha256=J0hMaJPIBNmZf4wo0xrtZBZd5vWie5FoX9XqtAXQToM,871
274
+ osbot_utils/helpers/xml/Xml__File.py,sha256=c3axXx07rIoK5tGzpLwH-X1B7nusSe1-SZAxJNZJ0KI,420
275
+ osbot_utils/helpers/xml/Xml__File__Load.py,sha256=iAH21HjrqFoZlU5-Kg2RE53RBdNs0mDaASETUTnbhRo,3625
276
+ osbot_utils/helpers/xml/Xml__File__To_Dict.py,sha256=1KGswrpMpUPu4JDYUbkP8GTOQfHGcGONnDYsi-_aqv0,2061
277
+ osbot_utils/helpers/xml/Xml__File__To_Xml.py,sha256=HvUbYOfLz6i160_to90dP3b1uy3Z85nnj7qaGN8LEBI,2884
278
+ osbot_utils/helpers/xml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
279
+ osbot_utils/helpers/xml/rss/RSS__Channel.py,sha256=HPLsGRNIaPh0_8GYE2a53bSV5Bb4E6j6tKGuy4bxg4Y,605
280
+ osbot_utils/helpers/xml/rss/RSS__Enclosure.py,sha256=f75U7nXXJ7hLs2zPDui0WsFAmMJaeaverjlxD4M-Otg,142
281
+ osbot_utils/helpers/xml/rss/RSS__Feed.py,sha256=lhFoeBMWdH1Dp8QnagCGj9bfZHKmB_HkE56hPVZNaM0,425
282
+ osbot_utils/helpers/xml/rss/RSS__Feed__Parser.py,sha256=qG4FbUexMmU6_pTgcxDBmBeZvh9d1dmFZGzQOwVlggk,5088
283
+ osbot_utils/helpers/xml/rss/RSS__Image.py,sha256=4uI0jd17pqb8FJ8HQcERXvn3WjGbiOVI8u1tv-IN59U,171
284
+ osbot_utils/helpers/xml/rss/RSS__Item.py,sha256=y-QI2WBfd9FEsVWc_eNirvZUUslpb2z27hxcm-RVHJQ,611
270
285
  osbot_utils/testing/Catch.py,sha256=HdNoKnrPBjvVj87XYN-Wa1zpo5z3oByURT6TKbd5QpQ,2229
271
286
  osbot_utils/testing/Custom_Handler_For_Http_Tests.py,sha256=LKscFEcuwTQQ9xl4q71PR5FA8U-q8OtfTkCJoIgQIoQ,5358
272
287
  osbot_utils/testing/Duration.py,sha256=iBrczAuw6j3jXtG7ZPraT0PXbCILEcCplJbqei96deA,2217
@@ -303,7 +318,7 @@ osbot_utils/utils/Json.py,sha256=0DZGlCU7Nqte5n0r7ctPXFybqA5MRfSrTz5zuK_6UFk,709
303
318
  osbot_utils/utils/Json_Cache.py,sha256=mLPkkDZN-3ZVJiDvV1KBJXILtKkTZ4OepzOsDoBPhWg,2006
304
319
  osbot_utils/utils/Lists.py,sha256=tPz5x5s3sRO97WZ_nsxREBPC5cwaHrhgaYBhsrffTT8,5599
305
320
  osbot_utils/utils/Misc.py,sha256=H_xexJgiTxB3jDeDiW8efGQbO0Zuy8MM0iQ7qXC92JI,17363
306
- osbot_utils/utils/Objects.py,sha256=iuNNp_u9aOiHtAXwkSo-8FikXWcqZczaZqNwHKKQEcM,19617
321
+ osbot_utils/utils/Objects.py,sha256=qzdwfQZMyI-ySlOhuUM6z2M4cW9eZDXpUeTIzgYwlS4,19977
307
322
  osbot_utils/utils/Png.py,sha256=V1juGp6wkpPigMJ8HcxrPDIP4bSwu51oNkLI8YqP76Y,1172
308
323
  osbot_utils/utils/Process.py,sha256=lr3CTiEkN3EiBx3ZmzYmTKlQoPdkgZBRjPulMxG-zdo,2357
309
324
  osbot_utils/utils/Python_Logger.py,sha256=tx8N6wRKL3RDHboDRKZn8SirSJdSAE9cACyJkxrThZ8,12792
@@ -315,8 +330,8 @@ osbot_utils/utils/Toml.py,sha256=Rxl8gx7mni5CvBAK-Ai02EKw-GwtJdd3yeHT2kMloik,166
315
330
  osbot_utils/utils/Version.py,sha256=Ww6ChwTxqp1QAcxOnztkTicShlcx6fbNsWX5xausHrg,422
316
331
  osbot_utils/utils/Zip.py,sha256=pR6sKliUY0KZXmqNzKY2frfW-YVQEVbLKiyqQX_lc-8,14052
317
332
  osbot_utils/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
318
- osbot_utils/version,sha256=D0MVxZOCQlRSLSl_5l6QLAIYIlH9b7vBFCZn_V3Z2WY,8
319
- osbot_utils-1.88.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
320
- osbot_utils-1.88.0.dist-info/METADATA,sha256=KAjZ52Wxr8Kq06sTXHWU8RzvgSGltoSiT7BWLDPKTRg,1317
321
- osbot_utils-1.88.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
322
- osbot_utils-1.88.0.dist-info/RECORD,,
333
+ osbot_utils/version,sha256=BspJJ07wPk_cd1Tk06A-p8nFKYNDb_kD0KykvCZJpMc,8
334
+ osbot_utils-1.90.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
335
+ osbot_utils-1.90.0.dist-info/METADATA,sha256=0FRdshsrS_RyXSve5jzGaHU5JXGt9_X6gDE8-N9frH0,1317
336
+ osbot_utils-1.90.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
337
+ osbot_utils-1.90.0.dist-info/RECORD,,