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.
- osbot_utils/helpers/Local_Cache.py +5 -7
- osbot_utils/helpers/Local_Caches.py +5 -5
- osbot_utils/helpers/llms/__init__.py +0 -0
- osbot_utils/helpers/llms/actions/LLM_Request__Execute.py +31 -0
- osbot_utils/helpers/llms/actions/Type_Safe__Schema_For__LLMs.py +213 -0
- osbot_utils/helpers/llms/actions/__init__.py +0 -0
- osbot_utils/helpers/llms/builders/LLM_Request__Builder.py +41 -0
- osbot_utils/helpers/llms/builders/LLM_Request__Builder__Open_AI.py +54 -0
- osbot_utils/helpers/llms/builders/LLM_Request__Factory.py +95 -0
- osbot_utils/helpers/llms/builders/__init__.py +0 -0
- osbot_utils/helpers/llms/cache/LLM_Cache__Path_Generator.py +83 -0
- osbot_utils/helpers/llms/cache/LLM_Request__Cache.py +112 -0
- osbot_utils/helpers/llms/cache/LLM_Request__Cache__File_System.py +237 -0
- osbot_utils/helpers/llms/cache/LLM_Request__Cache__Storage.py +85 -0
- osbot_utils/helpers/llms/cache/Virtual_Storage__Local__Folder.py +64 -0
- osbot_utils/helpers/llms/cache/Virtual_Storage__Sqlite.py +72 -0
- osbot_utils/helpers/llms/cache/__init__.py +0 -0
- osbot_utils/helpers/llms/platforms/__init__.py +0 -0
- osbot_utils/helpers/llms/platforms/open_ai/API__LLM__Open_AI.py +55 -0
- osbot_utils/helpers/llms/platforms/open_ai/__init__.py +0 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Cache__Index.py +9 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request.py +7 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Data.py +14 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Function_Call.py +8 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Message__Content.py +6 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Message__Role.py +9 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Response.py +9 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Response__Cache.py +13 -0
- osbot_utils/helpers/llms/schemas/__init__.py +0 -0
- osbot_utils/helpers/safe_str/Safe_Str.py +50 -0
- osbot_utils/helpers/safe_str/Safe_Str__File__Name.py +8 -0
- osbot_utils/helpers/safe_str/Safe_Str__File__Path.py +12 -0
- osbot_utils/helpers/safe_str/Safe_Str__Hash.py +14 -0
- osbot_utils/helpers/safe_str/Safe_Str__Text.py +9 -0
- osbot_utils/helpers/safe_str/Safe_Str__Text__Dangerous.py +9 -0
- osbot_utils/helpers/safe_str/__init__.py +0 -0
- osbot_utils/helpers/sqlite/Sqlite__Cursor.py +4 -6
- osbot_utils/helpers/sqlite/Sqlite__Database.py +1 -1
- osbot_utils/helpers/sqlite/Sqlite__Field.py +3 -8
- osbot_utils/helpers/sqlite/Sqlite__Table.py +1 -3
- osbot_utils/helpers/sqlite/domains/Sqlite__DB__Files.py +6 -2
- osbot_utils/helpers/sqlite/models/Sqlite__Field__Type.py +2 -2
- osbot_utils/helpers/sqlite/tables/Sqlite__Table__Files.py +5 -5
- osbot_utils/helpers/ssh/SSH__Execute.py +0 -1
- osbot_utils/helpers/ssh/SSH__Health_Check.py +4 -5
- osbot_utils/testing/performance/Performance_Measure__Session.py +1 -1
- osbot_utils/type_safe/Type_Safe__Base.py +38 -3
- osbot_utils/type_safe/Type_Safe__Dict.py +2 -8
- osbot_utils/type_safe/Type_Safe__List.py +8 -4
- osbot_utils/type_safe/Type_Safe__Method.py +46 -5
- osbot_utils/type_safe/Type_Safe__Tuple.py +1 -1
- osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py +3 -2
- osbot_utils/type_safe/shared/Type_Safe__Validation.py +6 -4
- osbot_utils/type_safe/steps/Type_Safe__Step__Default_Value.py +0 -2
- osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py +16 -0
- osbot_utils/type_safe/steps/Type_Safe__Step__Init.py +57 -1
- osbot_utils/utils/Files.py +8 -8
- osbot_utils/utils/Objects.py +0 -1
- osbot_utils/version +1 -1
- {osbot_utils-2.33.0.dist-info → osbot_utils-2.35.0.dist-info}/METADATA +2 -2
- {osbot_utils-2.33.0.dist-info → osbot_utils-2.35.0.dist-info}/RECORD +63 -30
- osbot_utils/helpers/cache_requests/flows/flow__Cache__Requests.py +0 -11
- {osbot_utils-2.33.0.dist-info → osbot_utils-2.35.0.dist-info}/LICENSE +0 -0
- {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 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,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
|
2
|
-
from osbot_utils.decorators.methods.
|
3
|
-
from osbot_utils.
|
4
|
-
from osbot_utils.
|
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
|
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
|
2
|
-
import
|
3
|
-
from
|
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
|
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,12 +1,12 @@
|
|
1
|
-
from osbot_utils.base_classes.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
|
4
|
-
from osbot_utils.utils.Status
|
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
|
2
|
-
|
3
|
-
from osbot_utils.utils.
|
4
|
-
from osbot_utils.utils.
|
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 *
|
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
|
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
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
37
|
-
|
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
|
-
|
57
|
-
|
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
|
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
|
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
|
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
|
-
|
273
|
-
|
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
|
-
|
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):
|