Skip to content

Commit

Permalink
Merge dev into main
Browse files Browse the repository at this point in the history
  • Loading branch information
DinisCruz committed Nov 23, 2024
2 parents 51d044c + 227cc5f commit 254e372
Show file tree
Hide file tree
Showing 20 changed files with 351 additions and 178 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Powerful Python util methods and classes that simplify common apis and tasks.

![Current Release](https://img.shields.io/badge/release-v1.82.0-blue)
![Current Release](https://img.shields.io/badge/release-v1.82.2-blue)
[![codecov](https://codecov.io/gh/owasp-sbot/OSBot-Utils/graph/badge.svg?token=GNVW0COX1N)](https://codecov.io/gh/owasp-sbot/OSBot-Utils)


Expand Down
89 changes: 58 additions & 31 deletions osbot_utils/base_classes/Type_Safe.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
# todo: find a way to add these documentations strings to a separate location so that
# the code is not polluted with them (like in the example below)
# the data is available in IDE's code complete
import functools
import inspect

import sys
import types
import typing
from decimal import Decimal
from enum import Enum, EnumMeta
from typing import List
from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List
from osbot_utils.helpers.Random_Guid import Random_Guid
from osbot_utils.helpers.Random_Guid_Short import Random_Guid_Short
from osbot_utils.helpers.Safe_Id import Safe_Id
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
from osbot_utils.utils.Dev import pprint
from osbot_utils.utils.Json import json_parse, json_to_bytes, json_to_gz
from osbot_utils.utils.Misc import list_set
from osbot_utils.utils.Objects import default_value, value_type_matches_obj_annotation_for_attr, \
raise_exception_on_obj_type_annotation_mismatch, obj_is_attribute_annotation_of_type, enum_from_value, \
obj_is_type_union_compatible, value_type_matches_obj_annotation_for_union_attr, \
convert_dict_to_value_from_obj_annotation, dict_to_obj, convert_to_value_from_obj_annotation, \
obj_attribute_annotation
from osbot_utils.utils.Objects import default_value # todo: remove test mocking requirement for this to be here (instead of on the respective method)

# Backport implementations of get_origin and get_args for Python 3.7
if sys.version_info < (3, 8): # pragma: no cover
def get_origin(tp):
import typing
if isinstance(tp, typing._GenericAlias):
return tp.__origin__
elif tp is typing.Generic:
Expand All @@ -34,6 +17,7 @@ def get_origin(tp):
return None

def get_args(tp):
import typing
if isinstance(tp, typing._GenericAlias):
return tp.__args__
else:
Expand All @@ -46,7 +30,7 @@ def get_args(tp):
else: # pragma: no cover
NoneType = type(None)

immutable_types = (bool, int, float, complex, str, tuple, frozenset, bytes, NoneType, EnumMeta)



#todo: see if we can also add type safety to method execution
Expand All @@ -56,9 +40,11 @@ def get_args(tp):
class Type_Safe:

def __init__(self, **kwargs):
from osbot_utils.utils.Objects import raise_exception_on_obj_type_annotation_mismatch

for (key, value) in self.__cls_kwargs__().items(): # assign all default values to self
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

raise_exception_on_obj_type_annotation_mismatch(self, key, value)
if hasattr(self, key):
existing_value = getattr(self, key)
Expand All @@ -79,6 +65,11 @@ def __enter__(self): return self
def __exit__(self, exc_type, exc_val, exc_tb): pass

def __setattr__(self, name, value):
from osbot_utils.utils.Objects import convert_dict_to_value_from_obj_annotation
from osbot_utils.utils.Objects import convert_to_value_from_obj_annotation
from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_attr
from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_union_attr

if not hasattr(self, '__annotations__'): # can't do type safety checks if the class does not have annotations
return super().__setattr__(name, value)

Expand All @@ -101,11 +92,20 @@ def __setattr__(self, name, value):
super().__setattr__(name, value)

def __attr_names__(self):
from osbot_utils.utils.Misc import list_set

return list_set(self.__locals__())

@classmethod
def __cls_kwargs__(cls, include_base_classes=True):
"""Return current class dictionary of class level variables and their values."""
def __cls_kwargs__(cls, include_base_classes=True): # Return current class dictionary of class level variables and their values
import functools
import inspect
from enum import EnumMeta
from osbot_utils.utils.Objects import obj_is_type_union_compatible

IMMUTABLE_TYPES = (bool, int, float, complex, str, tuple, frozenset, bytes, NoneType, EnumMeta)


kwargs = {}

for base_cls in inspect.getmro(cls):
Expand Down Expand Up @@ -134,18 +134,21 @@ def __cls_kwargs__(cls, include_base_classes=True):
if var_type and not isinstance(var_value, var_type): # check type
exception_message = f"variable '{var_name}' is defined as type '{var_type}' but has value '{var_value}' of type '{type(var_value)}'"
raise ValueError(exception_message)
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
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
#todo: fix type safety bug that I believe is caused here
if obj_is_type_union_compatible(var_type, immutable_types) is False: # if var_type is not something like Optional[Union[int, str]]
if type(var_type) not in immutable_types:
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}'"
if obj_is_type_union_compatible(var_type, IMMUTABLE_TYPES) is False: # if var_type is not something like Optional[Union[int, str]]
if type(var_type) not in IMMUTABLE_TYPES:
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}'"
raise ValueError(exception_message)
if include_base_classes is False:
break
return kwargs

@classmethod
def __default__value__(cls, var_type):
import typing
from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List

if var_type is typing.Set: # todo: refactor the dict, set and list logic, since they are 90% the same
return set()
if get_origin(var_type) is set:
Expand All @@ -164,8 +167,8 @@ def __default__value__(cls, var_type):
else:
return default_value(var_type) # for all other cases call default_value, which will try to create a default instance

def __default_kwargs__(self):
"""Return entire (including base classes) dictionary of class level variables and their values."""
def __default_kwargs__(self): # Return entire (including base classes) dictionary of class level variables and their values.
import inspect
kwargs = {}
cls = type(self)
for base_cls in inspect.getmro(cls): # Traverse the inheritance hierarchy and collect class-level attributes
Expand Down Expand Up @@ -217,9 +220,13 @@ def __schema__(cls):
# todo: see if there should be a prefix on these methods, to make it easier to spot them
# of if these are actually that useful that they should be added like this
def bytes(self):
from osbot_utils.utils.Json import json_to_bytes

return json_to_bytes(self.json())

def bytes_gz(self):
from osbot_utils.utils.Json import json_to_gz

return json_to_gz(self.json())

def json(self):
Expand All @@ -240,8 +247,8 @@ def reset(self):
for k,v in self.__cls_kwargs__().items():
setattr(self, k, v)

def update_from_kwargs(self, **kwargs):
"""Update instance attributes with values from provided keyword arguments."""
def update_from_kwargs(self, **kwargs): # Update instance attributes with values from provided keyword arguments.
from osbot_utils.utils.Objects import value_type_matches_obj_annotation_for_attr
for key, value in kwargs.items():
if value is not None:
if hasattr(self,'__annotations__'): # can only do type safety checks if the class does not have annotations
Expand Down Expand Up @@ -278,6 +285,16 @@ def deserialize_dict__using_key_value_annotations(self, key, value):

# todo: this needs refactoring, since the logic and code is getting quite complex (to be inside methods like this)
def deserialize_from_dict(self, data, raise_on_not_found=False):
from decimal import Decimal
from enum import EnumMeta
from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List
from osbot_utils.helpers.Random_Guid import Random_Guid
from osbot_utils.helpers.Random_Guid_Short import Random_Guid_Short
from osbot_utils.utils.Objects import obj_is_attribute_annotation_of_type
from osbot_utils.utils.Objects import obj_attribute_annotation
from osbot_utils.utils.Objects import enum_from_value
from osbot_utils.helpers.Safe_Id import Safe_Id
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now

for key, value in data.items():
if hasattr(self, key) and isinstance(getattr(self, key), Type_Safe):
Expand Down Expand Up @@ -327,16 +344,22 @@ def deserialize_from_dict(self, data, raise_on_not_found=False):
return self

def obj(self):
from osbot_utils.utils.Objects import dict_to_obj

return dict_to_obj(self.json())

def serialize_to_dict(self): # todo: see if we need this method or if the .json() is enough
return serialize_to_dict(self)

def print(self):
from osbot_utils.utils.Dev import pprint

pprint(serialize_to_dict(self))

@classmethod
def from_json(cls, json_data, raise_on_not_found=False):
from osbot_utils.utils.Json import json_parse

if type(json_data) is str:
json_data = json_parse(json_data)
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)
Expand All @@ -345,6 +368,10 @@ def from_json(cls, json_data, raise_on_not_found=False):

# todo: see if it is possible to add recursive protection to this logic
def serialize_to_dict(obj):
from decimal import Decimal
from enum import Enum
from typing import List

if isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None:
return obj
elif isinstance(obj, Enum):
Expand Down
3 changes: 2 additions & 1 deletion osbot_utils/decorators/lists/group_by.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#todo: refactor with index_by
from functools import wraps


def group_by(function): # returns the list provided grouped by the key provided in group_by
from functools import wraps

@wraps(function)
def wrapper(*args,**kwargs):
key = None
Expand Down
6 changes: 3 additions & 3 deletions osbot_utils/decorators/lists/index_by.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# todo: find way to also allow the used of function().index_by(key) to working
# : maybe using Fluent_List
from functools import wraps

from osbot_utils.fluent.Fluent_Dict import Fluent_Dict


def index_by(function): # returns the list provided indexed by the key provided in index_by
from functools import wraps
from osbot_utils.fluent.Fluent_Dict import Fluent_Dict

def apply(key, values):
if key:
results = {}
Expand Down
56 changes: 28 additions & 28 deletions osbot_utils/decorators/methods/cache_on_self.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import inspect
from functools import wraps
from osbot_utils.utils.Misc import str_md5
from typing import Any, Callable, TypeVar


CACHE_ON_SELF_KEY_PREFIX = '__cache_on_self__'
CACHE_ON_SELF_TYPES = [int, float, bytearray, bytes, bool,
complex, str]
Expand All @@ -17,12 +13,14 @@

T = TypeVar('T', bound=Callable[..., Any]) # so that we have type hinting when using this class

def cache_on_self(function: T) -> T:
"""
Use this for cases where we want the cache to be tied to the Class instance (i.e. not global for all executions)
"""
def cache_on_self(function: T) -> T: # Use this for cases where we want the cache to be tied to the Class instance (i.e. not global for all executions)
import inspect
from functools import wraps

@wraps(function)
def wrapper(*args, **kwargs):


if len(args) == 0 or inspect.isclass(type(args[0])) is False:
raise Exception("In Method_Wrappers.cache_on_self could not find self")

Expand Down Expand Up @@ -58,23 +56,25 @@ def cache_on_self__kwargs_to_str(kwargs):
return kwargs_values_as_str

def cache_on_self__get_cache_in_key(function, args=None, kwargs=None):
key_name = function.__name__
args_md5 = ''
kwargs_md5 = ''
args_values_as_str = cache_on_self__args_to_str(args)
kwargs_values_as_str = cache_on_self__kwargs_to_str(kwargs)
if args_values_as_str:
args_md5 = str_md5(args_values_as_str)
if kwargs_values_as_str:
kwargs_md5 = str_md5(kwargs_values_as_str)
return f'{CACHE_ON_SELF_KEY_PREFIX}_{key_name}_{args_md5}_{kwargs_md5}'

# class_name = self_obj.__class__.__name__
#
# function_name = function_obj.__name__
# if params:
# params_as_string = '_'.join(str(x) for x in params).replace('/',' ')
# params_md5 = str_md5(params_as_string)
# return f'{class_name}_{function_name}_{params_md5}.gz'
# else:
# return f'{class_name}_{function_name}.gz'
from osbot_utils.utils.Misc import str_md5

key_name = function.__name__
args_md5 = ''
kwargs_md5 = ''
args_values_as_str = cache_on_self__args_to_str(args)
kwargs_values_as_str = cache_on_self__kwargs_to_str(kwargs)
if args_values_as_str:
args_md5 = str_md5(args_values_as_str)
if kwargs_values_as_str:
kwargs_md5 = str_md5(kwargs_values_as_str)
return f'{CACHE_ON_SELF_KEY_PREFIX}_{key_name}_{args_md5}_{kwargs_md5}'

# class_name = self_obj.__class__.__name__
#
# function_name = function_obj.__name__
# if params:
# params_as_string = '_'.join(str(x) for x in params).replace('/',' ')
# params_md5 = str_md5(params_as_string)
# return f'{class_name}_{function_name}_{params_md5}.gz'
# else:
# return f'{class_name}_{function_name}.gz'
9 changes: 3 additions & 6 deletions osbot_utils/decorators/methods/remove_return_value.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
from functools import wraps
class remove_return_value: # removes the field from the return value of the function (if it exists)


class remove_return_value:
"""
removes the field from the return value of the function (if it exists)
"""
def __init__(self, field_name):
self.field_name = field_name # field to remove

def __call__(self, function):
from functools import wraps

@wraps(function) # makes __name__ work ok
def wrapper(*args,**kwargs): # wrapper function
data = function(*args,**kwargs) # calls wrapped function with original params
Expand Down
4 changes: 3 additions & 1 deletion osbot_utils/helpers/Random_Guid.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from osbot_utils.utils.Misc import random_guid, is_guid


class Random_Guid(str):
def __new__(cls, value=None):
from osbot_utils.utils.Misc import random_guid, is_guid

if value is None:
value = random_guid()
if is_guid(value):
Expand Down
5 changes: 4 additions & 1 deletion osbot_utils/helpers/Timestamp_Now.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from osbot_utils.utils.Misc import timestamp_now

class Timestamp_Now(int):
def __new__(cls, value=None):
from osbot_utils.utils.Misc import timestamp_now

if value is None:
value = timestamp_now()
return int.__new__(cls, value)

def __init__(self, value=None):
from osbot_utils.utils.Misc import timestamp_now

self.value = value if value is not None else timestamp_now()

def __str__(self):
Expand Down
Loading

0 comments on commit 254e372

Please sign in to comment.