diff --git a/README.md b/README.md index 6b20988d..bb3b8937 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/osbot_utils/base_classes/Type_Safe.py b/osbot_utils/base_classes/Type_Safe.py index 16ce67eb..005d5c5c 100644 --- a/osbot_utils/base_classes/Type_Safe.py +++ b/osbot_utils/base_classes/Type_Safe.py @@ -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: @@ -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: @@ -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 @@ -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) @@ -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) @@ -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): @@ -134,11 +134,11 @@ 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 @@ -146,6 +146,9 @@ def __cls_kwargs__(cls, include_base_classes=True): @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: @@ -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 @@ -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): @@ -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 @@ -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): @@ -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) @@ -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): diff --git a/osbot_utils/decorators/lists/group_by.py b/osbot_utils/decorators/lists/group_by.py index be235e76..1de0b02f 100644 --- a/osbot_utils/decorators/lists/group_by.py +++ b/osbot_utils/decorators/lists/group_by.py @@ -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 diff --git a/osbot_utils/decorators/lists/index_by.py b/osbot_utils/decorators/lists/index_by.py index eeece195..5e53b63f 100644 --- a/osbot_utils/decorators/lists/index_by.py +++ b/osbot_utils/decorators/lists/index_by.py @@ -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 = {} diff --git a/osbot_utils/decorators/methods/cache_on_self.py b/osbot_utils/decorators/methods/cache_on_self.py index 898db4f2..407d80b7 100644 --- a/osbot_utils/decorators/methods/cache_on_self.py +++ b/osbot_utils/decorators/methods/cache_on_self.py @@ -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] @@ -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") @@ -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' \ No newline at end of file + 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' \ No newline at end of file diff --git a/osbot_utils/decorators/methods/remove_return_value.py b/osbot_utils/decorators/methods/remove_return_value.py index 9eac98a6..5296533f 100644 --- a/osbot_utils/decorators/methods/remove_return_value.py +++ b/osbot_utils/decorators/methods/remove_return_value.py @@ -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 diff --git a/osbot_utils/helpers/Random_Guid.py b/osbot_utils/helpers/Random_Guid.py index 717c6def..00aa0b91 100644 --- a/osbot_utils/helpers/Random_Guid.py +++ b/osbot_utils/helpers/Random_Guid.py @@ -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): diff --git a/osbot_utils/helpers/Timestamp_Now.py b/osbot_utils/helpers/Timestamp_Now.py index cb6a2144..af8b4cdc 100644 --- a/osbot_utils/helpers/Timestamp_Now.py +++ b/osbot_utils/helpers/Timestamp_Now.py @@ -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): diff --git a/osbot_utils/helpers/sqlite/Sqlite__Database.py b/osbot_utils/helpers/sqlite/Sqlite__Database.py index ec7adae5..d44f01f3 100644 --- a/osbot_utils/helpers/sqlite/Sqlite__Database.py +++ b/osbot_utils/helpers/sqlite/Sqlite__Database.py @@ -1,18 +1,14 @@ -import sqlite3 - -from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self -from osbot_utils.decorators.methods.cache import cache -from osbot_utils.decorators.methods.cache_on_self import cache_on_self - -from osbot_utils.utils.Files import current_temp_folder, path_combine, folder_create, file_exists, file_delete -from osbot_utils.utils.Misc import random_filename +from osbot_utils.base_classes.Type_Safe import Type_Safe +from osbot_utils.decorators.methods.cache_on_self import cache_on_self +from osbot_utils.utils.Files import current_temp_folder, path_combine, folder_create, file_exists, file_delete +from osbot_utils.utils.Misc import random_filename SQLITE_DATABASE_PATH__IN_MEMORY = ':memory:' FOLDER_NAME_TEMP_DATABASES = '_temp_sqlite_databases' TEMP_DATABASE__FILE_NAME_PREFIX = 'random_sqlite_db__' TEMP_DATABASE__FILE_EXTENSION = '.sqlite' -class Sqlite__Database(Kwargs_To_Self): +class Sqlite__Database(Type_Safe): db_path : str = None closed : bool = False connected : bool = False @@ -38,6 +34,8 @@ def close(self): @cache_on_self def connect(self): + import sqlite3 + connection_string = self.connection_string() connection = sqlite3.connect(connection_string) connection.row_factory = self.dict_factory # this returns a dict as the row value of every query @@ -90,6 +88,8 @@ def path_temp_databases(self): return path_temp_databases def save_to(self, path): + import sqlite3 + connection = self.connection() file_conn = sqlite3.connect(path) connection.backup(file_conn) diff --git a/osbot_utils/helpers/sqlite/domains/Sqlite__DB__Files.py b/osbot_utils/helpers/sqlite/domains/Sqlite__DB__Files.py index a06dca04..1927a644 100644 --- a/osbot_utils/helpers/sqlite/domains/Sqlite__DB__Files.py +++ b/osbot_utils/helpers/sqlite/domains/Sqlite__DB__Files.py @@ -1,7 +1,7 @@ -from osbot_utils.decorators.lists.index_by import index_by -from osbot_utils.decorators.methods.cache_on_self import cache_on_self -from osbot_utils.helpers.sqlite.domains.Sqlite__DB__Local import Sqlite__DB__Local -from osbot_utils.helpers.sqlite.tables.Sqlite__Table__Files import Sqlite__Table__Files +from osbot_utils.decorators.lists.index_by import index_by +from osbot_utils.decorators.methods.cache_on_self import cache_on_self +from osbot_utils.helpers.sqlite.domains.Sqlite__DB__Local import Sqlite__DB__Local + class Sqlite__DB__Files(Sqlite__DB__Local): @@ -35,6 +35,8 @@ def file_names(self): @cache_on_self def table_files(self): + from osbot_utils.helpers.sqlite.tables.Sqlite__Table__Files import Sqlite__Table__Files + return Sqlite__Table__Files(database=self).setup() @index_by diff --git a/osbot_utils/helpers/trace/Trace_Call__Config.py b/osbot_utils/helpers/trace/Trace_Call__Config.py index e9583791..01192878 100644 --- a/osbot_utils/helpers/trace/Trace_Call__Config.py +++ b/osbot_utils/helpers/trace/Trace_Call__Config.py @@ -1,11 +1,13 @@ -from osbot_utils.utils.Dev import pprint -from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self + +from osbot_utils.base_classes.Type_Safe import Type_Safe +from osbot_utils.utils.Dev import pprint # todo: fix test requirement of mock to use this method + PRINT_MAX_STRING_LENGTH = 100 PRINT_PADDING__DURATION = 100 PRINT_PADDING_PARENT_INFO = 60 -class Trace_Call__Config(Kwargs_To_Self): +class Trace_Call__Config(Type_Safe): capture_locals : bool = False capture_duration : bool capture_extra_data : bool diff --git a/osbot_utils/utils/Dev.py b/osbot_utils/utils/Dev.py index dfc0bc68..e305df9c 100644 --- a/osbot_utils/utils/Dev.py +++ b/osbot_utils/utils/Dev.py @@ -1,26 +1,29 @@ -import json -import pprint as original_pprint - -from osbot_utils.utils.Misc import date_time_now - class Dev: @staticmethod def jformat(data): + import json + return json.dumps(data, indent=4) # use json.dumps to format @staticmethod def jprint(data): + import json + print() # add a line before print(json.dumps(data, indent=4)) # use json.dumps to format return data @staticmethod def pformat(data): + import pprint as original_pprint + return original_pprint.pformat(data, indent=2) # use a pprint to format @staticmethod def pprint(*args): + import pprint as original_pprint + print() # add a line before for arg in args: original_pprint.pprint(arg, indent=2) # use a pprint to format @@ -36,6 +39,8 @@ def nprint(data): @staticmethod def print_now(): + from osbot_utils.utils.Misc import date_time_now + print(date_time_now()) jformat = Dev.jformat diff --git a/osbot_utils/utils/Env.py b/osbot_utils/utils/Env.py index 113496ea..f60afba9 100644 --- a/osbot_utils/utils/Env.py +++ b/osbot_utils/utils/Env.py @@ -1,12 +1,6 @@ - -import os -import sys -from sys import platform -from osbot_utils.utils.Files import all_parent_folders, file_exists -from osbot_utils.utils.Misc import list_set -from osbot_utils.utils.Str import strip_quotes - def del_env(key): + import os + if key in os.environ: del os.environ[key] @@ -14,6 +8,8 @@ def env__home(): return get_env('HOME', '') def env__home__is__root(): + import os + return os.getenv('HOME') == '/root' def env__old_pwd(): @@ -28,6 +24,8 @@ def env__old_pwd__remove(value): return value def env__terminal__is__xterm(): + import os + return os.getenv('TERM') == 'xterm' def env__terminal__is_not__xterm(): @@ -35,16 +33,21 @@ def env__terminal__is_not__xterm(): def platform_darwin(): + from sys import platform return platform == 'darwin' def env_value(var_name): return env_vars().get(var_name, None) def env_var_set(var_name): + import os + value = os.getenv(var_name) return value is not None and value != '' def env_vars_list(): + from osbot_utils.utils.Misc import list_set + return list_set(env_vars()) def env_vars(reload_vars=False): @@ -52,6 +55,8 @@ def env_vars(reload_vars=False): if reload_vars reload data from .env file then return dictionary with current environment variables """ + import os + if reload_vars: load_dotenv() vars = os.environ @@ -61,7 +66,12 @@ def env_vars(reload_vars=False): return data def env_load_from_file(path, override=False): + import os + from osbot_utils.utils.Files import file_exists + if file_exists(path): + from osbot_utils.utils.Str import strip_quotes + with open(path) as f: for line in f: line = line.strip() @@ -75,6 +85,8 @@ def env_load_from_file(path, override=False): os.environ[key.strip()] = value.strip() def env_unload_from_file(path): + import os + if os.path.exists(path): with open(path) as f: for line in f: @@ -87,19 +99,33 @@ def env_unload_from_file(path): del os.environ[key] def find_dotenv_file(start_path=None, env_file_to_find='.env'): + import os + from osbot_utils.utils.Files import all_parent_folders + directories = all_parent_folders(path=start_path, include_path=True) # Define the possible directories to search for the .env file (which is this and all parent folders) for directory in directories: # Iterate through the directories and load the .env file if found env_path = os.path.join(directory,env_file_to_find) # Define the path to the .env file if os.path.exists(env_path): # If we found one return env_path # return the path to the .env file +def get_env(key, default=None): + import os + return os.getenv(key, default=default) + def in_github_action(): + import os + return os.getenv('GITHUB_ACTIONS') == 'true' def in_pytest_with_coverage(): + import os + return os.getenv('COVERAGE_RUN') == 'true' def in_python_debugger(): + import os + import sys + if sys.gettrace() is not None: # Check for a trace function return True pycharm_hosted = os.getenv('PYCHARM_HOSTED') == '1' # Check for PyCharm specific environment variables and other potential indicators @@ -121,10 +147,15 @@ def not_in_github_action(): return in_github_action() is False def set_env(key, value): + import os + os.environ[key] = value return value def unload_dotenv(dotenv_path=None): + import os + from osbot_utils.utils.Files import all_parent_folders + if dotenv_path: # If a specific dotenv path is provided, unload from it env_unload_from_file(dotenv_path) else: @@ -136,5 +167,4 @@ def unload_dotenv(dotenv_path=None): break # Stop after unloading the first .env file is_env_var_set = env_var_set -env_load = load_dotenv -get_env = os.getenv \ No newline at end of file +env_load = load_dotenv \ No newline at end of file diff --git a/osbot_utils/utils/Files.py b/osbot_utils/utils/Files.py index 919278ee..7b542544 100644 --- a/osbot_utils/utils/Files.py +++ b/osbot_utils/utils/Files.py @@ -1,17 +1,5 @@ -import gzip - import os -import glob -import pickle -import re -import shutil -import tempfile -from os.path import abspath, join -from pathlib import Path, PosixPath -from typing import Union - -from osbot_utils.utils.Misc import bytes_to_base64, base64_to_bytes, random_string - +from typing import Union class Files: @staticmethod @@ -21,6 +9,7 @@ def bytes(path): @staticmethod def copy(source:str, destination:str) -> str: + import shutil if file_exists(source): # make sure source file exists destination_parent_folder = parent_folder(destination) # get target parent folder folder_create(destination_parent_folder) # ensure target folder exists # todo: check if this is still needed (we should be using a copy method that creates the required fodlers) @@ -78,10 +67,14 @@ def exists(path): @staticmethod def find(path_pattern, recursive=True): + import glob + return glob.glob(path_pattern, recursive=recursive) @staticmethod def files(path, pattern= '*', only_files=True): + from pathlib import Path + result = [] for file in Path(path).rglob(pattern): if only_files and is_not_file(file): @@ -99,6 +92,8 @@ def files_names(files : list, check_if_exists=True): @staticmethod def file_create_all_parent_folders(file_path): + from pathlib import Path + if file_path: parent_path = parent_folder(file_path) if parent_path: @@ -136,10 +131,14 @@ def file_extension_fix(extension): @staticmethod def file_to_base64(path): + from osbot_utils.utils.Misc import bytes_to_base64 + return bytes_to_base64(file_bytes(path)) @staticmethod def file_from_base64(bytes_base64, path=None, extension=None): + from osbot_utils.utils.Misc import base64_to_bytes + bytes_ = base64_to_bytes(bytes_base64) return file_create_bytes(bytes=bytes_, path=path, extension=None) @@ -181,6 +180,8 @@ def folder_exists(path): @staticmethod def folder_copy(source, destination, ignore_pattern=None): + import shutil + if ignore_pattern: if type(ignore_pattern) is str: ignore_pattern = [ignore_pattern] @@ -214,6 +215,8 @@ def folder_delete(target_folder): @staticmethod def folder_delete_all(path): # this will remove recursively + import shutil + if folder_exists(path): shutil.rmtree(path) return folder_exists(path) is False @@ -267,6 +270,8 @@ def folders_recursive(parent_dir): @staticmethod def is_file(target): + from pathlib import Path + if isinstance(target, Path): return target.is_file() if type(target) is str: @@ -276,6 +281,8 @@ def is_file(target): @staticmethod def is_folder(target): + from pathlib import Path + if isinstance(target, Path): return target.is_dir() if type(target) is str: @@ -290,6 +297,8 @@ def lines(path): @staticmethod def lines_gz(path): + import gzip + with gzip.open(path, "rt") as file: for line in file: yield line @@ -304,6 +313,8 @@ def open(path, mode='r'): @staticmethod def open_gz(path, mode='r'): + import gzip + return gzip.open(path, mode=mode) @staticmethod @@ -320,6 +331,8 @@ def open_bytes(path): # return abspath(join(parent_path,sub_path)) def path_combine(path1: Union[str, os.PathLike], path2: Union[str, os.PathLike]) -> str: + from os.path import abspath, join + if path1 is None or path2 is None: raise ValueError("Both paths must be provided") @@ -345,6 +358,8 @@ def parent_folder_combine(file, path): @staticmethod def pickle_save_to_file(object_to_save, path=None): + import pickle + path = path or temp_file(extension=".pickle") file_to_store = open(path, "wb") pickle.dump(object_to_save, file_to_store) @@ -353,6 +368,8 @@ def pickle_save_to_file(object_to_save, path=None): @staticmethod def pickle_load_from_file(path=None): + import pickle + file_to_read = open(path, "rb") loaded_object = pickle.load(file_to_read) file_to_read.close() @@ -360,6 +377,8 @@ def pickle_load_from_file(path=None): @staticmethod def safe_file_name(file_name): + import re + if type(file_name) is not str: file_name = f"{file_name}" return re.sub(r'[^a-zA-Z0-9_.-]', '_',file_name or '') @@ -388,6 +407,8 @@ def save_bytes_as_file(bytes_to_save, path=None, extension=None): @staticmethod def temp_file(extension = '.tmp', contents=None, target_folder=None): + import tempfile + extension = file_extension_fix(extension) if target_folder is None: (fd, tmp_file) = tempfile.mkstemp(extension) @@ -401,6 +422,8 @@ def temp_file(extension = '.tmp', contents=None, target_folder=None): @staticmethod def temp_file_in_folder(target_folder, prefix="temp_file_", postfix='.txt'): + from osbot_utils.utils.Misc import random_string + if is_folder(target_folder): path_to_file = path_combine(target_folder, random_string(prefix=prefix, postfix=postfix)) file_create(path_to_file, random_string()) @@ -414,10 +437,14 @@ def temp_filename(extension='.tmp'): @staticmethod def temp_folder(prefix=None, suffix=None,target_folder=None): + import tempfile + return tempfile.mkdtemp(suffix, prefix, target_folder) @staticmethod def temp_folder_current(): + import tempfile + return tempfile.gettempdir() @staticmethod @@ -440,6 +467,8 @@ def write_bytes(path=None, bytes=None, extension=None): @staticmethod def write_gz(path=None, contents=None): + import gzip + path = path or temp_file(extension='.gz') contents = contents or '' if type(contents) is str: @@ -450,6 +479,8 @@ def write_gz(path=None, contents=None): # todo: refactor the methods above into static methods (as bellow) def absolute_path(path): + from os.path import abspath + return abspath(path) def all_parent_folders(path=None, include_path=False): @@ -499,6 +530,8 @@ def files_names_in_folder(target, with_extension=False): return files_names_without_extension(files_in_folder(target)) def files_in_folder(path,pattern='*', only_files=True): + from pathlib import Path + result = [] for file in Path(path).glob(pattern): if only_files and is_not_file(file): diff --git a/osbot_utils/utils/Json.py b/osbot_utils/utils/Json.py index b3b9e3e2..e9844bed 100644 --- a/osbot_utils/utils/Json.py +++ b/osbot_utils/utils/Json.py @@ -1,14 +1,11 @@ -import json -import os - -from osbot_utils.utils.Misc import str_lines, str_md5, str_sha256 -from osbot_utils.utils.Files import file_create_gz, file_create, load_file_gz, file_contents, file_lines, file_lines_gz -from osbot_utils.utils.Zip import str_to_gz, gz_to_str def bytes_to_json_loads(data): + import json return json.loads(data.decode()) def json_dumps(python_object, indent=4, pretty=True, sort_keys=False, default=str, raise_exception=False): + import json + if python_object: try: if pretty: @@ -25,6 +22,8 @@ def json_dumps_to_bytes(*args, **kwargs): return json_dumps(*args, **kwargs).encode() def json_lines_file_load(target_path): + from osbot_utils.utils.Files import file_lines + raw_json = '[' # start the json array lines = file_lines(target_path) # get all lines from the file provided in target_path raw_json += ','.join(lines) # add lines to raw_json split by json array separator @@ -32,6 +31,8 @@ def json_lines_file_load(target_path): return json_parse(raw_json) # convert json data into a python object def json_lines_file_load_gz(target_path): + from osbot_utils.utils.Files import file_lines_gz + raw_json = '[' # start the json array lines = file_lines_gz(target_path) # get all lines from the file provided in target_path raw_json += ','.join(lines) # add lines to raw_json split by json array separator @@ -40,14 +41,19 @@ def json_lines_file_load_gz(target_path): def json_sha_256(target): + from osbot_utils.utils.Misc import str_sha256 return str_sha256(json_dumps(target)) def json_to_gz(data): + from osbot_utils.utils.Zip import str_to_gz value = json_dumps(data, pretty=False) return str_to_gz(value) def gz_to_json(gz_data): + import json + from osbot_utils.utils.Zip import gz_to_str + data = gz_to_str(gz_data) return json.loads(data) @@ -62,11 +68,15 @@ def load_file(path): Note: will not throw errors and will return {} as default errors are logged to Json.log """ + from osbot_utils.utils.Files import file_contents + json_data = file_contents(path) return json_loads(json_data) @staticmethod def load_file_and_delete(path): + import os + data = json_load_file(path) if data: os.remove(path) @@ -74,11 +84,14 @@ def load_file_and_delete(path): @staticmethod def load_file_gz(path): + from osbot_utils.utils.Files import load_file_gz data = load_file_gz(path) return json_loads(data) @staticmethod def load_file_gz_and_delete(path): + import os + data = json_load_file_gz(path) if data: os.remove(path) @@ -91,6 +104,8 @@ def loads(json_data, raise_exception=False): Note: will not throw errors and will return {} as default errors are logged to Json.log """ + import json + if json_data: try: return json.loads(json_data) @@ -103,11 +118,15 @@ def loads(json_data, raise_exception=False): @staticmethod def loads_json_lines(json_lines): + from osbot_utils.utils.Misc import str_lines + json_data = '[' + ','.join(str_lines(json_lines.strip())) + ']' return json_loads(json_data) @staticmethod def md5(data): + from osbot_utils.utils.Misc import str_md5 + return str_md5(json_dump(data)) @staticmethod @@ -116,6 +135,8 @@ def round_trip(data): @staticmethod def save_file(python_object, path=None, pretty=False, sort_keys=False): + from osbot_utils.utils.Files import file_create + json_data = json_dumps(python_object=python_object, indent=2, pretty=pretty, sort_keys=sort_keys) return file_create(path=path, contents=json_data) @@ -125,6 +146,7 @@ def save_file_pretty(python_object, path=None): @staticmethod def save_file_gz(python_object, path=None, pretty=False): + from osbot_utils.utils.Files import file_create_gz json_data = json_dumps(python_object,indent=2, pretty=pretty) return file_create_gz(path=path, contents=json_data) diff --git a/osbot_utils/utils/Misc.py b/osbot_utils/utils/Misc.py index 1822703b..6da9dd2a 100644 --- a/osbot_utils/utils/Misc.py +++ b/osbot_utils/utils/Misc.py @@ -1,22 +1,4 @@ -import base64 -import hashlib -import importlib -import logging -import os -import random -import string import sys -import textwrap -import re -import threading -import uuid -import warnings -from datetime import datetime, timedelta -from secrets import token_bytes -from time import sleep -from typing import Iterable -from urllib.parse import quote_plus, unquote_plus - if sys.version_info >= (3, 11): from datetime import UTC @@ -28,21 +10,26 @@ def append_random_string(target, length=6, prefix='-'): return f'{target}{random_string(length, prefix)}' def attr_value_from_module_name(module_name, attr_name, default_value=None): + import importlib module = importlib.import_module(module_name) if hasattr(module, attr_name): return getattr(module, attr_name) return default_value def bytes_md5(target : bytes): + import hashlib return hashlib.md5(target).hexdigest() def bytes_sha256(target : bytes): + import hashlib return hashlib.sha256(target).hexdigest() def bytes_sha384(target : bytes): + import hashlib return hashlib.sha384(target).hexdigest() def base64_to_bytes(bytes_base64): + import base64 if type(bytes_base64) is str: bytes_base64 = bytes_base64.encode() return base64.decodebytes(bytes_base64) @@ -51,12 +38,14 @@ def base64_to_str(target, encoding='ascii'): return bytes_to_str(base64_to_bytes(target), encoding=encoding) def bytes_to_base64(target): + import base64 return base64.b64encode(target).decode() def bytes_to_str(target, encoding='ascii'): return target.decode(encoding=encoding) def convert_to_number(value): + import re if value: try: if value[0] in ['£','$','€']: @@ -69,10 +58,12 @@ def convert_to_number(value): return 0 def current_thread_id(): + import threading return threading.current_thread().native_id def date_time_from_to_str(date_time_str, format_from, format_to, print_conversion_error=False): + from datetime import datetime try: date_time = datetime.strptime(date_time_str, format_from) return date_time.strftime(format_to) @@ -96,10 +87,9 @@ def date_now(use_utc=True, return_str=True): return value def date_time_now(use_utc=True, return_str=True, milliseconds_numbers=0, date_time_format='%Y-%m-%d %H:%M:%S.%f'): + from datetime import datetime if use_utc: value = datetime.now(UTC) - #value = datetime.utcnow() # todo: this has been marked for depreciation in python 11 - # value = datetime.now(UTC) # but this doesn't seem to work in python 10.x : E ImportError: cannot import name 'UTC' from 'datetime' (/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/datetime.py) else: value = datetime.now() @@ -107,18 +97,18 @@ def date_time_now(use_utc=True, return_str=True, milliseconds_numbers=0, date_ti return date_time_to_str(value, milliseconds_numbers=milliseconds_numbers, date_time_format=date_time_format) return value -# def date_time_parse(value): -# if type(value) is datetime: -# return value -# return parser.parse(value) def date_time_less_time_delta(date_time, days=0, hours=0, minutes=0, seconds=0, date_time_format='%Y-%m-%d %H:%M:%S' , return_str=True): + from datetime import timedelta + new_date_time = date_time - timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) if return_str: return date_time_to_str(new_date_time, date_time_format=date_time_format) return new_date_time def date_time_now_less_time_delta(days=0,hours=0, minutes=0, seconds=0, date_time_format='%Y-%m-%d %H:%M:%S', return_str=True): + from datetime import datetime + return date_time_less_time_delta(datetime.now(UTC),days=days, hours=hours, minutes=minutes, seconds=seconds,date_time_format=date_time_format, return_str=return_str) def date_to_str(date, date_format='%Y-%m-%d'): @@ -181,6 +171,7 @@ def is_float(value): return False def is_guid(value): + import uuid try: uuid_obj = uuid.UUID(value) return str(uuid_obj) == value.lower() @@ -189,6 +180,7 @@ def is_guid(value): def ignore_warning__unclosed_ssl(): + import warnings warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*") @@ -213,15 +205,18 @@ def log_to_file(level="INFO"): return logger_add_handler__file() def logger(): + import logging return logging.getLogger() def logger_add_handler(handler): logger().addHandler(handler) def logger_add_handler__console(): + import logging logger_add_handler(logging.StreamHandler()) def logger_add_handler__file(log_file=None): + import logging from osbot_utils.utils.Files import temp_file log_file = log_file or temp_file(extension=".log") logger_add_handler(logging.FileHandler(filename=log_file)) @@ -275,6 +270,8 @@ def str_sha384(text:str): return def str_sha384_as_base64(text:str, include_prefix=True): + import hashlib + import base64 if type(text) is str: hash_object = hashlib.sha384(text.encode()) digest = hash_object.digest() # Getting the digest of the hash @@ -306,6 +303,8 @@ def time_delta_in_days_hours_or_minutes(time_delta): def time_now(use_utc=True, milliseconds_numbers=1): + from datetime import datetime + if use_utc: datetime_now = datetime.now(UTC) else: @@ -317,6 +316,8 @@ def time_to_str(datetime_value, time_format='%H:%M:%S.%f', milliseconds_numbers= return time_str_milliseconds(datetime_str=time_str, datetime_format=time_format, milliseconds_numbers=milliseconds_numbers) def timestamp_utc_now(): + from datetime import datetime + return int(datetime.now(UTC).timestamp() * 1000) def timestamp_utc_now_less_delta(days=0,hours=0, minutes=0, seconds=0): @@ -327,6 +328,8 @@ def datetime_to_timestamp(datetime): return int(datetime.timestamp() * 1000) def timestamp_to_datetime(timestamp): + from datetime import datetime + timestamp = float(timestamp) # handle cases when timestamp is a Decimal return datetime.fromtimestamp(timestamp/1000) @@ -346,9 +349,13 @@ def to_string(target): return '' def random_bytes(length=24): + from secrets import token_bytes return token_bytes(length) def random_filename(extension='.tmp', length=10): + import random + import string + from osbot_utils.utils.Files import file_extension_fix extension = file_extension_fix(extension) return '{0}{1}'.format(''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) , extension) @@ -357,9 +364,12 @@ def random_port(min=20000,max=65000): return random_number(min, max) def random_number(min=1,max=65000): + import random return random.randint(min, max) def random_password(length=24, prefix=''): + import random + import string password = prefix + ''.join(random.choices(string.ascii_lowercase + string.ascii_uppercase + string.punctuation + @@ -372,6 +382,10 @@ def random_password(length=24, prefix=''): return password def random_string(length:int=8, prefix:str='', postfix:str=''): + import string + + import random + if is_int(length): length -= 1 # so that we get the exact length when the value is provided else: @@ -383,6 +397,9 @@ def random_string_short(prefix:str = None): return random_id(prefix=prefix, length=6).lower() def random_string_and_numbers(length:int=6,prefix:str=''): + import random + import string + return prefix + ''.join(random.choices(string.ascii_uppercase + string.digits, k=length)) def random_text(prefix:str=None,length:int=12, lowercase=False): @@ -395,21 +412,27 @@ def random_text(prefix:str=None,length:int=12, lowercase=False): return value def random_uuid(): + import uuid return str(uuid.uuid4()) def random_uuid_short(): + import uuid return str(uuid.uuid4())[0:6] def remove(target_string, string_to_remove): # todo: refactor to str_* return replace(target_string, string_to_remove, '') def remove_multiple_spaces(target): # todo: refactor to str_* + import re + return re.sub(' +', ' ', target) def replace(target_string, string_to_find, string_to_replace): # todo: refactor to str_* return target_string.replace(string_to_find, string_to_replace) def remove_html_tags(html): + import re + if html: TAG_RE = re.compile(r'<[^>]+>') return TAG_RE.sub('', html).replace(' ', ' ') @@ -420,8 +443,9 @@ def split_lines(text): def split_spaces(target): return remove_multiple_spaces(target).split(' ') -def sorted_set(target : Iterable): - if target: +def sorted_set(target): + from typing import Iterable + if isinstance(target, Iterable) and target: return sorted(set(target)) return [] @@ -437,9 +461,13 @@ def str_to_bool(value): return False def str_to_date(str_date, format='%Y-%m-%d %H:%M:%S.%f'): + from datetime import datetime + return datetime.strptime(str_date,format) def str_to_date_time(str_date, format='%Y-%m-%d %H:%M:%S'): + from datetime import datetime + return datetime.strptime(str_date,format) def str_to_int(str_data): @@ -457,14 +485,18 @@ def under_debugger(): def url_encode(data): + from urllib.parse import quote_plus if type(data) is str: return quote_plus(data) def url_decode(data): + from urllib.parse import unquote_plus if type(data) is str: return unquote_plus(data) def utc_now(): + from datetime import datetime + return datetime.now(UTC) def upper(target : str): @@ -473,10 +505,13 @@ def upper(target : str): return "" def wait(seconds): + from time import sleep + if seconds and seconds > 0: sleep(seconds) def word_wrap(text,length = 40): + import textwrap if text: wrapped_text = "" for line in text.splitlines(): # handle case when there are newlines inside the text value @@ -486,6 +521,7 @@ def word_wrap(text,length = 40): return '' def word_wrap_escaped(text,length = 40): + import textwrap if text: return '\\n'.join(textwrap.wrap(text, length)) diff --git a/osbot_utils/utils/Objects.py b/osbot_utils/utils/Objects.py index eb8a9db5..f3581829 100644 --- a/osbot_utils/utils/Objects.py +++ b/osbot_utils/utils/Objects.py @@ -1,24 +1,14 @@ # todo add tests -import inspect -import json -import pickle import sys -import types -import typing -from collections.abc import Mapping -from typing import Union from types import SimpleNamespace -from osbot_utils.helpers.Safe_Id import Safe_Id -from osbot_utils.helpers.Timestamp_Now import Timestamp_Now -from osbot_utils.helpers.Random_Guid import Random_Guid -from osbot_utils.utils.Misc import list_set -from osbot_utils.utils.Str import str_unicode_escape, str_max_width -TYPE_SAFE__CONVERT_VALUE__SUPPORTED_TYPES = [Safe_Id, Random_Guid, Timestamp_Now] +class __(SimpleNamespace): + pass # Backport implementations of get_origin and get_args for Python 3.7 if sys.version_info < (3, 8): def get_origin(tp): + import typing if isinstance(tp, typing._GenericAlias): return tp.__origin__ elif tp is typing.Generic: @@ -27,6 +17,7 @@ def get_origin(tp): return None def get_args(tp): + import typing if isinstance(tp, typing._GenericAlias): return tp.__args__ else: @@ -36,6 +27,9 @@ def get_args(tp): def are_types_compatible_for_assigment(source_type, target_type): + import types + import typing + if source_type is target_type: return True if source_type is int and target_type is float: @@ -80,9 +74,13 @@ def base_classes_names(cls): return [cls.__name__ for cls in base_classes(cls)] def class_functions_names(target): + from osbot_utils.utils.Misc import list_set + return list_set(class_functions(target)) def class_functions(target): + import inspect + functions = {} for function_name, function_ref in inspect.getmembers(type(target), predicate=inspect.isfunction): functions[function_name] = function_ref @@ -110,6 +108,13 @@ def convert_dict_to_value_from_obj_annotation(target, attr_name, value): return value def convert_to_value_from_obj_annotation(target, attr_name, value): # todo: see the side effects of doing this for all ints and floats + + from osbot_utils.helpers.Safe_Id import Safe_Id + from osbot_utils.helpers.Timestamp_Now import Timestamp_Now + from osbot_utils.helpers.Random_Guid import Random_Guid + + TYPE_SAFE__CONVERT_VALUE__SUPPORTED_TYPES = [Safe_Id, Random_Guid, Timestamp_Now] + if target is not None and attr_name is not None: if hasattr(target, '__annotations__'): obj_annotations = target.__annotations__ @@ -138,10 +143,11 @@ def dict_remove(data, target): del data[target] return data -class __(SimpleNamespace): - pass + def dict_to_obj(target): + from collections.abc import Mapping + if isinstance(target, Mapping): new_dict = {} for key, value in target.items(): @@ -168,6 +174,8 @@ def obj_to_dict(target): return target # Return non-object types as is def str_to_obj(target): + import json + if hasattr(target, 'json'): return dict_to_obj(target.json()) return dict_to_obj(json.loads(target)) @@ -255,6 +263,8 @@ def obj_base_classes(obj): return [obj_type for obj_type in type_base_classes(type(obj))] def type_mro(target): + import inspect + if type(target) is type: cls = target else: @@ -278,6 +288,10 @@ def obj_base_classes_names(obj, show_module=False): return names def obj_data(target, convert_value_to_str=True, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False): + import inspect + import types + from osbot_utils.utils.Str import str_unicode_escape, str_max_width + result = {} if show_internals: show_private = True # show_private will skip all internals, so need to make sure it is True @@ -301,12 +315,6 @@ def obj_data(target, convert_value_to_str=True, name_width=30, value_width=100, result[name] = value return result -# def obj_data(target=None): -# data = {} -# for key,value in obj_items(target): -# data[key] = value -# return data - def obj_dict(target=None): if target and hasattr(target,'__dict__'): return target.__dict__ @@ -358,6 +366,8 @@ def obj_is_attribute_annotation_of_type(target, attr_name, expected_type): return False def obj_is_type_union_compatible(var_type, compatible_types): + from typing import Union + origin = get_origin(var_type) if origin is Union: # For Union types, including Optionals args = get_args(var_type) # Get the argument types @@ -368,6 +378,7 @@ def obj_is_type_union_compatible(var_type, compatible_types): return var_type in compatible_types or var_type is type(None) # Check for direct compatibility or type(None) for non-Union types def value_type_matches_obj_annotation_for_union_attr(target, attr_name, value): + from typing import Union value_type = type(value) attribute_annotation = obj_attribute_annotation(target,attr_name) origin = get_origin(attribute_annotation) @@ -378,9 +389,11 @@ def value_type_matches_obj_annotation_for_union_attr(target, attr_name, value): def pickle_save_to_bytes(target: object) -> bytes: + import pickle return pickle.dumps(target) def pickle_load_from_bytes(pickled_data: bytes): + import pickle if type(pickled_data) is bytes: try: return pickle.loads(pickled_data) @@ -388,6 +401,8 @@ def pickle_load_from_bytes(pickled_data: bytes): return {} def value_type_matches_obj_annotation_for_attr(target, attr_name, value): + import typing + if hasattr(target, '__annotations__'): obj_annotations = target.__annotations__ if hasattr(obj_annotations,'get'): diff --git a/osbot_utils/utils/Toml.py b/osbot_utils/utils/Toml.py index c1794aab..30a87545 100644 --- a/osbot_utils/utils/Toml.py +++ b/osbot_utils/utils/Toml.py @@ -2,10 +2,6 @@ from osbot_utils.utils.Files import file_create, file_contents from osbot_utils.utils.Objects import dict_to_obj -if sys.version_info >= (3, 11): - import tomllib -else: - tomllib = None def dict_to_toml(data, indent_level=0): toml_str = "" @@ -39,9 +35,11 @@ def toml_dict_from_file(toml_file): def toml_to_dict(str_toml): - if tomllib is None: - raise NotImplementedError("TOML parsing is not supported in Python versions earlier than 3.11") - return tomllib.loads(str_toml) + if sys.version_info >= (3, 11): + import tomllib + return tomllib.loads(str_toml) + raise NotImplementedError("TOML parsing is not supported in Python versions earlier than 3.11") + def toml_obj_from_file(toml_file): data = toml_dict_from_file(toml_file) diff --git a/osbot_utils/version b/osbot_utils/version index c648947b..4435e319 100644 --- a/osbot_utils/version +++ b/osbot_utils/version @@ -1 +1 @@ -v1.82.0 +v1.82.2 diff --git a/pyproject.toml b/pyproject.toml index b131a28d..675ebc65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "osbot_utils" -version = "v1.82.0" +version = "v1.82.2" description = "OWASP Security Bot - Utils" authors = ["Dinis Cruz "] license = "MIT"