Skip to content

Commit d4a8943

Browse files
committed
added 3 new transformers: Type_Safe__To__Json, Type_Safe__To__OpenAPI and Type_Safe__To__LLM_Tools
1 parent f06c852 commit d4a8943

10 files changed

Lines changed: 1561 additions & 10 deletions

osbot_fast_api/api/routes/Routes__Set_Cookie.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import Request, Response
22
from fastapi.responses import HTMLResponse
33
from osbot_utils.type_safe.Type_Safe import Type_Safe
4-
from osbot_utils.utils.Env import get_env, load_dotenv
4+
from osbot_utils.utils.Env import get_env, load_dotenv
55
from osbot_fast_api.api.routes.Fast_API__Routes import Fast_API__Routes
66
from osbot_fast_api.schemas.consts__Fast_API import ENV_VAR__FAST_API__AUTH__API_KEY__NAME
77

osbot_fast_api/api/transformers/Type_Safe__To__Dataclass.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from typing import Type, Dict, Any
2-
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
3-
from osbot_utils.type_safe.Type_Safe import Type_Safe
4-
from osbot_fast_api.api.transformers.BaseModel__To__Dataclass import basemodel__to__dataclass
5-
from osbot_fast_api.api.transformers.Type_Safe__To__BaseModel import type_safe__to__basemodel
1+
from typing import Type, Dict, Any
2+
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
3+
from osbot_utils.type_safe.Type_Safe import Type_Safe
4+
from osbot_fast_api.api.transformers.BaseModel__To__Dataclass import basemodel__to__dataclass
5+
from osbot_fast_api.api.transformers.Type_Safe__To__BaseModel import type_safe__to__basemodel
66

77

88
class Type_Safe__To__Dataclass(Type_Safe):
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
from typing import Type, Dict, Any, get_args, Union, Optional, Tuple
2+
from osbot_utils.type_safe.Type_Safe import Type_Safe
3+
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
4+
from osbot_utils.type_safe.primitives.safe_str.Safe_Str import Safe_Str
5+
from osbot_utils.type_safe.primitives.safe_int.Safe_Int import Safe_Int
6+
from osbot_utils.type_safe.primitives.safe_float.Safe_Float import Safe_Float
7+
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
8+
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Cache import type_safe_cache
9+
10+
11+
class Type_Safe__To__Json(Type_Safe): # Converts Type_Safe classes to JSON Schema (draft-07 compatible)
12+
schema_cache : Dict[Tuple[Type, bool], Dict[str, Any]]
13+
include_defaults : bool = True
14+
include_examples : bool = False
15+
strict_mode : bool = False # If True, includes all Type_Safe constraints
16+
17+
@type_safe
18+
def convert_class(self, type_safe_class : Type[Type_Safe] , # Type_Safe class to convert
19+
title : str = None , # Optional schema title
20+
description : str = None , # Optional schema description
21+
is_nested : bool = False
22+
) -> Dict[str, Any]: # Returns JSON Schema
23+
24+
25+
cache_key = (type_safe_class, is_nested) # Create a cache key that includes whether it's nested
26+
27+
if cache_key in self.schema_cache:
28+
return self.schema_cache[cache_key]
29+
30+
if type_safe_class in self.schema_cache: # Check cache first
31+
return self.schema_cache[type_safe_class]
32+
33+
schema = { "type" : "object" ,
34+
"title" : title or type_safe_class.__name__ ,
35+
"additionalProperties" : False } # Type_Safe classes are strict
36+
37+
if not is_nested:
38+
schema["$schema"] = "http://json-schema.org/draft-07/schema#" # Only add $schema for root level (non-nested) objects
39+
40+
if description:
41+
schema["description"] = description
42+
43+
properties = {}
44+
required = []
45+
46+
annotations = type_safe_cache.get_class_annotations(type_safe_class)
47+
cls_kwargs = type_safe_class.__cls_kwargs__()
48+
49+
for field_name, field_type in annotations:
50+
property_schema = self.convert_field_type(field_type)
51+
properties[field_name] = property_schema
52+
53+
if hasattr(type_safe_class, '__annotations_comments__'): # Add description from docstring if available
54+
comments = type_safe_class.__annotations_comments__
55+
if field_name in comments:
56+
property_schema["description"] = comments[field_name]
57+
58+
if field_name not in cls_kwargs: # Check if field is required (no default value)
59+
required.append(field_name)
60+
else:
61+
default_value = cls_kwargs[field_name]
62+
if self.include_defaults and default_value is not None:
63+
if isinstance(default_value, (str, int, float, bool)): # Add default to schema if it's a simple type
64+
property_schema["default"] = default_value
65+
66+
schema["properties"] = properties
67+
if required:
68+
schema["required"] = required
69+
70+
self.schema_cache[cache_key] = schema
71+
return schema
72+
73+
@type_safe
74+
def convert_field_type(self, field_type : Any # Field type to convert
75+
) -> Dict[str, Any]: # Returns JSON Schema for field
76+
77+
origin = type_safe_cache.get_origin(field_type)
78+
79+
if field_type is str: # Handle primitive types
80+
return {"type": "string"}
81+
elif field_type is int:
82+
return {"type": "integer"}
83+
elif field_type is float:
84+
return {"type": "number"}
85+
elif field_type is bool:
86+
return {"type": "boolean"}
87+
88+
if isinstance(field_type, type) and issubclass(field_type, Type_Safe__Primitive): # Handle Type_Safe__Primitive subclasses with their constraints
89+
schema = self.extract_primitive_schema(field_type)
90+
return schema
91+
92+
if origin is list: # Handle list/array types
93+
args = get_args(field_type)
94+
if args:
95+
return { "type" : "array" ,
96+
"items" : self.convert_field_type(args[0]) }
97+
return {"type": "array"}
98+
99+
if origin is dict:
100+
args = get_args(field_type) # Handle dict/object types
101+
if len(args) == 2:
102+
return { "type" : "object" , # JSON Schema doesn't support typed dict keys, so we use additionalProperties
103+
"additionalProperties" : self.convert_field_type(args[1]) }
104+
return {"type": "object"}
105+
106+
if origin is set: # Handle set types (convert to array with uniqueItems)
107+
args = get_args(field_type)
108+
schema = { "type" : "array" ,
109+
"uniqueItems" : True }
110+
if args:
111+
schema["items"] = self.convert_field_type(args[0])
112+
return schema
113+
114+
if origin in (Union, Optional): # Handle Union/Optional types
115+
args = get_args(field_type)
116+
117+
if type(None) in args: # Special case for Optional (Union with None)
118+
non_none_args = [arg for arg in args if arg is not type(None)]
119+
if len(non_none_args) == 1:
120+
schema = self.convert_field_type(non_none_args[0]) # Optional single type
121+
schema["nullable"] = True # JSON Schema draft-07 style
122+
return schema
123+
124+
return { "oneOf": [self.convert_field_type(arg) for arg in args] } # General Union case
125+
126+
if isinstance(field_type, type) and issubclass(field_type, Type_Safe): # This will register it in components_cache and return a $ref
127+
return self.convert_class(field_type, is_nested=True) # Call convert_class with is_nested=True for nested objects
128+
129+
return {"type": "object"} # Default fallback
130+
131+
@type_safe
132+
def extract_primitive_schema(self, primitive_class : Type[Type_Safe__Primitive] # Primitive class to analyze
133+
) -> Dict[str, Any]: # Returns schema with constraints
134+
135+
base_type = primitive_class.__primitive_base__
136+
137+
if base_type is str: # Start with base type schema
138+
schema = {"type": "string"}
139+
elif base_type is int:
140+
schema = {"type": "integer"}
141+
elif base_type is float:
142+
schema = {"type": "number"}
143+
else:
144+
schema = {"type": "string"} # Default fallback
145+
146+
if issubclass(primitive_class, Safe_Str): # Extract constraints from Safe_Str types
147+
if hasattr(primitive_class, 'max_length'):
148+
schema['maxLength'] = primitive_class.max_length
149+
if hasattr(primitive_class, 'regex') and self.strict_mode:
150+
if hasattr(primitive_class.regex, 'pattern'): # Only include regex pattern in strict mode
151+
schema['pattern'] = primitive_class.regex.pattern
152+
153+
if issubclass(primitive_class, Safe_Int): # Extract constraints from Safe_Int types
154+
if hasattr(primitive_class, 'min_value') and primitive_class.min_value is not None:
155+
schema['minimum'] = primitive_class.min_value
156+
if hasattr(primitive_class, 'max_value') and primitive_class.max_value is not None:
157+
schema['maximum'] = primitive_class.max_value
158+
159+
if issubclass(primitive_class, Safe_Float): # Extract constraints from Safe_Float types
160+
if hasattr(primitive_class, 'min_value') and primitive_class.min_value is not None:
161+
schema['minimum'] = primitive_class.min_value
162+
if hasattr(primitive_class, 'max_value') and primitive_class.max_value is not None:
163+
schema['maximum'] = primitive_class.max_value
164+
165+
return schema
166+
167+
@type_safe
168+
def convert_to_json_schema_string(self, type_safe_class : Type[Type_Safe] # Class to convert
169+
) -> str: # Returns JSON Schema as string
170+
import json
171+
schema = self.convert_class(type_safe_class)
172+
return json.dumps(schema, indent=2)
173+
174+
@type_safe
175+
def validate_against_schema(self, instance : Type_Safe , # Instance to validate
176+
schema : Dict[str, Any] # Schema to validate against
177+
) -> bool: # Returns True if valid | Validate a Type_Safe instance against a JSON schema
178+
try:
179+
import jsonschema
180+
instance_data = instance.json()
181+
jsonschema.validate(instance_data, schema)
182+
return True
183+
except ImportError:
184+
raise ImportError("jsonschema package required for validation")
185+
except jsonschema.ValidationError:
186+
return False
187+
188+
type_safe__to__json = Type_Safe__To__Json() # Singleton instance

0 commit comments

Comments
 (0)