osbot-utils 1.30.0__py3-none-any.whl → 1.32.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.
@@ -1,304 +1,4 @@
1
- # todo: find a way to add these documentations strings to a separate location so that
2
- # the code is not polluted with them (like in the example below)
3
- # the data is available in IDE's code complete
4
- import functools
5
- import inspect
6
- import sys
7
- import types
8
- import typing
9
- from decimal import Decimal
10
- from enum import Enum, EnumMeta
11
- from typing import List
12
- from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List
13
- from osbot_utils.utils.Dev import pprint
14
- from osbot_utils.utils.Json import json_parse
15
- from osbot_utils.utils.Misc import list_set
16
- from osbot_utils.utils.Objects import default_value, value_type_matches_obj_annotation_for_attr, \
17
- raise_exception_on_obj_type_annotation_mismatch, obj_is_attribute_annotation_of_type, enum_from_value, \
18
- obj_is_type_union_compatible, value_type_matches_obj_annotation_for_union_attr
1
+ from osbot_utils.base_classes.Type_Safe import Type_Safe
19
2
 
20
- # Backport implementations of get_origin and get_args for Python 3.7
21
- if sys.version_info < (3, 8):
22
- def get_origin(tp):
23
- if isinstance(tp, typing._GenericAlias):
24
- return tp.__origin__
25
- elif tp is typing.Generic:
26
- return typing.Generic
27
- else:
28
- return None
29
-
30
- def get_args(tp):
31
- if isinstance(tp, typing._GenericAlias):
32
- return tp.__args__
33
- else:
34
- return ()
35
- else:
36
- from typing import get_origin, get_args
37
-
38
- if sys.version_info >= (3, 10):
39
- NoneType = types.NoneType
40
- else:
41
- NoneType = type(None)
42
-
43
- immutable_types = (bool, int, float, complex, str, tuple, frozenset, bytes, NoneType, EnumMeta)
44
-
45
-
46
- #todo: see if we can also add type safety to method execution
47
- # for example if we have an method like def add_node(self, title: str, call_index: int):
48
- # throw an exception if the type of the value passed in is not the same as the one defined in the method
49
-
50
- class Kwargs_To_Self:
51
-
52
- def __init__(self, **kwargs):
53
- """
54
- Initialize an instance of the derived class, strictly assigning provided keyword
55
- arguments to corresponding instance attributes.
56
-
57
- Parameters:
58
- **kwargs: Variable length keyword arguments.
59
-
60
- Raises:
61
- Exception: If a key from kwargs does not correspond to any attribute
62
- pre-defined in the class, an exception is raised to prevent
63
- setting an undefined attribute.
64
-
65
- """
66
- # if 'disable_type_safety' in kwargs: # special case
67
- # self.__type_safety__ = kwargs['disable_type_safety'] is False
68
- # del kwargs['disable_type_safety']
69
-
70
- for (key, value) in self.__cls_kwargs__().items(): # assign all default values to self
71
- 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
72
- raise_exception_on_obj_type_annotation_mismatch(self, key, value)
73
- if hasattr(self, key):
74
- existing_value = getattr(self, key)
75
- if existing_value is not None:
76
- setattr(self, key, existing_value)
77
- continue
78
- setattr(self, key, value)
79
-
80
- for (key, value) in kwargs.items(): # overwrite with values provided in ctor
81
- if hasattr(self, key):
82
- if value is not None: # prevent None values from overwriting existing values, which is quite common in default constructors
83
- setattr(self, key, value)
84
- else:
85
- raise Exception(f"{self.__class__.__name__} has no attribute '{key}' and cannot be assigned the value '{value}'. "
86
- f"Use {self.__class__.__name__}.__default_kwargs__() see what attributes are available")
87
-
88
- def __enter__(self): return self
89
- def __exit__(self, exc_type, exc_val, exc_tb): pass
90
-
91
- def __setattr__(self, name, value):
92
- if not hasattr(self, '__annotations__'): # can't do type safety checks if the class does not have annotations
93
- return super().__setattr__(name, value)
94
-
95
- # if self.__type_safety__:
96
- # if self.__lock_attributes__:
97
- # todo: this can't work on all, current hypothesis is that this will work for the values that are explicitly set
98
- # if not hasattr(self, name):
99
- # raise AttributeError(f"'[Object Locked] Current object is locked (with __lock_attributes__=True) which prevents new attributes allocations (i.e. setattr calls). In this case {type(self).__name__}' object has no attribute '{name}'") from None
100
-
101
- if value is not None:
102
- check_1 = value_type_matches_obj_annotation_for_attr (self, name, value)
103
- check_2 = value_type_matches_obj_annotation_for_union_attr(self, name, value)
104
- if (check_1 is False and check_2 is None or
105
- check_1 is None and check_2 is False or
106
- check_1 is False and check_2 is False ): # fix for type safety assigment on Union vars
107
- raise Exception(f"Invalid type for attribute '{name}'. Expected '{self.__annotations__.get(name)}' but got '{type(value)}'")
108
- else:
109
- if hasattr(self, name) and self.__annotations__.get(name) : # don't allow previously set variables to be set to None
110
- if getattr(self, name) is not None: # unless it is already set to None
111
- raise Exception(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)}'")
112
-
113
- super().__setattr__(name, value)
114
-
115
- def __attr_names__(self):
116
- return list_set(self.__locals__())
117
-
118
- @classmethod
119
- def __cls_kwargs__(cls, include_base_classes=True):
120
- """Return current class dictionary of class level variables and their values."""
121
- kwargs = {}
122
-
123
- for base_cls in inspect.getmro(cls):
124
- if base_cls is object: # Skip the base 'object' class
125
- continue
126
- for k, v in vars(base_cls).items():
127
- # todo: refactor this logic since it is weird to start with a if not..., and then if ... continue (all these should be if ... continue )
128
- if not k.startswith('__') and not isinstance(v, types.FunctionType): # remove instance functions
129
- if isinstance(v, classmethod): # also remove class methods
130
- continue
131
- if type(v) is functools._lru_cache_wrapper: # todo, find better way to handle edge cases like this one (which happens when the @cache decorator is used in a instance method that uses Kwargs_To_Self)
132
- continue
133
- if (k in kwargs) is False: # do not set the value is it has already been set
134
- kwargs[k] = v
135
-
136
- if hasattr(base_cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
137
- for var_name, var_type in base_cls.__annotations__.items():
138
- if hasattr(base_cls, var_name) is False: # only add if it has not already been defined
139
- if var_name in kwargs:
140
- continue
141
- var_value = cls.__default__value__(var_type)
142
- kwargs[var_name] = var_value
143
- else:
144
- var_value = getattr(base_cls, var_name)
145
- if var_value is not None: # allow None assignments on ctor since that is a valid use case
146
- if var_type and not isinstance(var_value, var_type): # check type
147
- exception_message = f"variable '{var_name}' is defined as type '{var_type}' but has value '{var_value}' of type '{type(var_value)}'"
148
- raise Exception(exception_message)
149
- if var_type not in immutable_types and var_name.startswith('__') is False: # if var_type is not one of the immutable_types or is an __ internal
150
- #todo: fix type safety bug that I believe is caused here
151
- if obj_is_type_union_compatible(var_type, immutable_types) is False: # if var_type is not something like Optional[Union[int, str]]
152
- if type(var_type) not in immutable_types:
153
- exception_message = f"variable '{var_name}' is defined as type '{var_type}' which is not supported by Kwargs_To_Self, with only the following immutable types being supported: '{immutable_types}'"
154
- raise Exception(exception_message)
155
- if include_base_classes is False:
156
- break
157
- return kwargs
158
-
159
- @classmethod
160
- def __default__value__(cls, var_type):
161
- if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
162
- return set()
163
- if get_origin(var_type) is set:
164
- return set() # todo: add Type_Safe__Set
165
-
166
- if var_type is typing.Dict:
167
- return {}
168
- if get_origin(var_type) is dict:
169
- return {} # todo: add Type_Safe__Dict
170
-
171
- if var_type is typing.List:
172
- return [] # handle case when List was used with no type information provided
173
- if get_origin(var_type) is list: # if we have list defined as list[type]
174
- item_type = get_args(var_type)[0] # get the type that was defined
175
- return Type_Safe__List(expected_type=item_type) # and used it as expected_type in Type_Safe__List
176
- else:
177
- return default_value(var_type) # for all other cases call default_value, which will try to create a default instance
178
-
179
- def __default_kwargs__(self):
180
- """Return entire (including base classes) dictionary of class level variables and their values."""
181
- kwargs = {}
182
- cls = type(self)
183
- for base_cls in inspect.getmro(cls): # Traverse the inheritance hierarchy and collect class-level attributes
184
- if base_cls is object: # Skip the base 'object' class
185
- continue
186
- for k, v in vars(base_cls).items():
187
- if not k.startswith('__') and not isinstance(v, types.FunctionType): # remove instance functions
188
- if not isinstance(v, classmethod):
189
- kwargs[k] = v
190
- # add the vars defined with the annotations
191
- if hasattr(base_cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
192
- for var_name, var_type in base_cls.__annotations__.items():
193
- var_value = getattr(self, var_name)
194
- kwargs[var_name] = var_value
195
-
196
- return kwargs
197
-
198
- def __kwargs__(self):
199
- """Return a dictionary of the current instance's attribute values including inherited class defaults."""
200
- kwargs = {}
201
- # Update with instance-specific values
202
- for key, value in self.__default_kwargs__().items():
203
- kwargs[key] = self.__getattribute__(key)
204
- # if hasattr(self, key):
205
- # kwargs[key] = self.__getattribute__(key)
206
- # else:
207
- # kwargs[key] = value # todo: see if this is stil a valid scenario
208
- return kwargs
209
-
210
-
211
- def __locals__(self):
212
- """Return a dictionary of the current instance's attribute values."""
213
- kwargs = self.__kwargs__()
214
-
215
- if not isinstance(vars(self), types.FunctionType):
216
- for k, v in vars(self).items():
217
- if not isinstance(v, types.FunctionType) and not isinstance(v,classmethod):
218
- if k.startswith('__') is False:
219
- kwargs[k] = v
220
- return kwargs
221
-
222
- @classmethod
223
- def __schema__(cls):
224
- if hasattr(cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
225
- return cls.__annotations__
226
- return {}
227
-
228
- # global methods added to any class that base classes this
229
- # todo: see if there should be a prefix on these methods, to make it easier to spot them
230
- # of if these are actually that useful that they should be added like this
231
- def json(self):
232
- return self.serialize_to_dict()
233
-
234
- def merge_with(self, target):
235
- original_attrs = {k: v for k, v in self.__dict__.items() if k not in target.__dict__} # Store the original attributes of self that should be retained.
236
- self.__dict__ = target.__dict__ # Set the target's __dict__ to self, now self and target share the same __dict__.
237
- self.__dict__.update(original_attrs) # Reassign the original attributes back to self.
238
- return self
239
-
240
- # def locked(self, value=True): # todo: figure out best way to do this (maybe???)
241
- # self.__lock_attributes__ = value # : update, with the latest changes were we don't show internals on __locals__() this might be a good way to do this
242
- # return self
243
-
244
- def reset(self):
245
- for k,v in self.__cls_kwargs__().items():
246
- setattr(self, k, v)
247
-
248
- def update_from_kwargs(self, **kwargs):
249
- """Update instance attributes with values from provided keyword arguments."""
250
- for key, value in kwargs.items():
251
- if value is not None:
252
- if hasattr(self,'__annotations__'): # can only do type safety checks if the class does not have annotations
253
- if value_type_matches_obj_annotation_for_attr(self, key, value) is False:
254
- raise Exception(f"Invalid type for attribute '{key}'. Expected '{self.__annotations__.get(key)}' but got '{type(value)}'")
255
- setattr(self, key, value)
256
- return self
257
-
258
-
259
- def deserialize_from_dict(self, data):
260
- for key, value in data.items():
261
- if hasattr(self, key) and isinstance(getattr(self, key), Kwargs_To_Self):
262
- getattr(self, key).deserialize_from_dict(value) # Recursive call for complex nested objects
263
- else:
264
- if hasattr(self, '__annotations__'): # can only do type safety checks if the class does not have annotations
265
- if obj_is_attribute_annotation_of_type(self, key, EnumMeta): # Handle the case when the value is an Enum
266
- enum_type = getattr(self, '__annotations__').get(key)
267
- if type(value) is not enum_type: # If the value is not already of the target type
268
- value = enum_from_value(enum_type, value) # Try to resolve the value into the enum
269
-
270
- setattr(self, key, value) # Direct assignment for primitive types and other structures
271
-
272
- return self
273
-
274
- def serialize_to_dict(self): # todo: see if we need this method or if the .json() is enough
275
- return serialize_to_dict(self)
276
-
277
- def print(self):
278
- pprint(serialize_to_dict(self))
279
-
280
- @classmethod
281
- def from_json(cls, json_data):
282
- if type(json_data) is str:
283
- json_data = json_parse(json_data)
284
- 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)
285
- return cls().deserialize_from_dict(json_data)
286
- return None
287
-
288
- # todo: see if it is possible to add recursive protection to this logic
289
- def serialize_to_dict(obj):
290
- if isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None:
291
- return obj
292
- elif isinstance(obj, Enum):
293
- return obj.name
294
- elif isinstance(obj, list) or isinstance(obj, List):
295
- return [serialize_to_dict(item) for item in obj]
296
- elif isinstance(obj, dict):
297
- return {key: serialize_to_dict(value) for key, value in obj.items()}
298
- elif hasattr(obj, "__dict__"):
299
- data = {} # todo: look at a more advanced version which saved the type of the object, for example with {'__type__': type(obj).__name__}
300
- for key, value in obj.__dict__.items():
301
- data[key] = serialize_to_dict(value) # Recursive call for complex types
302
- return data
303
- else:
304
- raise TypeError(f"Type {type(obj)} not serializable")
3
+ # todo: refactor all of Kwargs_To_Self to use Type_Safe
4
+ Kwargs_To_Self = Type_Safe
@@ -1,6 +1,301 @@
1
- from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
1
+ # todo: find a way to add these documentations strings to a separate location so that
2
+ # the code is not polluted with them (like in the example below)
3
+ # the data is available in IDE's code complete
4
+ import functools
5
+ import inspect
6
+ import sys
7
+ import types
8
+ import typing
9
+ from decimal import Decimal
10
+ from enum import Enum, EnumMeta
11
+ from typing import List
12
+ from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List
13
+ from osbot_utils.utils.Dev import pprint
14
+ from osbot_utils.utils.Json import json_parse
15
+ from osbot_utils.utils.Misc import list_set
16
+ from osbot_utils.utils.Objects import default_value, value_type_matches_obj_annotation_for_attr, \
17
+ raise_exception_on_obj_type_annotation_mismatch, obj_is_attribute_annotation_of_type, enum_from_value, \
18
+ obj_is_type_union_compatible, value_type_matches_obj_annotation_for_union_attr, \
19
+ convert_dict_to_value_from_obj_annotation
2
20
 
3
- # todo: refactor all of Kwargs_To_Self into this class (in a way to minimize the side effects, since
4
- # Kwargs_To_Self is used in many places in the codebase)
21
+ # Backport implementations of get_origin and get_args for Python 3.7
22
+ if sys.version_info < (3, 8):
23
+ def get_origin(tp):
24
+ if isinstance(tp, typing._GenericAlias):
25
+ return tp.__origin__
26
+ elif tp is typing.Generic:
27
+ return typing.Generic
28
+ else:
29
+ return None
5
30
 
6
- Type_Safe = Kwargs_To_Self
31
+ def get_args(tp):
32
+ if isinstance(tp, typing._GenericAlias):
33
+ return tp.__args__
34
+ else:
35
+ return ()
36
+ else:
37
+ from typing import get_origin, get_args
38
+
39
+ if sys.version_info >= (3, 10):
40
+ NoneType = types.NoneType
41
+ else:
42
+ NoneType = type(None)
43
+
44
+ immutable_types = (bool, int, float, complex, str, tuple, frozenset, bytes, NoneType, EnumMeta)
45
+
46
+
47
+ #todo: see if we can also add type safety to method execution
48
+ # for example if we have an method like def add_node(self, title: str, call_index: int):
49
+ # throw an exception if the type of the value passed in is not the same as the one defined in the method
50
+
51
+ class Type_Safe:
52
+
53
+ def __init__(self, **kwargs):
54
+ """
55
+ Initialize an instance of the derived class, strictly assigning provided keyword
56
+ arguments to corresponding instance attributes.
57
+
58
+ Parameters:
59
+ **kwargs: Variable length keyword arguments.
60
+
61
+ Raises:
62
+ Exception: If a key from kwargs does not correspond to any attribute
63
+ pre-defined in the class, an exception is raised to prevent
64
+ setting an undefined attribute.
65
+
66
+ """
67
+ # if 'disable_type_safety' in kwargs: # special case
68
+ # self.__type_safety__ = kwargs['disable_type_safety'] is False
69
+ # del kwargs['disable_type_safety']
70
+
71
+ for (key, value) in self.__cls_kwargs__().items(): # assign all default values to self
72
+ 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
73
+ raise_exception_on_obj_type_annotation_mismatch(self, key, value)
74
+ if hasattr(self, key):
75
+ existing_value = getattr(self, key)
76
+ if existing_value is not None:
77
+ setattr(self, key, existing_value)
78
+ continue
79
+ setattr(self, key, value)
80
+
81
+ for (key, value) in kwargs.items(): # overwrite with values provided in ctor
82
+ if hasattr(self, key):
83
+ if value is not None: # prevent None values from overwriting existing values, which is quite common in default constructors
84
+ setattr(self, key, value)
85
+ else:
86
+ raise ValueError(f"{self.__class__.__name__} has no attribute '{key}' and cannot be assigned the value '{value}'. "
87
+ f"Use {self.__class__.__name__}.__default_kwargs__() see what attributes are available")
88
+
89
+ def __enter__(self): return self
90
+ def __exit__(self, exc_type, exc_val, exc_tb): pass
91
+
92
+ def __setattr__(self, name, value):
93
+ if not hasattr(self, '__annotations__'): # can't do type safety checks if the class does not have annotations
94
+ return super().__setattr__(name, value)
95
+
96
+ if value is not None:
97
+ if type(value) is dict:
98
+ value = convert_dict_to_value_from_obj_annotation(self, name, value)
99
+ check_1 = value_type_matches_obj_annotation_for_attr (self, name, value)
100
+ check_2 = value_type_matches_obj_annotation_for_union_attr(self, name, value)
101
+ if (check_1 is False and check_2 is None or
102
+ check_1 is None and check_2 is False or
103
+ check_1 is False and check_2 is False ): # fix for type safety assigment on Union vars
104
+ raise ValueError(f"Invalid type for attribute '{name}'. Expected '{self.__annotations__.get(name)}' but got '{type(value)}'")
105
+ else:
106
+ if hasattr(self, name) and self.__annotations__.get(name) : # don't allow previously set variables to be set to None
107
+ if getattr(self, name) is not None: # unless it is already set to None
108
+ 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)}'")
109
+
110
+ super().__setattr__(name, value)
111
+
112
+ def __attr_names__(self):
113
+ return list_set(self.__locals__())
114
+
115
+ @classmethod
116
+ def __cls_kwargs__(cls, include_base_classes=True):
117
+ """Return current class dictionary of class level variables and their values."""
118
+ kwargs = {}
119
+
120
+ for base_cls in inspect.getmro(cls):
121
+ if base_cls is object: # Skip the base 'object' class
122
+ continue
123
+ for k, v in vars(base_cls).items():
124
+ # todo: refactor this logic since it is weird to start with a if not..., and then if ... continue (all these should be if ... continue )
125
+ if not k.startswith('__') and not isinstance(v, types.FunctionType): # remove instance functions
126
+ if isinstance(v, classmethod): # also remove class methods
127
+ continue
128
+ if type(v) is functools._lru_cache_wrapper: # todo, find better way to handle edge cases like this one (which happens when the @cache decorator is used in a instance method that uses Kwargs_To_Self)
129
+ continue
130
+ if (k in kwargs) is False: # do not set the value is it has already been set
131
+ kwargs[k] = v
132
+
133
+ if hasattr(base_cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
134
+ for var_name, var_type in base_cls.__annotations__.items():
135
+ if hasattr(base_cls, var_name) is False: # only add if it has not already been defined
136
+ if var_name in kwargs:
137
+ continue
138
+ var_value = cls.__default__value__(var_type)
139
+ kwargs[var_name] = var_value
140
+ else:
141
+ var_value = getattr(base_cls, var_name)
142
+ if var_value is not None: # allow None assignments on ctor since that is a valid use case
143
+ if var_type and not isinstance(var_value, var_type): # check type
144
+ exception_message = f"variable '{var_name}' is defined as type '{var_type}' but has value '{var_value}' of type '{type(var_value)}'"
145
+ raise ValueError(exception_message)
146
+ if var_type not in immutable_types and var_name.startswith('__') is False: # if var_type is not one of the immutable_types or is an __ internal
147
+ #todo: fix type safety bug that I believe is caused here
148
+ if obj_is_type_union_compatible(var_type, immutable_types) is False: # if var_type is not something like Optional[Union[int, str]]
149
+ if type(var_type) not in immutable_types:
150
+ exception_message = f"variable '{var_name}' is defined as type '{var_type}' which is not supported by Kwargs_To_Self, with only the following immutable types being supported: '{immutable_types}'"
151
+ raise ValueError(exception_message)
152
+ if include_base_classes is False:
153
+ break
154
+ return kwargs
155
+
156
+ @classmethod
157
+ def __default__value__(cls, var_type):
158
+ if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
159
+ return set()
160
+ if get_origin(var_type) is set:
161
+ return set() # todo: add Type_Safe__Set
162
+
163
+ if var_type is typing.Dict:
164
+ return {}
165
+ if get_origin(var_type) is dict:
166
+ return {} # todo: add Type_Safe__Dict
167
+
168
+ if var_type is typing.List:
169
+ return [] # handle case when List was used with no type information provided
170
+ if get_origin(var_type) is list: # if we have list defined as list[type]
171
+ item_type = get_args(var_type)[0] # get the type that was defined
172
+ return Type_Safe__List(expected_type=item_type) # and used it as expected_type in Type_Safe__List
173
+ else:
174
+ return default_value(var_type) # for all other cases call default_value, which will try to create a default instance
175
+
176
+ def __default_kwargs__(self):
177
+ """Return entire (including base classes) dictionary of class level variables and their values."""
178
+ kwargs = {}
179
+ cls = type(self)
180
+ for base_cls in inspect.getmro(cls): # Traverse the inheritance hierarchy and collect class-level attributes
181
+ if base_cls is object: # Skip the base 'object' class
182
+ continue
183
+ for k, v in vars(base_cls).items():
184
+ if not k.startswith('__') and not isinstance(v, types.FunctionType): # remove instance functions
185
+ if not isinstance(v, classmethod):
186
+ kwargs[k] = v
187
+ # add the vars defined with the annotations
188
+ if hasattr(base_cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
189
+ for var_name, var_type in base_cls.__annotations__.items():
190
+ var_value = getattr(self, var_name)
191
+ kwargs[var_name] = var_value
192
+
193
+ return kwargs
194
+
195
+ def __kwargs__(self):
196
+ """Return a dictionary of the current instance's attribute values including inherited class defaults."""
197
+ kwargs = {}
198
+ # Update with instance-specific values
199
+ for key, value in self.__default_kwargs__().items():
200
+ kwargs[key] = self.__getattribute__(key)
201
+ # if hasattr(self, key):
202
+ # kwargs[key] = self.__getattribute__(key)
203
+ # else:
204
+ # kwargs[key] = value # todo: see if this is stil a valid scenario
205
+ return kwargs
206
+
207
+
208
+ def __locals__(self):
209
+ """Return a dictionary of the current instance's attribute values."""
210
+ kwargs = self.__kwargs__()
211
+
212
+ if not isinstance(vars(self), types.FunctionType):
213
+ for k, v in vars(self).items():
214
+ if not isinstance(v, types.FunctionType) and not isinstance(v,classmethod):
215
+ if k.startswith('__') is False:
216
+ kwargs[k] = v
217
+ return kwargs
218
+
219
+ @classmethod
220
+ def __schema__(cls):
221
+ if hasattr(cls,'__annotations__'): # can only do type safety checks if the class does not have annotations
222
+ return cls.__annotations__
223
+ return {}
224
+
225
+ # global methods added to any class that base classes this
226
+ # todo: see if there should be a prefix on these methods, to make it easier to spot them
227
+ # of if these are actually that useful that they should be added like this
228
+ def json(self):
229
+ return self.serialize_to_dict()
230
+
231
+ def merge_with(self, target):
232
+ original_attrs = {k: v for k, v in self.__dict__.items() if k not in target.__dict__} # Store the original attributes of self that should be retained.
233
+ self.__dict__ = target.__dict__ # Set the target's __dict__ to self, now self and target share the same __dict__.
234
+ self.__dict__.update(original_attrs) # Reassign the original attributes back to self.
235
+ return self
236
+
237
+ # def locked(self, value=True): # todo: figure out best way to do this (maybe???)
238
+ # self.__lock_attributes__ = value # : update, with the latest changes were we don't show internals on __locals__() this might be a good way to do this
239
+ # return self
240
+
241
+ def reset(self):
242
+ for k,v in self.__cls_kwargs__().items():
243
+ setattr(self, k, v)
244
+
245
+ def update_from_kwargs(self, **kwargs):
246
+ """Update instance attributes with values from provided keyword arguments."""
247
+ for key, value in kwargs.items():
248
+ if value is not None:
249
+ if hasattr(self,'__annotations__'): # can only do type safety checks if the class does not have annotations
250
+ if value_type_matches_obj_annotation_for_attr(self, key, value) is False:
251
+ raise ValueError(f"Invalid type for attribute '{key}'. Expected '{self.__annotations__.get(key)}' but got '{type(value)}'")
252
+ setattr(self, key, value)
253
+ return self
254
+
255
+
256
+ def deserialize_from_dict(self, data):
257
+ for key, value in data.items():
258
+ if hasattr(self, key) and isinstance(getattr(self, key), Type_Safe):
259
+ getattr(self, key).deserialize_from_dict(value) # Recursive call for complex nested objects
260
+ else:
261
+ if hasattr(self, '__annotations__'): # can only do type safety checks if the class does not have annotations
262
+ if obj_is_attribute_annotation_of_type(self, key, EnumMeta): # Handle the case when the value is an Enum
263
+ enum_type = getattr(self, '__annotations__').get(key)
264
+ if type(value) is not enum_type: # If the value is not already of the target type
265
+ value = enum_from_value(enum_type, value) # Try to resolve the value into the enum
266
+
267
+ setattr(self, key, value) # Direct assignment for primitive types and other structures
268
+
269
+ return self
270
+
271
+ def serialize_to_dict(self): # todo: see if we need this method or if the .json() is enough
272
+ return serialize_to_dict(self)
273
+
274
+ def print(self):
275
+ pprint(serialize_to_dict(self))
276
+
277
+ @classmethod
278
+ def from_json(cls, json_data):
279
+ if type(json_data) is str:
280
+ json_data = json_parse(json_data)
281
+ 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)
282
+ return cls().deserialize_from_dict(json_data)
283
+ return None
284
+
285
+ # todo: see if it is possible to add recursive protection to this logic
286
+ def serialize_to_dict(obj):
287
+ if isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None:
288
+ return obj
289
+ elif isinstance(obj, Enum):
290
+ return obj.name
291
+ elif isinstance(obj, list) or isinstance(obj, List):
292
+ return [serialize_to_dict(item) for item in obj]
293
+ elif isinstance(obj, dict):
294
+ return {key: serialize_to_dict(value) for key, value in obj.items()}
295
+ elif hasattr(obj, "__dict__"):
296
+ data = {} # todo: look at a more advanced version which saved the type of the object, for example with {'__type__': type(obj).__name__}
297
+ for key, value in obj.__dict__.items():
298
+ data[key] = serialize_to_dict(value) # Recursive call for complex types
299
+ return data
300
+ else:
301
+ raise TypeError(f"Type {type(obj)} not serializable")
@@ -42,7 +42,7 @@ class Catch:
42
42
  if self.expected_error:
43
43
  self.assert_error_is(self.expected_error)
44
44
  if self.expect_exception and exception_type is None:
45
- raise Exception(f'Expected exception: {self.expected_error} but no exception was raised')
45
+ raise ValueError(f'Expected exception: {self.expected_error} but no exception was raised')
46
46
  if self.catch_exception:
47
47
  return True # returning true here will prevent the exception to be propagated (which is the objective of this class :) )
48
48
  return False
osbot_utils/utils/Env.py CHANGED
@@ -6,14 +6,28 @@ from osbot_utils.utils.Files import all_parent_folders, file_exists
6
6
  from osbot_utils.utils.Misc import list_set
7
7
  from osbot_utils.utils.Str import strip_quotes
8
8
 
9
- def env__home_root(): # todo: this should be refatored to be env__home__is__root
9
+
10
+ def env__home():
11
+ return get_env('HOME', '')
12
+
13
+ def env__home__is__root():
10
14
  return os.getenv('HOME') == '/root'
11
15
 
12
- def env__terminal_xterm(): # todo: this should be refatored to be env__terminal__is__xterm
16
+ def env__old_pwd():
17
+ return get_env('OLDPWD', '')
18
+
19
+ def env__pwd():
20
+ return get_env('PWD', '')
21
+
22
+ def env__old_pwd__remove(value):
23
+ return value.replace(env__old_pwd(), '')
24
+
25
+ def env__terminal__is__xterm():
13
26
  return os.getenv('TERM') == 'xterm'
14
27
 
15
- def env__not_terminal_xterm():
16
- return not env__terminal_xterm()
28
+ def env__terminal__is_not__xterm():
29
+ return not env__terminal__is__xterm()
30
+
17
31
 
18
32
  def platform_darwin():
19
33
  return platform == 'darwin'
@@ -21,6 +35,10 @@ def platform_darwin():
21
35
  def env_value(var_name):
22
36
  return env_vars().get(var_name, None)
23
37
 
38
+ def env_var_set(var_name):
39
+ value = os.getenv(var_name)
40
+ return value is not None and value != ''
41
+
24
42
  def env_vars_list():
25
43
  return list_set(env_vars())
26
44
 
@@ -109,6 +127,6 @@ def unload_dotenv(dotenv_path=None):
109
127
  env_unload_from_file(env_path) # Process it
110
128
  break # Stop after unloading the first .env file
111
129
 
112
-
113
- env_load = load_dotenv
114
- get_env = os.getenv
130
+ is_env_var_set = env_var_set
131
+ env_load = load_dotenv
132
+ get_env = os.getenv
@@ -340,6 +340,8 @@ class Files:
340
340
  def parent_folder_combine(file, path):
341
341
  return Files.path_combine(os.path.dirname(file),path)
342
342
 
343
+
344
+
343
345
  @staticmethod
344
346
  def pickle_save_to_file(object_to_save, path=None):
345
347
  path = path or temp_file(extension=".pickle")
@@ -491,12 +493,26 @@ def folders_names_in_folder(target):
491
493
  folders = folders_in_folder(target)
492
494
  return folders_names(folders)
493
495
 
496
+ def path_combine_safe(base_path, file_location, raise_exception=False): # handle possible directory transversal attacks
497
+ full_path = os.path.join(base_path, file_location) # Combine and normalize paths
498
+ normalised_base_path = os.path.normpath(base_path)
499
+ normalised_full_path = os.path.normpath(full_path)
500
+
501
+ if os.path.commonpath([normalised_base_path, normalised_full_path]) == normalised_base_path: # Check for directory traversal
502
+ return normalised_full_path
503
+ if raise_exception:
504
+ raise ValueError("Invalid file location: directory traversal attempt detected.")
505
+ return None
506
+
494
507
  def parent_folder_create(target):
495
508
  return folder_create(parent_folder(target))
496
509
 
497
510
  def parent_folder_exists(target):
498
511
  return folder_exists(parent_folder(target))
499
512
 
513
+ def parent_folder_name(target):
514
+ return folder_name(parent_folder(target))
515
+
500
516
  def parent_folder_not_exists(target):
501
517
  return parent_folder_exists(target) is False
502
518
 
osbot_utils/utils/Http.py CHANGED
@@ -22,6 +22,13 @@ def current_host_online(url_to_use=URL_CHECK_HOST_ONLINE):
22
22
  def dns_ip_address(host):
23
23
  return socket.gethostbyname(host)
24
24
 
25
+ def is_url_online(target):
26
+ try:
27
+ http_request(target, method='HEAD')
28
+ return True
29
+ except:
30
+ return False
31
+
25
32
  def is_port_open(host, port, timeout=0.5):
26
33
  return port_is_open(host=host, port=port, timeout=timeout)
27
34
 
@@ -69,6 +69,9 @@ def base_classes(cls):
69
69
  target = type(cls)
70
70
  return type_base_classes(target)
71
71
 
72
+ def base_classes_names(cls):
73
+ return [cls.__name__ for cls in base_classes(cls)]
74
+
72
75
  def class_functions_names(target):
73
76
  return list_set(class_functions(target))
74
77
 
@@ -89,6 +92,17 @@ def class_full_name(target):
89
92
  type_name = type_target.__name__
90
93
  return f'{type_module}.{type_name}'
91
94
 
95
+ def convert_dict_to_value_from_obj_annotation(target, attr_name, value):
96
+ if target is not None and attr_name is not None:
97
+ if hasattr(target, '__annotations__'):
98
+ obj_annotations = target.__annotations__
99
+ if hasattr(obj_annotations,'get'):
100
+ attribute_annotation = obj_annotations.get(attr_name)
101
+ if 'Type_Safe' in base_classes_names(attribute_annotation):
102
+ return attribute_annotation(**value)
103
+ return value
104
+
105
+
92
106
  def default_value(target : type):
93
107
  try:
94
108
  return target() # try to create the object using the default constructor
osbot_utils/version CHANGED
@@ -1 +1 @@
1
- v1.30.0
1
+ v1.32.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: osbot_utils
3
- Version: 1.30.0
3
+ Version: 1.32.0
4
4
  Summary: OWASP Security Bot - Utils
5
5
  Home-page: https://github.com/owasp-sbot/OSBot-Utils
6
6
  License: MIT
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
22
22
 
23
23
  Powerful Python util methods and classes that simplify common apis and tasks.
24
24
 
25
- ![Current Release](https://img.shields.io/badge/release-v1.30.0-blue)
25
+ ![Current Release](https://img.shields.io/badge/release-v1.32.0-blue)
26
26
  [![codecov](https://codecov.io/gh/owasp-sbot/OSBot-Utils/graph/badge.svg?token=GNVW0COX1N)](https://codecov.io/gh/owasp-sbot/OSBot-Utils)
27
27
 
28
28
 
@@ -1,8 +1,8 @@
1
1
  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
- osbot_utils/base_classes/Kwargs_To_Self.py,sha256=zahQ344JF13t4UO7xfnaSrcWPTXn052rMsjTFLpDunE,17450
5
- osbot_utils/base_classes/Type_Safe.py,sha256=PHBIXCRumEnXGognc1qKDJzcAK_vAsylEWfREG5nvjY,260
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=62eV2AnIAN-7VshhOpvWw0v_DZHJucLq10VQHjgCfX8,17089
6
6
  osbot_utils/base_classes/Type_Safe__List.py,sha256=-80C9OhsK6iDR2dAG8yNLAZV0qg5x3faqvSUigFCMJw,517
7
7
  osbot_utils/base_classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  osbot_utils/context_managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -240,7 +240,7 @@ osbot_utils/helpers/trace/Trace_Call__Stats.py,sha256=gmiotIrOXe2ssxodzQQ56t8eGT
240
240
  osbot_utils/helpers/trace/Trace_Call__View_Model.py,sha256=a40nn6agCEMd2ecsJ93n8vXij0omh0D69QilqwmN_ao,4545
241
241
  osbot_utils/helpers/trace/Trace_Files.py,sha256=SNpAmuBlSUS9NyVocgZ5vevzqVaIqoh622yZge3a53A,978
242
242
  osbot_utils/helpers/trace/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
243
- osbot_utils/testing/Catch.py,sha256=r8aBtJYpZljEJGsthqchavHNPNhusqc-U4ljojvBO7I,2228
243
+ osbot_utils/testing/Catch.py,sha256=HdNoKnrPBjvVj87XYN-Wa1zpo5z3oByURT6TKbd5QpQ,2229
244
244
  osbot_utils/testing/Duration.py,sha256=iBrczAuw6j3jXtG7ZPraT0PXbCILEcCplJbqei96deA,2217
245
245
  osbot_utils/testing/Hook_Method.py,sha256=A1t6WQzeR9hv5ddaz7ILtvKyJpRrRBRnftB-zgcIQpA,4126
246
246
  osbot_utils/testing/Log_To_Queue.py,sha256=pZQ7I1ne-H365a4WLS60oAD-B16pxIZO4suvCdaTW8U,1703
@@ -265,17 +265,17 @@ osbot_utils/utils/Assert.py,sha256=u9XLgYn91QvNWZGyPi29SjPJSXRHlm9andIn3NJEVog,1
265
265
  osbot_utils/utils/Call_Stack.py,sha256=MAq_0vMxnbeLfCe9qQz7GwJYaOuXpt3qtQwN6wiXsU0,6595
266
266
  osbot_utils/utils/Csv.py,sha256=oHLVpjRJqrLMz9lubMCNEoThXWju5rNTprcwHc1zq2c,1012
267
267
  osbot_utils/utils/Dev.py,sha256=HibpQutYy_iG8gGV8g1GztxNN4l29E4Bi7UZaVL6-L8,1203
268
- osbot_utils/utils/Env.py,sha256=Pbel6npitij9zag6SsWdPVH2j1BTZjxnwOR1vLiIyMo,5248
268
+ osbot_utils/utils/Env.py,sha256=243O6ENzaRjHXp8DIHUuv4Wfk-zHTE18KZr0cU8uWyo,5474
269
269
  osbot_utils/utils/Exceptions.py,sha256=KyOUHkXQ_6jDTq04Xm261dbEZuRidtsM4dgzNwSG8-8,389
270
- osbot_utils/utils/Files.py,sha256=bZFvjyC4TAmFH6RSp3Xvj_xBxfybCT2cE7x2qNTQmeE,20834
270
+ osbot_utils/utils/Files.py,sha256=RHbxq8AVdGT9S-OrxEWhNEOhbIrHDRU1UbGB1O05Ga8,21615
271
271
  osbot_utils/utils/Functions.py,sha256=0E6alPJ0fJpBiJgFOWooCOi265wSRyxxXAJ5CELBnso,3498
272
- osbot_utils/utils/Http.py,sha256=Z8V149M2HDrKBoXkDD5EXgqTGx6vQoUqXugXK__wcuw,4572
272
+ osbot_utils/utils/Http.py,sha256=WlXEfgT_NaiDVD7vCDUxy_nOm5Qf8x_L0A3zd8B5tX8,4706
273
273
  osbot_utils/utils/Int.py,sha256=PmlUdU4lSwf4gJdmTVdqclulkEp7KPCVUDO6AcISMF4,116
274
274
  osbot_utils/utils/Json.py,sha256=UNaBazuH1R40fsHjpjuK8kmAANmUHoK9Q0PUeYmgPeY,6254
275
275
  osbot_utils/utils/Json_Cache.py,sha256=mLPkkDZN-3ZVJiDvV1KBJXILtKkTZ4OepzOsDoBPhWg,2006
276
276
  osbot_utils/utils/Lists.py,sha256=CLEjgZwAixJAFlubWEKjnUUhUN85oqvR7UqExVW7rdY,5502
277
277
  osbot_utils/utils/Misc.py,sha256=ljscBemI5wOhfkl1BVpsqshacTOCKkOisV4er9xPCWM,16640
278
- osbot_utils/utils/Objects.py,sha256=WFH3oeXR1CU03oyzXfcIlktcoXQuKw9cIJn8xTs02AE,14654
278
+ osbot_utils/utils/Objects.py,sha256=qAWNLISL-gYTl1Ihj4fBSZ9I6n-p-YPUhRZu9YQwqWQ,15235
279
279
  osbot_utils/utils/Png.py,sha256=V1juGp6wkpPigMJ8HcxrPDIP4bSwu51oNkLI8YqP76Y,1172
280
280
  osbot_utils/utils/Process.py,sha256=lr3CTiEkN3EiBx3ZmzYmTKlQoPdkgZBRjPulMxG-zdo,2357
281
281
  osbot_utils/utils/Python_Logger.py,sha256=tx8N6wRKL3RDHboDRKZn8SirSJdSAE9cACyJkxrThZ8,12792
@@ -286,8 +286,8 @@ osbot_utils/utils/Toml.py,sha256=dqiegndCJF7V1YT1Tc-b0-Bl6QWyL5q30urmQwMXfMQ,140
286
286
  osbot_utils/utils/Version.py,sha256=Ww6ChwTxqp1QAcxOnztkTicShlcx6fbNsWX5xausHrg,422
287
287
  osbot_utils/utils/Zip.py,sha256=t9txUxJzLBEHot6WJwF0iTTUQ1Gf_V2pVwsWzAqw_NU,12163
288
288
  osbot_utils/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
289
- osbot_utils/version,sha256=rySeFYh2TX0iwzR0MwppiJ1yllnTK1JQxMRPUGe8DS0,8
290
- osbot_utils-1.30.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
291
- osbot_utils-1.30.0.dist-info/METADATA,sha256=XspSKWOcbtP6-hGL3r9N7NmMZn7n91lK68mf8vPxetI,1266
292
- osbot_utils-1.30.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
293
- osbot_utils-1.30.0.dist-info/RECORD,,
289
+ osbot_utils/version,sha256=bXbruCHeH-hb0hIQlj3n7tV2_vXn3ypQsk6TaP1y0Zk,8
290
+ osbot_utils-1.32.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
291
+ osbot_utils-1.32.0.dist-info/METADATA,sha256=B4r-4DcyCbnaGO-x7uJIrie537cYmE8Ux4hGFen0Wtc,1266
292
+ osbot_utils-1.32.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
293
+ osbot_utils-1.32.0.dist-info/RECORD,,