osbot-utils 2.33.0__py3-none-any.whl → 2.35.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. osbot_utils/helpers/Local_Cache.py +5 -7
  2. osbot_utils/helpers/Local_Caches.py +5 -5
  3. osbot_utils/helpers/llms/__init__.py +0 -0
  4. osbot_utils/helpers/llms/actions/LLM_Request__Execute.py +31 -0
  5. osbot_utils/helpers/llms/actions/Type_Safe__Schema_For__LLMs.py +213 -0
  6. osbot_utils/helpers/llms/actions/__init__.py +0 -0
  7. osbot_utils/helpers/llms/builders/LLM_Request__Builder.py +41 -0
  8. osbot_utils/helpers/llms/builders/LLM_Request__Builder__Open_AI.py +54 -0
  9. osbot_utils/helpers/llms/builders/LLM_Request__Factory.py +95 -0
  10. osbot_utils/helpers/llms/builders/__init__.py +0 -0
  11. osbot_utils/helpers/llms/cache/LLM_Cache__Path_Generator.py +83 -0
  12. osbot_utils/helpers/llms/cache/LLM_Request__Cache.py +112 -0
  13. osbot_utils/helpers/llms/cache/LLM_Request__Cache__File_System.py +237 -0
  14. osbot_utils/helpers/llms/cache/LLM_Request__Cache__Storage.py +85 -0
  15. osbot_utils/helpers/llms/cache/Virtual_Storage__Local__Folder.py +64 -0
  16. osbot_utils/helpers/llms/cache/Virtual_Storage__Sqlite.py +72 -0
  17. osbot_utils/helpers/llms/cache/__init__.py +0 -0
  18. osbot_utils/helpers/llms/platforms/__init__.py +0 -0
  19. osbot_utils/helpers/llms/platforms/open_ai/API__LLM__Open_AI.py +55 -0
  20. osbot_utils/helpers/llms/platforms/open_ai/__init__.py +0 -0
  21. osbot_utils/helpers/llms/schemas/Schema__LLM_Cache__Index.py +9 -0
  22. osbot_utils/helpers/llms/schemas/Schema__LLM_Request.py +7 -0
  23. osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Data.py +14 -0
  24. osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Function_Call.py +8 -0
  25. osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Message__Content.py +6 -0
  26. osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Message__Role.py +9 -0
  27. osbot_utils/helpers/llms/schemas/Schema__LLM_Response.py +9 -0
  28. osbot_utils/helpers/llms/schemas/Schema__LLM_Response__Cache.py +13 -0
  29. osbot_utils/helpers/llms/schemas/__init__.py +0 -0
  30. osbot_utils/helpers/safe_str/Safe_Str.py +50 -0
  31. osbot_utils/helpers/safe_str/Safe_Str__File__Name.py +8 -0
  32. osbot_utils/helpers/safe_str/Safe_Str__File__Path.py +12 -0
  33. osbot_utils/helpers/safe_str/Safe_Str__Hash.py +14 -0
  34. osbot_utils/helpers/safe_str/Safe_Str__Text.py +9 -0
  35. osbot_utils/helpers/safe_str/Safe_Str__Text__Dangerous.py +9 -0
  36. osbot_utils/helpers/safe_str/__init__.py +0 -0
  37. osbot_utils/helpers/sqlite/Sqlite__Cursor.py +4 -6
  38. osbot_utils/helpers/sqlite/Sqlite__Database.py +1 -1
  39. osbot_utils/helpers/sqlite/Sqlite__Field.py +3 -8
  40. osbot_utils/helpers/sqlite/Sqlite__Table.py +1 -3
  41. osbot_utils/helpers/sqlite/domains/Sqlite__DB__Files.py +6 -2
  42. osbot_utils/helpers/sqlite/models/Sqlite__Field__Type.py +2 -2
  43. osbot_utils/helpers/sqlite/tables/Sqlite__Table__Files.py +5 -5
  44. osbot_utils/helpers/ssh/SSH__Execute.py +0 -1
  45. osbot_utils/helpers/ssh/SSH__Health_Check.py +4 -5
  46. osbot_utils/testing/performance/Performance_Measure__Session.py +1 -1
  47. osbot_utils/type_safe/Type_Safe__Base.py +38 -3
  48. osbot_utils/type_safe/Type_Safe__Dict.py +2 -8
  49. osbot_utils/type_safe/Type_Safe__List.py +8 -4
  50. osbot_utils/type_safe/Type_Safe__Method.py +46 -5
  51. osbot_utils/type_safe/Type_Safe__Tuple.py +1 -1
  52. osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py +3 -2
  53. osbot_utils/type_safe/shared/Type_Safe__Validation.py +6 -4
  54. osbot_utils/type_safe/steps/Type_Safe__Step__Default_Value.py +0 -2
  55. osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py +16 -0
  56. osbot_utils/type_safe/steps/Type_Safe__Step__Init.py +57 -1
  57. osbot_utils/utils/Files.py +8 -8
  58. osbot_utils/utils/Objects.py +0 -1
  59. osbot_utils/version +1 -1
  60. {osbot_utils-2.33.0.dist-info → osbot_utils-2.35.0.dist-info}/METADATA +2 -2
  61. {osbot_utils-2.33.0.dist-info → osbot_utils-2.35.0.dist-info}/RECORD +63 -30
  62. osbot_utils/helpers/cache_requests/flows/flow__Cache__Requests.py +0 -11
  63. {osbot_utils-2.33.0.dist-info → osbot_utils-2.35.0.dist-info}/LICENSE +0 -0
  64. {osbot_utils-2.33.0.dist-info → osbot_utils-2.35.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,14 @@
1
+ from typing import List, Optional
2
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Request__Function_Call import Schema__LLM_Request__Function_Call
3
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Request__Message__Content import Schema__LLM_Request__Message__Content
4
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
5
+
6
+ class Schema__LLM_Request__Data(Type_Safe): # Schema for LLM API request data
7
+ model : str # LLM model identifier
8
+ platform : str
9
+ provider : str
10
+ messages : List [Schema__LLM_Request__Message__Content] # Message content entries
11
+ function_call : Optional[Schema__LLM_Request__Function_Call ] = None # Details of function call
12
+ temperature : Optional[float ] = None # Model temperature (0-1)
13
+ top_p : Optional[float ] = None # Nucleus sampling parameter
14
+ max_tokens : Optional[int ] = None # Maximum tokens to generate
@@ -0,0 +1,8 @@
1
+ from typing import Type
2
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
3
+
4
+
5
+ class Schema__LLM_Request__Function_Call(Type_Safe):
6
+ parameters : Type[Type_Safe] # Class to generate schema from for function calling
7
+ function_name : str # Name of the function to call
8
+ description : str # Description of the function
@@ -0,0 +1,6 @@
1
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Request__Message__Role import Schema__LLM_Request__Message__Role
2
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
3
+
4
+ class Schema__LLM_Request__Message__Content(Type_Safe): # Schema for message content in LLM requests.
5
+ role : Schema__LLM_Request__Message__Role # Message role (system, user, assistant)
6
+ content: str # Message content
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Schema__LLM_Request__Message__Role(Enum):
5
+ ASSISTANT : str = 'assistant'
6
+ SYSTEM : str = 'system'
7
+ USER : str = 'user'
8
+
9
+
@@ -0,0 +1,9 @@
1
+ from osbot_utils.helpers.Obj_Id import Obj_Id
2
+ from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
3
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
4
+
5
+
6
+ class Schema__LLM_Response(Type_Safe):
7
+ response_id : Obj_Id
8
+ timestamp : Timestamp_Now
9
+ response_data : dict
@@ -0,0 +1,13 @@
1
+ from osbot_utils.helpers.Obj_Id import Obj_Id
2
+ from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
3
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Request import Schema__LLM_Request
4
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Response import Schema__LLM_Response
5
+ from osbot_utils.helpers.safe_str.Safe_Str__Hash import Safe_Str__Hash
6
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
7
+
8
+ class Schema__LLM_Response__Cache(Type_Safe):
9
+ cache_id : Obj_Id
10
+ hash__request : Safe_Str__Hash = None
11
+ llm_response : Schema__LLM_Response = None
12
+ llm_request : Schema__LLM_Request = None
13
+ timestamp : Timestamp_Now
File without changes
@@ -0,0 +1,50 @@
1
+ import re
2
+ from typing import Optional
3
+
4
+ TYPE_SAFE__STR__REGEX__SAFE_STR = re.compile(r'[^a-zA-Z0-9]') # Only allow alphanumerics and numbers
5
+ TYPE_SAFE__STR__MAX_LENGTH = 512
6
+
7
+ class Safe_Str(str):
8
+ max_length : int = TYPE_SAFE__STR__MAX_LENGTH
9
+ regex : re.Pattern = TYPE_SAFE__STR__REGEX__SAFE_STR
10
+ replacement_char : str = '_'
11
+ allow_empty : bool = True
12
+ trim_whitespace : bool = False
13
+ allow_all_replacement_char: bool = True
14
+ strict_validation : bool = False # If True, don't replace invalid chars, raise an error instead
15
+ exact_length : bool = False # If True, require exact length match, not just max length
16
+
17
+
18
+ def __new__(cls, value: Optional[str] = None) -> 'Safe_Str':
19
+
20
+ if value is None: # Validate inputs
21
+ if cls.allow_empty:
22
+ value = ""
23
+ else:
24
+ raise ValueError("Value cannot be None when allow_empty is False")
25
+
26
+ if not isinstance(value, str): # Convert to string if not already
27
+ value = str(value)
28
+
29
+ if cls.trim_whitespace: # Trim whitespace if requested
30
+ value = value.strip()
31
+
32
+ if not cls.allow_empty and (value is None or value == ""): # Check for empty string if not allowed
33
+ raise ValueError("Value cannot be empty when allow_empty is False")
34
+
35
+ if cls.exact_length and len(value) != cls.max_length:
36
+ raise ValueError(f"Value must be exactly {cls.max_length} characters long (was {len(value)})")
37
+ elif not cls.exact_length and len(value) > cls.max_length: # Check max length
38
+ raise ValueError(f"Value exceeds maximum length of {cls.max_length} characters (was {len(value)})")
39
+
40
+ if cls.strict_validation: # If using strict validation, check if the value matches the regex pattern exactly
41
+ if not cls.regex.search(value) is None: # If there are non-matching characters
42
+ raise ValueError(f"Value contains invalid characters (must match pattern: {cls.regex.pattern})")
43
+ sanitized_value = value
44
+ else:
45
+ sanitized_value = cls.regex.sub(cls.replacement_char, value) # Apply regex sanitization
46
+
47
+ if not cls.allow_all_replacement_char and set(sanitized_value) == {cls.replacement_char} and sanitized_value: # Check if sanitized value consists entirely of replacement characters
48
+ raise ValueError(f"Sanitized value consists entirely of '{cls.replacement_char}' characters")
49
+
50
+ return str.__new__(cls, sanitized_value)
@@ -0,0 +1,8 @@
1
+ import re
2
+ from osbot_utils.helpers.safe_str.Safe_Str import Safe_Str
3
+
4
+ class Safe_Str__File__Name(Safe_Str):
5
+ regex = re.compile(r'[^a-zA-Z0-9_\-. ]')
6
+ allow_empty = False
7
+ trim_whitespace = True
8
+ allow_all_replacement_char = False
@@ -0,0 +1,12 @@
1
+ import re
2
+ from osbot_utils.helpers.safe_str.Safe_Str import Safe_Str
3
+
4
+ TYPE_SAFE_STR__FILE_PATH__REGEX = re.compile(r'[^a-zA-Z0-9_\-./\\ ]') # Allow alphanumerics, underscores, hyphens, dots, slashes, and spaces
5
+ TYPE_SAFE_STR__FILE_PATH__MAX_LENGTH = 1024
6
+
7
+ class Safe_Str__File__Path(Safe_Str):
8
+ regex = TYPE_SAFE_STR__FILE_PATH__REGEX
9
+ max_length = TYPE_SAFE_STR__FILE_PATH__MAX_LENGTH
10
+ allow_empty = True
11
+ trim_whitespace = True
12
+ allow_all_replacement_char = False
@@ -0,0 +1,14 @@
1
+ import re
2
+ from osbot_utils.helpers.safe_str.Safe_Str import Safe_Str
3
+
4
+ # Constants for hash validation
5
+ SIZE__VALUE_HASH = 10
6
+ TYPE_SAFE_STR__HASH__REGEX = re.compile(r'[^a-fA-F0-9]') # Only allow hexadecimal characters
7
+
8
+ class Safe_Str__Hash(Safe_Str):
9
+ regex = TYPE_SAFE_STR__HASH__REGEX
10
+ max_length = SIZE__VALUE_HASH
11
+ allow_empty = False # Don't allow empty hash values
12
+ trim_whitespace = True # Trim any whitespace
13
+ strict_validation = True # Enable strict validation - new attribute
14
+ exact_length = True # Require exact length match - new attribute
@@ -0,0 +1,9 @@
1
+ import re
2
+ from osbot_utils.helpers.safe_str.Safe_Str import Safe_Str
3
+
4
+ TYPE_SAFE_STR__TEXT__MAX_LENGTH = 4096
5
+ TYPE_SAFE_STR__TEXT__REGEX = r'[^a-zA-Z0-9_ ()\[\]\-+=:;,.?]'
6
+
7
+ class Safe_Str__Text(Safe_Str):
8
+ regex = re.compile(TYPE_SAFE_STR__TEXT__REGEX)
9
+ max_length = TYPE_SAFE_STR__TEXT__MAX_LENGTH
@@ -0,0 +1,9 @@
1
+ import re
2
+ from osbot_utils.helpers.safe_str.Safe_Str import Safe_Str
3
+
4
+ TYPE_SAFE_STR__TEXT__DANGEROUS__MAX_LENGTH = 65536
5
+ TYPE_SAFE_STR__TEXT__DANGEROUS__REGEX = r'[^a-zA-Z0-9_\s!@#$%^&*()\[\]{}\-+=:;,.?"/\\<>\']'
6
+
7
+ class Safe_Str__Text__Dangerous(Safe_Str):
8
+ regex = re.compile(TYPE_SAFE_STR__TEXT__DANGEROUS__REGEX)
9
+ max_length = TYPE_SAFE_STR__TEXT__DANGEROUS__MAX_LENGTH
File without changes
@@ -1,9 +1,7 @@
1
- from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
2
- from osbot_utils.decorators.methods.cache import cache
3
- from osbot_utils.decorators.methods.cache_on_self import cache_on_self
4
- from osbot_utils.helpers.sqlite.Sqlite__Database import Sqlite__Database
5
- from osbot_utils.utils.Status import status_ok, status_error, status_exception
6
-
1
+ from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
2
+ from osbot_utils.decorators.methods.cache_on_self import cache_on_self
3
+ from osbot_utils.helpers.sqlite.Sqlite__Database import Sqlite__Database
4
+ from osbot_utils.utils.Status import status_ok, status_error, status_exception
7
5
 
8
6
  class Sqlite__Cursor(Kwargs_To_Self):
9
7
  database : Sqlite__Database
@@ -1,4 +1,4 @@
1
- from osbot_utils.type_safe.Type_Safe import Type_Safe
1
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
2
2
  from osbot_utils.decorators.methods.cache_on_self import cache_on_self
3
3
  from osbot_utils.utils.Files import current_temp_folder, path_combine, folder_create, file_exists, file_delete
4
4
  from osbot_utils.utils.Misc import random_filename
@@ -1,11 +1,6 @@
1
- import inspect
2
- import typing
3
- from decimal import Decimal
4
- from enum import auto, Enum
5
- from typing import Optional, Union
6
-
7
- from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
8
- from osbot_utils.helpers.sqlite.models.Sqlite__Field__Type import Sqlite__Field__Type
1
+ from typing import Optional, Union
2
+ from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
3
+ from osbot_utils.helpers.sqlite.models.Sqlite__Field__Type import Sqlite__Field__Type
9
4
 
10
5
 
11
6
  class Sqlite__Field(Kwargs_To_Self):
@@ -1,5 +1,4 @@
1
1
  import re
2
-
3
2
  from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
4
3
  from osbot_utils.decorators.lists.filter_list import filter_list
5
4
  from osbot_utils.decorators.lists.index_by import index_by
@@ -8,10 +7,9 @@ from osbot_utils.helpers.Print_Table import Print_Table
8
7
  from osbot_utils.helpers.sqlite.Sqlite__Database import Sqlite__Database
9
8
  from osbot_utils.helpers.sqlite.Sqlite__Globals import DEFAULT_FIELD_NAME__ID, ROW_BASE_CLASS, SQL_TABLE__MODULE_NAME__ROW_SCHEMA
10
9
  from osbot_utils.helpers.sqlite.models.Sqlite__Field__Type import Sqlite__Field__Type
11
- from osbot_utils.utils.Dev import pprint
12
10
  from osbot_utils.utils.Json import json_load
13
11
  from osbot_utils.utils.Misc import list_set
14
- from osbot_utils.utils.Objects import base_types, default_value, bytes_to_obj, obj_to_bytes
12
+ from osbot_utils.utils.Objects import base_types, default_value, bytes_to_obj, obj_to_bytes
15
13
  from osbot_utils.utils.Str import str_cap_snake_case
16
14
 
17
15
  class Sqlite__Table(Kwargs_To_Self):
@@ -1,7 +1,7 @@
1
1
  from osbot_utils.decorators.lists.index_by import index_by
2
2
  from osbot_utils.decorators.methods.cache_on_self import cache_on_self
3
3
  from osbot_utils.helpers.sqlite.domains.Sqlite__DB__Local import Sqlite__DB__Local
4
-
4
+ from osbot_utils.utils.Json import str_to_json
5
5
 
6
6
 
7
7
  class Sqlite__DB__Files(Sqlite__DB__Local):
@@ -21,9 +21,13 @@ class Sqlite__DB__Files(Sqlite__DB__Local):
21
21
  def file(self, path, include_contents=False):
22
22
  return self.table_files().file(path, include_contents=include_contents)
23
23
 
24
- def file_contents(self, path):
24
+ def file_contents(self, path) -> str:
25
25
  return self.table_files().file_contents(path)
26
26
 
27
+ def file_contents__json(self, path) -> dict:
28
+ file_contents = self.file_contents(path)
29
+ return str_to_json(file_contents)
30
+
27
31
  def file__with_content(self, path):
28
32
  return self.file(path, include_contents=True)
29
33
 
@@ -1,7 +1,7 @@
1
1
  import sys
2
2
  import types
3
- from decimal import Decimal
4
- from enum import Enum, auto
3
+ from decimal import Decimal
4
+ from enum import Enum, auto
5
5
  from osbot_utils.decorators.methods.cache import cache
6
6
 
7
7
  if sys.version_info >= (3, 10):
@@ -1,12 +1,12 @@
1
- from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
1
+ from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
2
2
  from osbot_utils.helpers.sqlite.Sqlite__Table import Sqlite__Table
3
- from osbot_utils.utils.Misc import timestamp_utc_now, bytes_sha256, str_sha256
4
- from osbot_utils.utils.Status import status_warning, status_ok
3
+ from osbot_utils.utils.Misc import timestamp_utc_now, bytes_sha256, str_sha256
4
+ from osbot_utils.utils.Status import status_warning, status_ok
5
5
 
6
6
  SQLITE__TABLE_NAME__FILES = 'files'
7
7
 
8
8
  class Schema__Table__Files(Kwargs_To_Self):
9
- path : str
9
+ path : str # todo: add support for using Safe_Str__File__Path (this will need changes to how Sqlite__Field__Type is mapped in add_field_with_type)
10
10
  contents : bytes
11
11
  metadata : bytes
12
12
  timestamp: int
@@ -50,7 +50,7 @@ class Sqlite__Table__Files(Sqlite__Table):
50
50
  return status_ok(message='file deleted')
51
51
 
52
52
  def create_node_data(self, path, contents=None, metadata= None):
53
- node_data = {'path' : path ,
53
+ node_data = {'path' : str(path),
54
54
  'contents': contents ,
55
55
  'metadata': metadata }
56
56
  if self.set_timestamp:
@@ -2,7 +2,6 @@ from osbot_utils.type_safe.Type_Safe import Type_Safe
2
2
  from osbot_utils.helpers.duration.decorators.capture_duration import capture_duration
3
3
  from osbot_utils.decorators.lists.group_by import group_by
4
4
  from osbot_utils.decorators.lists.index_by import index_by
5
- from osbot_utils.utils.Dev import pprint
6
5
  from osbot_utils.utils.Env import get_env
7
6
  from osbot_utils.utils.Http import is_port_open
8
7
  from osbot_utils.utils.Misc import str_to_int, str_to_bool
@@ -1,8 +1,7 @@
1
- from osbot_utils.helpers.ssh.SSH__Execute import ENV_VAR__SSH__HOST, ENV_VAR__SSH__KEY_FILE, ENV_VAR__SSH__USER, \
2
- ENV_VAR__SSH__PORT, ENV_VAR__SSH__STRICT_HOST_CHECK, SSH__Execute
3
- from osbot_utils.utils.Env import get_env
4
- from osbot_utils.utils.Misc import list_set
5
- from osbot_utils.utils.Status import status_ok, status_error
1
+ from osbot_utils.helpers.ssh.SSH__Execute import ENV_VAR__SSH__HOST, ENV_VAR__SSH__KEY_FILE, ENV_VAR__SSH__USER, ENV_VAR__SSH__PORT, ENV_VAR__SSH__STRICT_HOST_CHECK, SSH__Execute
2
+ from osbot_utils.utils.Env import get_env
3
+ from osbot_utils.utils.Misc import list_set
4
+ from osbot_utils.utils.Status import status_ok, status_error
6
5
 
7
6
  ENV_VARS__FOR_SSH = {'ssh_host' : ENV_VAR__SSH__HOST ,
8
7
  'ssh_key_file' : ENV_VAR__SSH__KEY_FILE ,
@@ -122,7 +122,7 @@ class Performance_Measure__Session(Type_Safe):
122
122
  if self.assert_enabled is False:
123
123
  return
124
124
  if in_github_action():
125
- max_time = max_time * 5 # adjust for GitHub's slowness
125
+ max_time = max_time * 6 # adjust for GitHub's slowness
126
126
 
127
127
  assert self.result.final_score <= max_time, f"Performance changed for {self.result.name}: got {self.result.final_score:,d}ns, expected less than {max_time}ns"
128
128
 
@@ -1,3 +1,4 @@
1
+ import types
1
2
  from typing import get_args, Union, Optional, Any, ForwardRef
2
3
  from osbot_utils.helpers.Obj_Id import Obj_Id
3
4
  from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
@@ -28,9 +29,9 @@ class Type_Safe__Base:
28
29
  actual_type_name = type_str(type(item))
29
30
  raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
30
31
 
31
- elif origin is list and args: # Expected type is List[...]
32
+ elif origin in (list, set) and args: # Expected type is List[...]
32
33
  (item_type,) = args
33
- if not isinstance(item, list):
34
+ if not isinstance(item, (list,set)):
34
35
  expected_type_name = type_str(expected_type)
35
36
  actual_type_name = type_str(type(item))
36
37
  raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
@@ -81,6 +82,20 @@ class Type_Safe__Base:
81
82
  expected_type_name = type_str(expected_type)
82
83
  actual_type_name = type_str(type(item))
83
84
  raise TypeError(f"Expected '{expected_type_name}', but got '{actual_type_name}'")
85
+ elif origin is type: # Expected type is Type[T]
86
+ if type(item) is str:
87
+ item = deserialize_type__using_value(item)
88
+ if not isinstance(item, type): # First check if item is actually a type
89
+ expected_type_name = type_str(expected_type)
90
+ actual_type_name = type_str(type(item))
91
+ raise TypeError(f"Expected {expected_type_name}, but got instance: {actual_type_name}")
92
+
93
+ args = get_args(expected_type)
94
+ if args: # Check if there are type arguments
95
+ type_arg = args[0] # Then check if item is a subclass of T
96
+ if not issubclass(item, type_arg):
97
+ raise TypeError(f"Expected subclass of {type_str(type_arg)}, got {type_str(item)}")
98
+ return True # If no args, any type is valid
84
99
  else:
85
100
  if isinstance(item, origin):
86
101
  return True
@@ -103,4 +118,24 @@ def type_str(tp):
103
118
  else:
104
119
  args = get_args(tp)
105
120
  args_str = ', '.join(type_str(arg) for arg in args)
106
- return f"{origin.__name__}[{args_str}]"
121
+ return f"{origin.__name__}[{args_str}]"
122
+
123
+ # todo: this is duplicated from Type_Safe__Step__From_Json (review and figure out how to do this more centrally)
124
+ def deserialize_type__using_value(value): # TODO: Check the security implications of this deserialisation
125
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
126
+ if value:
127
+ try:
128
+ module_name, type_name = value.rsplit('.', 1)
129
+ if module_name == 'builtins' and type_name == 'NoneType': # Special case for NoneType (which serialises as builtins.* , but it actually in types.* )
130
+ value = types.NoneType
131
+ else:
132
+ module = __import__(module_name, fromlist=[type_name])
133
+ value = getattr(module, type_name)
134
+ if isinstance(value, type) is False:
135
+ raise ValueError(f"Security alert, in deserialize_type__using_value only classes are allowed")
136
+ # todo: figure out a way to do this
137
+ # if issubclass(value, (Type_Safe, str, int)) is False:
138
+ # raise ValueError(f"Security alert, in deserialize_type__using_value only class of type Type_Safe, str, int are allowed")
139
+ except (ValueError, ImportError, AttributeError) as e:
140
+ raise ValueError(f"Could not reconstruct type from '{value}': {str(e)}")
141
+ return value
@@ -1,5 +1,4 @@
1
- from typing import Type
2
-
1
+ from typing import Type
3
2
  from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base
4
3
 
5
4
  class Type_Safe__Dict(Type_Safe__Base, dict):
@@ -9,13 +8,8 @@ class Type_Safe__Dict(Type_Safe__Base, dict):
9
8
  self.expected_key_type = expected_key_type
10
9
  self.expected_value_type = expected_value_type
11
10
 
12
- # todo: see if we need to do this, since there was not code coverage hitting it
13
- # for k, v in self.items(): # check type-safety of ctor arguments
14
- # self.is_instance_of_type(k, self.expected_key_type )
15
- # self.is_instance_of_type(v, self.expected_value_type)
16
-
17
11
  def __setitem__(self, key, value): # Check type-safety before allowing assignment.
18
- self.is_instance_of_type(key, self.expected_key_type)
12
+ self.is_instance_of_type(key , self.expected_key_type)
19
13
  self.is_instance_of_type(value, self.expected_value_type)
20
14
  super().__setitem__(key, value)
21
15
 
@@ -11,10 +11,14 @@ class Type_Safe__List(Type_Safe__Base, list):
11
11
  return f"list[{expected_type_name}] with {len(self)} elements"
12
12
 
13
13
  def append(self, item):
14
- try:
15
- self.is_instance_of_type(item, self.expected_type)
16
- except TypeError as e:
17
- raise TypeError(f"In Type_Safe__List: Invalid type for item: {e}")
14
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
15
+ if type(self.expected_type) is type and issubclass(self.expected_type, Type_Safe) and type(item) is dict: # if self.expected_type is Type_Safe and we have a dict
16
+ item = self.expected_type.from_json(item) # try to convert the dict into self.expected_type
17
+ else:
18
+ try:
19
+ self.is_instance_of_type(item, self.expected_type)
20
+ except TypeError as e:
21
+ raise TypeError(f"In Type_Safe__List: Invalid type for item: {e}")
18
22
  super().append(item)
19
23
 
20
24
 
@@ -2,6 +2,8 @@ import inspect
2
2
  from enum import Enum
3
3
  from typing import get_args, get_origin, Union, List, Any, Dict # For type hinting utilities
4
4
 
5
+ from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
6
+
5
7
 
6
8
  class Type_Safe__Method: # Class to handle method type safety validation
7
9
  def __init__(self, func): # Initialize with function
@@ -29,12 +31,23 @@ class Type_Safe__Method:
29
31
  return bound_args # Return bound arguments
30
32
 
31
33
  def validate_parameter(self, param_name: str, param_value: Any, bound_args): # Validate a single parameter
34
+ self.validate_immutable_parameter(param_name, param_value) # Validata the param_value (make sure if it set it is on of IMMUTABLE_TYPES)
32
35
  if param_name in self.annotations: # Check if parameter is annotated
33
36
  expected_type = self.annotations[param_name] # Get expected type
34
37
  self.check_parameter_value(param_name, param_value, expected_type, bound_args)# Check value against type
35
38
 
36
- def check_parameter_value(self, param_name: str, param_value: Any, # Check parameter value against expected type
37
- expected_type: Any, bound_args): # Method parameters
39
+ def validate_immutable_parameter(self, param_name, param_value):
40
+ param = self.sig.parameters.get(param_name) # Check if this is a default value from a mutable type
41
+ if param and param.default is param_value: # This means the default value is being used
42
+ if param_value is not None:
43
+ if isinstance(param_value, IMMUTABLE_TYPES) is False: # Check if the value is an instance of any IMMUTABLE_TYPES
44
+ raise ValueError(f"Parameter '{param_name}' has a mutable default value of type '{type(param_value).__name__}'. "
45
+ f"Only immutable types are allowed as default values in type_safe functions.")
46
+
47
+ def check_parameter_value(self, param_name : str,
48
+ param_value : Any, # Check parameter value against expected type
49
+ expected_type: Any,
50
+ bound_args ): # Method parameters
38
51
  is_optional = self.is_optional_type(expected_type) # Check if type is optional
39
52
  has_default = self.has_default_value(param_name) # Check if has default value
40
53
 
@@ -42,8 +55,23 @@ class Type_Safe__Method:
42
55
  self.validate_none_value(param_name, is_optional, has_default) # Validate None value
43
56
  return # Exit early
44
57
 
58
+ if is_optional: # Extract the non-None type from Optional
59
+ non_none_type = next(arg for arg in get_args(expected_type) if arg is not type(None))
60
+ non_none_origin = get_origin(non_none_type)
61
+
62
+ if non_none_origin is type: # If it's Optional[Type[T]], validate it
63
+ self.validate_type_parameter(param_name, param_value, non_none_type)
64
+ return
65
+ else: # If it's any other Optional type, validate against the non-None type
66
+ self.check_parameter_value(param_name, param_value, non_none_type, bound_args)
67
+ return
68
+
45
69
  origin_type = get_origin(expected_type) # Get base type
46
70
 
71
+ if origin_type is type:
72
+ self.validate_type_parameter(param_name, param_value, expected_type)
73
+ return
74
+
47
75
  if self.is_list_type(origin_type): # Check if list type
48
76
  self.validate_list_type(param_name, param_value, expected_type) # Validate list
49
77
  return # Exit early
@@ -52,9 +80,9 @@ class Type_Safe__Method:
52
80
  self.validate_union_type(param_name, param_value, expected_type) # Validate union
53
81
  return # Exit early
54
82
 
55
- if self.try_basic_type_conversion(param_value, expected_type, param_name, # Try basic type conversion
56
- bound_args): # Pass bound args
57
- return # Exit if conversion successful
83
+ # if self.try_basic_type_conversion(param_value, expected_type, param_name, # Try basic type conversion
84
+ # bound_args): # Pass bound args
85
+ # return # Exit if conversion successful
58
86
 
59
87
  self.validate_direct_type(param_name, param_value, expected_type) # Direct type validation
60
88
 
@@ -85,6 +113,19 @@ class Type_Safe__Method:
85
113
  if not isinstance(item, item_type): # Validate item type
86
114
  raise ValueError(f"List item at index {i} expected type {item_type}, but got {type(item)}") # Raise error for invalid item
87
115
 
116
+ def validate_type_parameter(self, param_name: str, param_value: Any, expected_type: Any): # Validate a Type[T] parameter
117
+ if not isinstance(param_value, type):
118
+ raise ValueError(f"Parameter '{param_name}' expected a type class but got {type(param_value)}")
119
+
120
+
121
+ type_args = get_args(expected_type) # Extract the T from Type[T]
122
+ if type_args:
123
+ required_base = type_args[0] # get direct type (this doesn't handle Forward refs)
124
+ if hasattr(required_base, '__origin__') or isinstance(required_base, type):
125
+ if not issubclass(param_value, required_base):
126
+ raise ValueError(f"Parameter '{param_name}' expected type {expected_type}, but got "
127
+ f"{param_value.__module__}.{param_value.__name__} which is not a subclass of {required_base}")
128
+
88
129
  def is_union_type(self, origin_type: Any, is_optional: bool) -> bool: # Check if type is a Union
89
130
  return origin_type is Union and not is_optional # Must be Union but not Optional
90
131
 
@@ -8,7 +8,7 @@ class Type_Safe__Tuple(Type_Safe__Base, tuple):
8
8
  instance.expected_types = expected_types
9
9
  return instance
10
10
 
11
- def __init__(self, expected_types, items=None): # todo: see if we should be assining expected_types to self here
11
+ def __init__(self, expected_types, items=None): # todo: see if we should be assigning expected_types to self here
12
12
  if items:
13
13
  self.validate_items(items)
14
14
 
@@ -1,8 +1,9 @@
1
1
  import types
2
2
  from enum import EnumMeta
3
3
 
4
- from osbot_utils.helpers.Safe_Id import Safe_Id
4
+ from osbot_utils.helpers.Safe_Id import Safe_Id
5
+ from osbot_utils.helpers.safe_str.Safe_Str import Safe_Str
5
6
 
6
7
  IMMUTABLE_TYPES = (bool, int, float, complex, str, bytes, types.NoneType, EnumMeta, type,
7
- Safe_Id # ok to add since these classes use str as a base class
8
+ #Safe_Id, Safe_Str # ok to add since these classes use str as a base class # todo: see if we still need these
8
9
  )
@@ -4,6 +4,7 @@ import types
4
4
  import typing
5
5
  from enum import EnumMeta
6
6
  from typing import Any, Annotated, Optional, get_args, get_origin, ForwardRef, Type, Dict, _GenericAlias
7
+ from osbot_utils.helpers.safe_str.Safe_Str import Safe_Str
7
8
  from osbot_utils.type_safe.shared.Type_Safe__Annotations import type_safe_annotations
8
9
  from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
9
10
  from osbot_utils.type_safe.shared.Type_Safe__Shared__Variables import IMMUTABLE_TYPES
@@ -268,9 +269,9 @@ class Type_Safe__Validation:
268
269
  direct_type_match = type_safe_validation.check_if__type_matches__obj_annotation__for_attr(target, name, value)
269
270
  union_type_match = type_safe_validation.check_if__type_matches__obj_annotation__for_union_and_annotated(target, name, value)
270
271
 
271
- is_invalid = (direct_type_match is False and union_type_match is None) or \
272
- (direct_type_match is None and union_type_match is False) or \
273
- (direct_type_match is False and union_type_match is False)
272
+ is_invalid = (direct_type_match is False and union_type_match is None ) or \
273
+ (direct_type_match is None and union_type_match is False) or \
274
+ (direct_type_match is False and union_type_match is False)
274
275
 
275
276
  if is_invalid:
276
277
  expected_type = annotations.get(name)
@@ -286,7 +287,8 @@ class Type_Safe__Validation:
286
287
  if self.obj_is_type_union_compatible(var_type, IMMUTABLE_TYPES) is False: # if var_type is not something like Optional[Union[int, str]]
287
288
  if var_type not in IMMUTABLE_TYPES or type(var_type) not in IMMUTABLE_TYPES:
288
289
  if not isinstance(var_type, EnumMeta):
289
- type_safe_raise_exception.immutable_type_error(var_name, var_type)
290
+ if not issubclass(var_type, str):
291
+ type_safe_raise_exception.immutable_type_error(var_name, var_type)
290
292
 
291
293
  def validate_variable_type(self, var_name, var_type, var_value): # Validate type compatibility
292
294
  if type(var_type) is type and not isinstance(var_value, var_type):
@@ -1,8 +1,6 @@
1
-
2
1
  import sys
3
2
  import inspect
4
3
  import typing
5
-
6
4
  from osbot_utils.type_safe.Type_Safe__Set import Type_Safe__Set
7
5
  from osbot_utils.type_safe.Type_Safe__Tuple import Type_Safe__Tuple
8
6
  from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache