osbot-utils 2.11.0__py3-none-any.whl → 2.13.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.
Files changed (38) hide show
  1. osbot_utils/context_managers/capture_duration.py +19 -12
  2. osbot_utils/helpers/CPrint.py +0 -1
  3. osbot_utils/helpers/Obj_Id.py +29 -0
  4. osbot_utils/helpers/trace/Trace_Call.py +1 -2
  5. osbot_utils/helpers/trace/Trace_Call__Handler.py +14 -14
  6. osbot_utils/helpers/xml/Xml__File.py +1 -1
  7. osbot_utils/helpers/xml/Xml__File__To_Dict.py +1 -1
  8. osbot_utils/helpers/xml/Xml__File__To_Xml.py +1 -1
  9. osbot_utils/testing/performance/Performance_Measure__Session.py +128 -0
  10. osbot_utils/testing/performance/__init__.py +0 -0
  11. osbot_utils/testing/performance/models/Model__Performance_Measure__Measurement.py +14 -0
  12. osbot_utils/testing/performance/models/Model__Performance_Measure__Result.py +10 -0
  13. osbot_utils/testing/performance/models/__init__.py +0 -0
  14. osbot_utils/type_safe/Type_Safe.py +35 -418
  15. osbot_utils/type_safe/Type_Safe__Base.py +8 -24
  16. osbot_utils/type_safe/Type_Safe__Dict.py +9 -8
  17. osbot_utils/type_safe/shared/Type_Safe__Annotations.py +29 -0
  18. osbot_utils/type_safe/shared/Type_Safe__Cache.py +143 -0
  19. osbot_utils/type_safe/shared/Type_Safe__Convert.py +47 -0
  20. osbot_utils/type_safe/shared/Type_Safe__Not_Cached.py +24 -0
  21. osbot_utils/type_safe/shared/Type_Safe__Raise_Exception.py +14 -0
  22. osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py +4 -0
  23. osbot_utils/type_safe/shared/Type_Safe__Validation.py +246 -0
  24. osbot_utils/type_safe/shared/__init__.py +0 -0
  25. osbot_utils/type_safe/steps/Type_Safe__Step__Class_Kwargs.py +114 -0
  26. osbot_utils/type_safe/steps/Type_Safe__Step__Default_Kwargs.py +42 -0
  27. osbot_utils/type_safe/steps/Type_Safe__Step__Default_Value.py +74 -0
  28. osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py +138 -0
  29. osbot_utils/type_safe/steps/Type_Safe__Step__Init.py +24 -0
  30. osbot_utils/type_safe/steps/Type_Safe__Step__Set_Attr.py +92 -0
  31. osbot_utils/type_safe/steps/__init__.py +0 -0
  32. osbot_utils/utils/Objects.py +27 -232
  33. osbot_utils/utils/Status.py +0 -2
  34. osbot_utils/version +1 -1
  35. {osbot_utils-2.11.0.dist-info → osbot_utils-2.13.0.dist-info}/METADATA +2 -2
  36. {osbot_utils-2.11.0.dist-info → osbot_utils-2.13.0.dist-info}/RECORD +38 -17
  37. {osbot_utils-2.11.0.dist-info → osbot_utils-2.13.0.dist-info}/LICENSE +0 -0
  38. {osbot_utils-2.11.0.dist-info → osbot_utils-2.13.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,74 @@
1
+
2
+ import sys
3
+ import inspect
4
+ import typing
5
+
6
+ from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
7
+ from osbot_utils.utils.Objects import default_value
8
+ from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
9
+ from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
10
+
11
+
12
+ # Backport implementations of get_args for Python 3.7 # todo: refactor into separate class (focused on past python version compatibility)
13
+ if sys.version_info < (3, 8): # pragma: no cover
14
+
15
+ def get_args(tp):
16
+ if isinstance(tp, typing._GenericAlias):
17
+ return tp.__args__
18
+ else:
19
+ return ()
20
+ else:
21
+ from typing import get_args, ForwardRef
22
+
23
+
24
+ class Type_Safe__Step__Default_Value:
25
+
26
+ def default_value(self, _cls, var_type):
27
+
28
+ origin = type_safe_cache.get_origin(var_type) # todo: refactor this to use the get_origin method
29
+ if origin is type: # Special handling for Type[T] # todo: reuse the get_origin value
30
+ type_args = get_args(var_type)
31
+ if type_args:
32
+ if isinstance(type_args[0], ForwardRef):
33
+ forward_name = type_args[0].__forward_arg__
34
+ for base_cls in inspect.getmro(_cls):
35
+ if base_cls.__name__ == forward_name:
36
+ return _cls # note: in this case we return the cls, and not the base_cls (which makes sense since this happens when the cls class uses base_cls as base, which has a ForwardRef to base_cls )
37
+ return type_args[0] # Return the actual type as the default value
38
+
39
+ if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
40
+ return set()
41
+
42
+ if origin is set:
43
+ return set() # todo: add Type_Safe__Set
44
+
45
+ if var_type is typing.Dict:
46
+ return {}
47
+
48
+ if origin is dict: # e.g. Dict[key_type, value_type]
49
+ key_type, value_type = get_args(var_type)
50
+ if isinstance(key_type, ForwardRef): # Handle forward references on key_type ---
51
+ forward_name = key_type.__forward_arg__
52
+ if forward_name == _cls.__name__:
53
+ key_type = _cls
54
+ if isinstance(value_type, ForwardRef): # Handle forward references on value_type ---
55
+ forward_name = value_type.__forward_arg__
56
+ if forward_name == _cls.__name__:
57
+ value_type = _cls
58
+ return Type_Safe__Dict(expected_key_type=key_type, expected_value_type=value_type)
59
+
60
+ if var_type is typing.List:
61
+ return [] # handle case when List was used with no type information provided
62
+
63
+ if origin is list: # if we have list defined as list[type]
64
+ item_type = get_args(var_type)[0] # get the type that was defined
65
+ if isinstance(item_type, ForwardRef): # handle the case when the type is a forward reference
66
+ forward_name = item_type.__forward_arg__
67
+ if forward_name == _cls.__name__: # if the forward reference is to the current class (simple name check)
68
+ item_type = _cls # set the item_type to the current class
69
+ return Type_Safe__List(expected_type=item_type) # and used it as expected_type in Type_Safe__List
70
+ else:
71
+ return default_value(var_type) # for all other cases call default_value, which will try to create a default instance
72
+
73
+
74
+ type_safe_step_default_value = Type_Safe__Step__Default_Value()
@@ -0,0 +1,138 @@
1
+ import sys
2
+ import types
3
+ from decimal import Decimal
4
+ from enum import EnumMeta
5
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
6
+ from osbot_utils.type_safe.Type_Safe__List import Type_Safe__List
7
+ from osbot_utils.helpers.Random_Guid import Random_Guid
8
+ from osbot_utils.helpers.Random_Guid_Short import Random_Guid_Short
9
+ from osbot_utils.type_safe.shared.Type_Safe__Annotations import type_safe_annotations
10
+ from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
11
+ from osbot_utils.utils.Objects import enum_from_value
12
+ from osbot_utils.helpers.Safe_Id import Safe_Id
13
+ from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
14
+
15
+ # todo; refactor all this python compatibility into the python_3_8 class
16
+ if sys.version_info < (3, 8): # pragma: no cover
17
+
18
+ def get_args(tp):
19
+ import typing
20
+ if isinstance(tp, typing._GenericAlias):
21
+ return tp.__args__
22
+ else:
23
+ return ()
24
+ else:
25
+ from typing import get_args, Any
26
+
27
+
28
+ class Type_Safe__Step__From_Json:
29
+
30
+ # todo: this needs refactoring, since the logic and code is getting quite complex (to be inside methods like this)
31
+ def deserialize_from_dict(self, _self, data, raise_on_not_found=False):
32
+
33
+ if hasattr(data, 'items') is False:
34
+ raise ValueError(f"Expected a dictionary, but got '{type(data)}'")
35
+
36
+ for key, value in data.items():
37
+ if hasattr(_self, key) and isinstance(getattr(_self, key), Type_Safe):
38
+ self.deserialize_from_dict(getattr(_self, key), value) # if the attribute is a Type_Safe object, then also deserialize it
39
+ else:
40
+ if hasattr(_self, '__annotations__'): # can only do type safety checks if the class does not have annotations
41
+ if hasattr(_self, key) is False: # make sure we are now adding new attributes to the class
42
+ if raise_on_not_found:
43
+ raise ValueError(f"Attribute '{key}' not found in '{_self.__class__.__name__}'")
44
+ else:
45
+ continue
46
+ if type_safe_annotations.obj_attribute_annotation(_self, key) == type: # Handle type objects
47
+ value = self.deserialize_type__using_value(value)
48
+ elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, dict): # handle the case when the value is a dict
49
+ value = self.deserialize_dict__using_key_value_annotations(_self, key, value)
50
+ elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, list): # handle the case when the value is a list
51
+ attribute_annotation = type_safe_annotations.obj_attribute_annotation(_self, key) # get the annotation for this variable
52
+ attribute_annotation_args = get_args(attribute_annotation)
53
+ if attribute_annotation_args:
54
+ expected_type = get_args(attribute_annotation)[0] # get the first arg (which is the type)
55
+ type_safe_list = Type_Safe__List(expected_type) # create a new instance of Type_Safe__List
56
+ for item in value: # next we need to convert all items (to make sure they all match the type)
57
+ if type(item) is dict:
58
+ new_item = expected_type(**item) # create new object
59
+ else:
60
+ new_item = expected_type(item)
61
+ type_safe_list.append(new_item) # and add it to the new type_safe_list obejct
62
+ value = type_safe_list # todo: refactor out this create list code, maybe to an deserialize_from_list method
63
+ else:
64
+ if value is not None:
65
+ if type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, EnumMeta): # Handle the case when the value is an Enum
66
+ enum_type = getattr(_self, '__annotations__').get(key)
67
+ if type(value) is not enum_type: # If the value is not already of the target type
68
+ value = enum_from_value(enum_type, value) # Try to resolve the value into the enum
69
+
70
+ # todo: refactor these special cases into a separate method to class
71
+ elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Decimal): # handle Decimals
72
+ value = Decimal(value)
73
+ elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Safe_Id): # handle Safe_Id
74
+ value = Safe_Id(value)
75
+ elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Random_Guid): # handle Random_Guid
76
+ value = Random_Guid(value)
77
+ elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Random_Guid_Short): # handle Random_Guid_Short
78
+ value = Random_Guid_Short(value)
79
+ elif type_safe_annotations.obj_is_attribute_annotation_of_type(_self, key, Timestamp_Now): # handle Timestamp_Now
80
+ value = Timestamp_Now(value)
81
+ setattr(_self, key, value) # Direct assignment for primitive types and other structures
82
+
83
+ return _self
84
+
85
+ def deserialize_type__using_value(self, value):
86
+ if value:
87
+ try:
88
+ module_name, type_name = value.rsplit('.', 1)
89
+ if module_name == 'builtins' and type_name == 'NoneType': # Special case for NoneType (which serialises as builtins.* , but it actually in types.* )
90
+ value = types.NoneType
91
+ else:
92
+ module = __import__(module_name, fromlist=[type_name])
93
+ value = getattr(module, type_name)
94
+ except (ValueError, ImportError, AttributeError) as e:
95
+ raise ValueError(f"Could not reconstruct type from '{value}': {str(e)}")
96
+ return value
97
+
98
+ def deserialize_dict__using_key_value_annotations(self, _self, key, value):
99
+ from osbot_utils.type_safe.Type_Safe__Dict import Type_Safe__Dict
100
+
101
+ annotations = type_safe_cache.get_obj_annotations(_self)
102
+ dict_annotations_tuple = get_args(annotations.get(key))
103
+ if not dict_annotations_tuple: # happens when the value is a dict/Dict with no annotations
104
+ return value
105
+ if not type(value) is dict:
106
+ return value
107
+ key_class = dict_annotations_tuple[0]
108
+ value_class = dict_annotations_tuple[1]
109
+ new_value = Type_Safe__Dict(expected_key_type=key_class, expected_value_type=value_class)
110
+
111
+ for dict_key, dict_value in value.items():
112
+ if issubclass(key_class, Type_Safe):
113
+ new__dict_key = self.deserialize_from_dict(key_class(), dict_key)
114
+ else:
115
+ new__dict_key = key_class(dict_key)
116
+
117
+ if type(dict_value) == value_class: # if the value is already the target, then just use it
118
+ new__dict_value = dict_value
119
+ elif issubclass(value_class, Type_Safe):
120
+ new__dict_value = self.deserialize_from_dict(value_class(), dict_value)
121
+ elif value_class is Any:
122
+ new__dict_value = dict_value
123
+ else:
124
+ new__dict_value = value_class(dict_value)
125
+ new_value[new__dict_key] = new__dict_value
126
+
127
+ return new_value
128
+
129
+ def from_json(self, _cls, json_data, raise_on_not_found=False):
130
+ from osbot_utils.utils.Json import json_parse
131
+
132
+ if type(json_data) is str:
133
+ json_data = json_parse(json_data)
134
+ if json_data: # if there is no data or is {} then don't create an object (since this could be caused by bad data being provided)
135
+ return self.deserialize_from_dict(_cls(), json_data,raise_on_not_found=raise_on_not_found)
136
+ return _cls()
137
+
138
+ type_safe_step_from_json = Type_Safe__Step__From_Json()
@@ -0,0 +1,24 @@
1
+ class Type_Safe__Step__Init:
2
+
3
+ def init(self, __self ,
4
+ __class_kwargs ,
5
+ **kwargs
6
+ ) -> None:
7
+
8
+ for (key, value) in __class_kwargs.items(): # assign all default values to target
9
+ if hasattr(__self, key):
10
+ existing_value = getattr(__self, key)
11
+ if existing_value is not None:
12
+ setattr(__self, key, existing_value)
13
+ continue
14
+ setattr(__self, key, value)
15
+
16
+ for (key, value) in kwargs.items(): # overwrite with values provided in ctor
17
+ if hasattr(__self, key):
18
+ if value is not None: # prevent None values from overwriting existing values, which is quite common in default constructors
19
+ setattr(__self, key, value)
20
+ else:
21
+ raise ValueError(f"{__self.__class__.__name__} has no attribute '{key}' and cannot be assigned the value '{value}'. "
22
+ f"Use {__self.__class__.__name__}.__default_kwargs__() see what attributes are available")
23
+
24
+ type_safe_step_init = Type_Safe__Step__Init()
@@ -0,0 +1,92 @@
1
+ from typing import get_origin, Annotated, get_args
2
+ from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
3
+ from osbot_utils.type_safe.shared.Type_Safe__Convert import type_safe_convert
4
+ from osbot_utils.type_safe.shared.Type_Safe__Validation import type_safe_validation
5
+ from osbot_utils.type_safe.validators.Type_Safe__Validator import Type_Safe__Validator
6
+
7
+ class Type_Safe__Step__Set_Attr:
8
+
9
+ def resolve_value(self, _self, annotations, name, value):
10
+ if type(value) is dict:
11
+ value = self.resolve_value__dict(_self, name, value)
12
+ 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)
13
+ value = self.resolve_value__int_str(_self, name, value)
14
+ else:
15
+ value = self.resolve_value__from_origin(value)
16
+
17
+ type_safe_validation.validate_type_compatibility(_self, annotations, name, value)
18
+ return value
19
+
20
+ def resolve_value__dict(self, _self, name, value):
21
+ return type_safe_convert.convert_dict_to_value_from_obj_annotation(_self, name, value)
22
+
23
+ def resolve_value__int_str(self, _self, name, value):
24
+ immutable_vars = type_safe_cache.get_class_immutable_vars(_self.__class__) # get the cached value of immutable vars for this class
25
+
26
+ if name in immutable_vars: # we only need to do the conversion if the variable is immutable
27
+ return value
28
+
29
+ return type_safe_convert.convert_to_value_from_obj_annotation(_self, name, value)
30
+
31
+ def resolve_value__from_origin(self, value):
32
+ #origin = type_safe_cache.get_origin(value) # todo: figure out why this is the only place that the type_safe_cache.get_origin doesn't work (due to WeakKeyDictionary key error on value)
33
+ origin = get_origin(value)
34
+
35
+ if origin is not None:
36
+ value = origin
37
+ return value
38
+
39
+ def handle_get_class__annotated(self, annotation, name, value):
40
+ annotation_args = get_args(annotation)
41
+ target_type = annotation_args[0]
42
+ for attribute in annotation_args[1:]:
43
+ if isinstance(attribute, Type_Safe__Validator):
44
+ attribute.validate(value=value, field_name=name, target_type=target_type)
45
+
46
+ def handle_get_class__dict(self, _self, name, value):
47
+ if value: # todo: see side effects of doing this here (since going into deserialize_dict__using_key_value_annotations has performance hit)
48
+ from osbot_utils.type_safe.steps.Type_Safe__Step__From_Json import Type_Safe__Step__From_Json # here because of circular dependencies
49
+ value = Type_Safe__Step__From_Json().deserialize_dict__using_key_value_annotations(_self, name, value) # todo: refactor how this actually works since it is not good to having to use the deserialize_dict__using_key_value_annotations from here
50
+ return value
51
+
52
+ def handle_get_class(self, _self, annotations, name, value):
53
+ if hasattr(annotations, 'get'):
54
+ annotation = annotations.get(name)
55
+ if annotation:
56
+ annotation_origin = type_safe_cache.get_origin(annotation)
57
+ if annotation_origin is Annotated:
58
+ self.handle_get_class__annotated(annotation, name, value)
59
+ elif annotation_origin is dict:
60
+ value = self.handle_get_class__dict(_self, name, value)
61
+ return value
62
+
63
+ def handle_special_generic_alias(self, _super, _self, name, value):
64
+ immutable_vars = type_safe_cache.get_class_immutable_vars(_self.__class__) # todo: refactor this section into a separate method
65
+ if name in immutable_vars:
66
+ expected_type = immutable_vars[name]
67
+ current_type = type if value is type else type(value)
68
+ type_safe_validation.validate_if__types_are_compatible_for_assigment(name, current_type, expected_type)
69
+ _super.__setattr__(name, value)
70
+ return True
71
+ return False
72
+
73
+ def setattr(self, _super, _self, name, value):
74
+ if type_safe_validation.check_if__value_is__special_generic_alias(value):
75
+ if self.handle_special_generic_alias(_super, _self, name, value):
76
+ return
77
+
78
+ annotations = dict(type_safe_cache.get_obj_annotations(_self))
79
+
80
+ if not annotations: # can't do type safety checks if the class does not have annotations
81
+ return _super.__setattr__(name, value)
82
+
83
+ if value is not None:
84
+ value = self.resolve_value (_self, annotations, name, value)
85
+ value = self.handle_get_class(_self, annotations, name, value)
86
+ else:
87
+ type_safe_validation.validate_if_value_has_been_set(_self, annotations, name, value)
88
+
89
+ _super.__setattr__(name, value)
90
+
91
+
92
+ type_safe_step_set_attr = Type_Safe__Step__Set_Attr()
File without changes
@@ -1,73 +1,10 @@
1
1
  # todo add tests
2
2
  import sys
3
- from types import SimpleNamespace
4
- from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
5
-
3
+ from types import SimpleNamespace
6
4
 
7
5
  class __(SimpleNamespace):
8
6
  pass
9
7
 
10
- # Backport implementations of get_origin and get_args for Python 3.7
11
- if sys.version_info < (3, 8):
12
- def get_origin(tp):
13
- import typing
14
- if isinstance(tp, typing._GenericAlias):
15
- return tp.__origin__
16
- elif tp is typing.Generic:
17
- return typing.Generic
18
- else:
19
- return None
20
-
21
- def get_args(tp):
22
- import typing
23
- if isinstance(tp, typing._GenericAlias):
24
- return tp.__args__
25
- else:
26
- return ()
27
- else:
28
- from typing import get_origin, get_args, List, Tuple, Dict, Type, _GenericAlias, ForwardRef
29
-
30
-
31
- def are_types_compatible_for_assigment(source_type, target_type):
32
- import types
33
- import typing
34
-
35
- if isinstance(target_type, str): # If the "target_type" is a forward reference (string), handle it here.
36
- if target_type == source_type.__name__: # Simple check: does the string match the actual class name
37
- return True
38
- if source_type is target_type:
39
- return True
40
- if source_type is int and target_type is float:
41
- return True
42
- if target_type in source_type.__mro__: # this means that the source_type has the target_type has of its base types
43
- return True
44
- if target_type is callable: # handle case where callable was used as the target type
45
- if source_type is types.MethodType: # and a method or function was used as the source type
46
- return True
47
- if source_type is types.FunctionType:
48
- return True
49
- if source_type is staticmethod:
50
- return True
51
- if target_type is typing.Any:
52
- return True
53
- return False
54
-
55
- def are_types_magic_mock(source_type, target_type):
56
- from unittest.mock import MagicMock
57
- if isinstance(source_type, MagicMock):
58
- return True
59
- if isinstance(target_type, MagicMock):
60
- return True
61
- if source_type is MagicMock:
62
- return True
63
- if target_type is MagicMock:
64
- return True
65
- # if class_full_name(source_type) == 'unittest.mock.MagicMock':
66
- # return True
67
- # if class_full_name(target_type) == 'unittest.mock.MagicMock':
68
- # return True
69
- return False
70
-
71
8
  def base_classes(cls):
72
9
  if type(cls) is type:
73
10
  target = cls
@@ -106,46 +43,6 @@ def class_full_name(target):
106
43
  type_name = type_target.__name__
107
44
  return f'{type_module}.{type_name}'
108
45
 
109
- def convert_dict_to_value_from_obj_annotation(target, attr_name, value): # todo: refactor this with code from convert_str_to_value_from_obj_annotation since it is mostly the same
110
- if target is not None and attr_name is not None:
111
- if hasattr(target, '__annotations__'):
112
- obj_annotations = target.__annotations__
113
- if hasattr(obj_annotations,'get'):
114
- attribute_annotation = obj_annotations.get(attr_name)
115
- if 'Type_Safe' in base_classes_names(attribute_annotation):
116
- return attribute_annotation(**value)
117
- return value
118
-
119
- def convert_to_value_from_obj_annotation(target, attr_name, value): # todo: see the side effects of doing this for all ints and floats
120
-
121
- from osbot_utils.helpers.Guid import Guid
122
- from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
123
- from osbot_utils.helpers.Random_Guid import Random_Guid
124
- from osbot_utils.helpers.Safe_Id import Safe_Id
125
- from osbot_utils.helpers.Str_ASCII import Str_ASCII
126
-
127
- TYPE_SAFE__CONVERT_VALUE__SUPPORTED_TYPES = [Guid, Random_Guid, Safe_Id, Str_ASCII, Timestamp_Now]
128
-
129
- if target is not None and attr_name is not None:
130
- if hasattr(target, '__annotations__'):
131
- obj_annotations = target.__annotations__
132
- if hasattr(obj_annotations,'get'):
133
- attribute_annotation = obj_annotations.get(attr_name)
134
- if attribute_annotation:
135
- origin = get_origin(attribute_annotation) # Add handling for Type[T] annotations
136
- if origin is type and isinstance(value, str):
137
- try: # Convert string path to actual type
138
- if len(value.rsplit('.', 1)) > 1:
139
- module_name, class_name = value.rsplit('.', 1)
140
- module = __import__(module_name, fromlist=[class_name])
141
- return getattr(module, class_name)
142
- except (ValueError, ImportError, AttributeError) as e:
143
- raise ValueError(f"Could not convert '{value}' to type: {str(e)}")
144
-
145
- if attribute_annotation in TYPE_SAFE__CONVERT_VALUE__SUPPORTED_TYPES: # for now hard-coding this to just these types until we understand the side effects
146
- return attribute_annotation(value)
147
- return value
148
-
149
46
 
150
47
  def default_value(target : type):
151
48
  try:
@@ -358,93 +255,6 @@ def obj_get_value(target=None, key=None, default=None):
358
255
  def obj_values(target=None):
359
256
  return list(obj_dict(target).values())
360
257
 
361
- def raise_exception_on_obj_type_annotation_mismatch(target, attr_name, value):
362
- if value_type_matches_obj_annotation_for_attr(target, attr_name, value) is False: # handle case with normal types
363
- if value_type_matches_obj_annotation_for_union_and_annotated(target, attr_name, value) is True: # handle union cases
364
- return # this is done like this because value_type_matches_obj_annotation_for_union_attr will return None when there is no Union objects
365
- raise TypeError(f"Invalid type for attribute '{attr_name}'. Expected '{target.__annotations__.get(attr_name)}' but got '{type(value)}'")
366
-
367
- def obj_attribute_annotation(target, attr_name):
368
- if target is not None and attr_name is not None:
369
- if hasattr(target, '__annotations__'):
370
- obj_annotations = target.__annotations__
371
- if hasattr(obj_annotations,'get'):
372
- attribute_annotation = obj_annotations.get(attr_name)
373
- return attribute_annotation
374
- return None
375
-
376
- def obj_is_attribute_annotation_of_type(target, attr_name, expected_type):
377
- attribute_annotation = obj_attribute_annotation(target, attr_name)
378
- if expected_type is attribute_annotation:
379
- return True
380
- if expected_type is type(attribute_annotation):
381
- return True
382
- if expected_type is get_origin(attribute_annotation): # handle genericAlias
383
- return True
384
- return False
385
-
386
- def obj_is_type_union_compatible(var_type, compatible_types):
387
- from typing import Union
388
-
389
- origin = get_origin(var_type)
390
- if isinstance(var_type, _GenericAlias) and origin is type: # Add handling for Type[T]
391
- return type in compatible_types # Allow if 'type' is in compatible types
392
- if origin is Union: # For Union types, including Optionals
393
- args = get_args(var_type) # Get the argument types
394
- for arg in args: # Iterate through each argument in the Union
395
- if not (arg in compatible_types or arg is type(None)): # Check if the argument is either in the compatible_types or is type(None)
396
- return False # If any arg doesn't meet the criteria, return False immediately
397
- return True # If all args are compatible, return True
398
- return var_type in compatible_types or var_type is type(None) # Check for direct compatibility or type(None) for non-Union types
399
-
400
-
401
- def value_type_matches_obj_annotation_for_union_and_annotated(target, attr_name, value):
402
- from osbot_utils.helpers.python_compatibility.python_3_8 import Annotated
403
- from typing import Union, get_origin, get_args
404
-
405
- value_type = type(value)
406
- attribute_annotation = obj_attribute_annotation(target, attr_name)
407
- origin = get_origin(attribute_annotation)
408
-
409
- if origin is Union: # Handle Union types (including Optional)
410
- args = get_args(attribute_annotation)
411
- return value_type in args
412
-
413
- # todo: refactor the logic below to a separate method (and check for duplicate code with other get_origin usage)
414
- if origin is Annotated: # Handle Annotated types
415
- args = get_args(attribute_annotation)
416
- base_type = args[0] # First argument is the base type
417
- base_origin = get_origin(base_type)
418
-
419
- if base_origin is None: # Non-container types
420
- return isinstance(value, base_type)
421
-
422
- if base_origin in (list, List): # Handle List types
423
- if not isinstance(value, list):
424
- return False
425
- item_type = get_args(base_type)[0]
426
- return all(isinstance(item, item_type) for item in value)
427
-
428
- if base_origin in (tuple, Tuple): # Handle Tuple types
429
- if not isinstance(value, tuple):
430
- return False
431
- item_types = get_args(base_type)
432
- return len(value) == len(item_types) and all(
433
- isinstance(item, item_type)
434
- for item, item_type in zip(value, item_types)
435
- )
436
-
437
- if base_origin in (dict, Dict): # Handle Dict types
438
- if not isinstance(value, dict):
439
- return False
440
- key_type, value_type = get_args(base_type)
441
- return all(isinstance(k, key_type) and isinstance(v, value_type)
442
- for k, v in value.items())
443
-
444
- # todo: add support for for other typing constructs
445
- 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)
446
-
447
-
448
258
  def pickle_save_to_bytes(target: object) -> bytes:
449
259
  import pickle
450
260
  return pickle.dumps(target)
@@ -457,47 +267,32 @@ def pickle_load_from_bytes(pickled_data: bytes):
457
267
  except Exception:
458
268
  return {}
459
269
 
460
- def all_annotations(target):
461
- annotations = {}
462
- if hasattr(target.__class__, '__mro__'):
463
- for base in reversed(target.__class__.__mro__):
464
- if hasattr(base, '__annotations__'):
465
- annotations.update(base.__annotations__)
466
- return annotations
467
-
468
- def value_type_matches_obj_annotation_for_attr(target, attr_name, value):
469
- import typing
470
- annotations = all_annotations(target)
471
- attr_type = annotations.get(attr_name)
472
- if attr_type:
473
- origin_attr_type = get_origin(attr_type) # to handle when type definition contains a generic
474
- if origin_attr_type is type: # Add handling for Type[T]
475
- type_arg = get_args(attr_type)[0] # Get T from Type[T]
476
- if type_arg == value:
477
- return True
478
- if isinstance(type_arg, (str, ForwardRef)): # Handle forward reference
479
- type_arg = target.__class__ # If it's a forward reference, the target class should be the containing class
480
- return isinstance(value, type) and issubclass(value, type_arg) # Check that value is a type and is subclass of type_arg
481
-
482
- if origin_attr_type is Annotated: # if the type is Annotated
483
- args = get_args(attr_type)
484
- origin_attr_type = args[0]
485
-
486
- elif origin_attr_type is typing.Union:
487
- args = get_args(attr_type)
488
- 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] )
489
- attr_type = args[0]
490
- origin_attr_type = get_origin(attr_type)
491
-
492
- if origin_attr_type:
493
- attr_type = origin_attr_type
494
- value_type = type(value)
495
- if are_types_compatible_for_assigment(source_type=value_type, target_type=attr_type):
496
- return True
497
- if are_types_magic_mock(source_type=value_type, target_type=attr_type):
498
- return True
499
- return value_type is attr_type
500
- return None
270
+ # todo: see if it is possible to add recursive protection to this logic
271
+ def serialize_to_dict(obj):
272
+ from decimal import Decimal
273
+ from enum import Enum
274
+ from typing import List
275
+
276
+ if isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None:
277
+ return obj
278
+ elif isinstance(obj, Enum):
279
+ return obj.name
280
+ elif isinstance(obj, type):
281
+ return f"{obj.__module__}.{obj.__name__}" # save the full type name
282
+ elif isinstance(obj, list) or isinstance(obj, List):
283
+ return [serialize_to_dict(item) for item in obj]
284
+ elif isinstance(obj, dict):
285
+ return {key: serialize_to_dict(value) for key, value in obj.items()}
286
+ elif hasattr(obj, "__dict__"):
287
+ data = {} # todo: look at a more advanced version which saved the type of the object, for example with {'__type__': type(obj).__name__}
288
+ for key, value in obj.__dict__.items():
289
+ if key.startswith('__') is False: # don't process internal variables (for example the ones set by @cache_on_self)
290
+ data[key] = serialize_to_dict(value) # Recursive call for complex types
291
+ return data
292
+ else:
293
+ raise TypeError(f"Type {type(obj)} not serializable")
294
+
295
+
501
296
 
502
297
 
503
298
 
@@ -1,6 +1,4 @@
1
1
  # todo refactor into Status class
2
- import traceback
3
-
4
2
  from osbot_utils.utils.Python_Logger import Python_Logger
5
3
 
6
4
  class Status:
osbot_utils/version CHANGED
@@ -1 +1 @@
1
- v2.11.0
1
+ v2.13.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: osbot_utils
3
- Version: 2.11.0
3
+ Version: 2.13.0
4
4
  Summary: OWASP Security Bot - Utils
5
5
  License: MIT
6
6
  Author: Dinis Cruz
@@ -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-v2.11.0-blue)
26
+ ![Current Release](https://img.shields.io/badge/release-v2.13.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