Skip to content

Commit 065f846

Browse files
committed
first implementation of add_route_with_body with support for the transparent use of Type_Safe classes on post param and return value :)
1 parent 0aa3a62 commit 065f846

3 files changed

Lines changed: 428 additions & 34 deletions

File tree

osbot_fast_api/api/Fast_API_Routes.py

Lines changed: 159 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
import functools
12
import inspect
2-
from typing import get_type_hints
3-
from fastapi import APIRouter, FastAPI
4-
from osbot_utils.type_safe.Type_Safe import Type_Safe
5-
from osbot_utils.decorators.lists.index_by import index_by
3+
from typing import get_type_hints
4+
from fastapi import APIRouter, FastAPI
5+
from osbot_utils.type_safe.Type_Safe import Type_Safe
6+
from osbot_utils.decorators.lists.index_by import index_by
7+
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
8+
from fastapi.exceptions import RequestValidationError
9+
from osbot_utils.utils.Dev import pprint
10+
from pydantic_core import ErrorDetails
11+
12+
from osbot_fast_api.utils.type_safe.BaseModel__To__Type_Safe import basemodel__to__type_safe
13+
from osbot_fast_api.utils.type_safe.Type_Safe__To__BaseModel import type_safe__to__basemodel
14+
615

716
class Fast_API_Routes(Type_Safe): # refactor to Fast_API__Routes
817
router : APIRouter
@@ -22,53 +31,170 @@ def add_route(self,function, methods):
2231
self.router.add_api_route(path=path, endpoint=function, methods=methods)
2332
return self
2433

25-
def add_route_delete(self, function):
26-
return self.add_route(function=function, methods=['DELETE'])
27-
28-
def add_route_get(self, function):
29-
return self.add_route(function=function, methods=['GET'])
30-
31-
def add_route_post(self, function): # add post with support for Type_Safe objects
32-
sig = inspect.signature(function) # Check if function has a Type_Safe parameter
34+
def add_route_with_body(self, function, methods):
35+
sig = inspect.signature(function)
3336
type_hints = get_type_hints(function)
3437

35-
type_safe_param = False
38+
# Find Type_Safe parameters and convert them to BaseModel classes
39+
type_safe_conversions = {}
40+
3641
for param_name, param in sig.parameters.items():
3742
if param_name == 'self':
3843
continue
3944
param_type = type_hints.get(param_name)
4045
if param_type and inspect.isclass(param_type):
41-
if issubclass(param_type, Type_Safe):
42-
type_safe_param = True
43-
break
44-
45-
if type_safe_param:
46-
def wrapper(data: dict):
47-
param_object = param_type.from_json(data)
48-
kwargs = { param_name: param_object }
49-
result = function(**kwargs)
46+
if issubclass(param_type, Type_Safe) and not issubclass(param_type, Type_Safe__Primitive):
47+
basemodel_class = type_safe__to__basemodel.convert_class(param_type)
48+
type_safe_conversions[param_name] = (param_type, basemodel_class)
49+
50+
if type_safe_conversions:
51+
@functools.wraps(function)
52+
def wrapper(**kwargs):
53+
converted_kwargs = {}
54+
for param_name, param_value in kwargs.items():
55+
if param_name in type_safe_conversions:
56+
type_safe_class, _ = type_safe_conversions[param_name]
57+
if isinstance(param_value, dict):
58+
converted_kwargs[param_name] = type_safe_class(**param_value)
59+
else:
60+
data = param_value.model_dump() # Get the data from BaseModel
61+
converted_kwargs[param_name] = type_safe_class(**data) # Create instance of original Type_Safe class
62+
else:
63+
converted_kwargs[param_name] = param_value
64+
65+
result = function(**converted_kwargs)
66+
5067
if isinstance(result, Type_Safe):
51-
return result.json()
68+
return type_safe__to__basemodel.convert_instance(result).model_dump()
5269
return result
5370

71+
# Build new parameters with BaseModel types
72+
new_params = []
73+
for param_name, param in sig.parameters.items():
74+
if param_name == 'self':
75+
continue
76+
if param_name in type_safe_conversions:
77+
_, basemodel_class = type_safe_conversions[param_name]
78+
new_params.append(inspect.Parameter(
79+
name=param_name,
80+
kind=param.kind,
81+
default=param.default,
82+
annotation=basemodel_class
83+
))
84+
else:
85+
new_params.append(param)
5486

55-
# todo: the code below is not working (need to add support for supporting Type_Safe return values)
56-
# Remove the return type annotation to prevent FastAPI validation
57-
# wrapper.__annotations__ = function.__annotations__.copy()
58-
# if 'return' in wrapper.__annotations__:
59-
# del wrapper.__annotations__['return'] # Remove return type so FastAPI doesn't validate
87+
# Set the new signature on the wrapper
88+
wrapper.__signature__ = inspect.Signature(parameters=new_params)
6089

90+
# Also update annotations for FastAPI
91+
wrapper.__annotations__ = {}
92+
for param_name, param_type in type_hints.items():
93+
if param_name in type_safe_conversions:
94+
_, basemodel_class = type_safe_conversions[param_name]
95+
wrapper.__annotations__[param_name] = basemodel_class
96+
else:
97+
wrapper.__annotations__[param_name] = param_type
6198

62-
#path = '/' + function.__name__.replace('_', '-')
6399
path = self.parse_function_name(function.__name__)
64-
self.router.add_api_route(path=path, endpoint=wrapper, methods=['POST'])
100+
self.router.add_api_route(path=path, endpoint=wrapper, methods=methods)
65101
return self
66102
else:
67-
# Normal route
68-
return self.add_route(function=function, methods=['POST'])
103+
return self.add_route(function=function, methods=methods)
104+
105+
def add_route_delete(self, function):
106+
#return self.add_route(function=function, methods=['DELETE'])
107+
108+
return self.add_route_with_body(function, methods=['DELETE'])
109+
110+
def add_route_get(self, function):
111+
import functools
112+
sig = inspect.signature(function)
113+
type_hints = get_type_hints(function)
114+
115+
primitive_conversions = {} # Check for Type_Safe__Primitive parameters
116+
117+
for param_name, param in sig.parameters.items():
118+
if param_name == 'self':
119+
continue
120+
param_type = type_hints.get(param_name)
121+
if param_type and inspect.isclass(param_type):
122+
if issubclass(param_type, Type_Safe__Primitive):
123+
primitive_base = param_type.__primitive_base__
124+
if primitive_base is None:
125+
for base in param_type.__mro__:
126+
if base in (str, int, float):
127+
primitive_base = base
128+
break
129+
130+
if primitive_base:
131+
primitive_conversions[param_name] = (param_type, primitive_base)
132+
133+
if primitive_conversions:
134+
# Create a wrapper that preserves the exact signature
135+
@functools.wraps(function)
136+
def wrapper(*args, **kwargs):
137+
# Convert primitive values to Type_Safe__Primitive instances
138+
converted_kwargs = {}
139+
validation_errors = []
140+
for param_name, param_value in kwargs.items():
141+
if param_name in primitive_conversions:
142+
type_safe_primitive_class, _ = primitive_conversions[param_name]
143+
try:
144+
converted_kwargs[param_name] = type_safe_primitive_class(param_value)
145+
except (ValueError, TypeError) as e:
146+
# Create validation error in FastAPI format
147+
validation_errors.append({'type': 'value_error',
148+
'loc' : ('query', param_name),
149+
'msg' : str(e),
150+
'input': param_value})
151+
else:
152+
converted_kwargs[param_name] = param_value
153+
154+
# If there were validation errors, raise them
155+
if validation_errors:
156+
raise RequestValidationError(validation_errors)
157+
158+
# Call with self if it's in args
159+
if args:
160+
result = function(*args, **converted_kwargs)
161+
else:
162+
result = function(**converted_kwargs)
163+
164+
# Convert result if needed
165+
if isinstance(result, Type_Safe__Primitive):
166+
return result.__primitive_base__(result)
167+
return result
168+
169+
# Build new parameter list with primitive types
170+
new_params = []
171+
for param_name, param in sig.parameters.items():
172+
if param_name == 'self':
173+
continue # Skip self
174+
if param_name in primitive_conversions:
175+
_, primitive_type = primitive_conversions[param_name]
176+
# Replace with primitive type parameter
177+
new_params.append(inspect.Parameter(
178+
name=param_name,
179+
kind=param.kind,
180+
default=param.default,
181+
annotation=primitive_type
182+
))
183+
else:
184+
new_params.append(param)
185+
186+
# Create new signature
187+
wrapper.__signature__ = inspect.Signature(parameters=new_params)
188+
189+
return self.add_route(function=wrapper, methods=['GET'])
190+
else:
191+
return self.add_route(function=function, methods=['GET'])
192+
193+
def add_route_post(self, function):
194+
return self.add_route_with_body(function, methods=['POST'])
69195

70196
def add_route_put(self, function):
71-
return self.add_route(function=function, methods=['PUT'])
197+
return self.add_route_with_body(function, methods=['PUT'])
72198

73199
def fast_api_utils(self):
74200
from osbot_fast_api.utils.Fast_API_Utils import Fast_API_Utils

tests/unit/utils/type_safe/test_type_safe__Fast_API_Routes__conversions.py renamed to tests/unit/utils/type_safe/test__type_safe__Fast_API_Routes__conversions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from osbot_fast_api.utils.type_safe.Type_Safe__To__Dataclass import type_safe__to__dataclass
1919

2020

21-
class test_type_safe__Fast_API_Routes__conversions(TestCase):
21+
class test__type_safe__Fast_API_Routes__conversions(TestCase):
2222

2323
def test__1__with_BaseModel__indirect_support(self): # Using Pydantic BaseModel with Type_Safe conversion
2424

0 commit comments

Comments
 (0)