tachyon-api 0.9.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.
- tachyon_api/__init__.py +59 -0
- tachyon_api/app.py +699 -0
- tachyon_api/background.py +72 -0
- tachyon_api/cache.py +270 -0
- tachyon_api/cli/__init__.py +9 -0
- tachyon_api/cli/__main__.py +8 -0
- tachyon_api/cli/commands/__init__.py +5 -0
- tachyon_api/cli/commands/generate.py +190 -0
- tachyon_api/cli/commands/lint.py +186 -0
- tachyon_api/cli/commands/new.py +82 -0
- tachyon_api/cli/commands/openapi.py +128 -0
- tachyon_api/cli/main.py +69 -0
- tachyon_api/cli/templates/__init__.py +8 -0
- tachyon_api/cli/templates/project.py +194 -0
- tachyon_api/cli/templates/service.py +330 -0
- tachyon_api/core/__init__.py +12 -0
- tachyon_api/core/lifecycle.py +106 -0
- tachyon_api/core/websocket.py +92 -0
- tachyon_api/di.py +86 -0
- tachyon_api/exceptions.py +39 -0
- tachyon_api/files.py +14 -0
- tachyon_api/middlewares/__init__.py +4 -0
- tachyon_api/middlewares/core.py +40 -0
- tachyon_api/middlewares/cors.py +159 -0
- tachyon_api/middlewares/logger.py +123 -0
- tachyon_api/models.py +73 -0
- tachyon_api/openapi.py +419 -0
- tachyon_api/params.py +268 -0
- tachyon_api/processing/__init__.py +14 -0
- tachyon_api/processing/dependencies.py +172 -0
- tachyon_api/processing/parameters.py +484 -0
- tachyon_api/processing/response_processor.py +93 -0
- tachyon_api/responses.py +92 -0
- tachyon_api/router.py +161 -0
- tachyon_api/security.py +295 -0
- tachyon_api/testing.py +110 -0
- tachyon_api/utils/__init__.py +15 -0
- tachyon_api/utils/type_converter.py +113 -0
- tachyon_api/utils/type_utils.py +162 -0
- tachyon_api-0.9.0.dist-info/METADATA +291 -0
- tachyon_api-0.9.0.dist-info/RECORD +44 -0
- tachyon_api-0.9.0.dist-info/WHEEL +4 -0
- tachyon_api-0.9.0.dist-info/entry_points.txt +3 -0
- tachyon_api-0.9.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dependency resolution for Tachyon applications.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Type-based dependency injection (@injectable)
|
|
6
|
+
- Callable dependency injection (Depends(callable))
|
|
7
|
+
- Nested dependencies
|
|
8
|
+
- Dependency caching (singleton and per-request)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import inspect
|
|
13
|
+
from typing import Any, Callable, Dict, Type
|
|
14
|
+
|
|
15
|
+
from starlette.requests import Request
|
|
16
|
+
|
|
17
|
+
from ..di import Depends, _registry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DependencyResolver:
|
|
21
|
+
"""
|
|
22
|
+
Resolves dependencies for endpoint functions.
|
|
23
|
+
|
|
24
|
+
This class encapsulates the logic for:
|
|
25
|
+
- Resolving @injectable classes
|
|
26
|
+
- Resolving Depends(callable) functions
|
|
27
|
+
- Handling nested dependencies
|
|
28
|
+
- Caching instances
|
|
29
|
+
- Supporting dependency overrides for testing
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, app_instance):
|
|
33
|
+
"""
|
|
34
|
+
Initialize dependency resolver.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
app_instance: The Tachyon app instance
|
|
38
|
+
"""
|
|
39
|
+
self.app = app_instance
|
|
40
|
+
|
|
41
|
+
def resolve_dependency(self, cls: Type) -> Any:
|
|
42
|
+
"""
|
|
43
|
+
Resolve a dependency and its sub-dependencies recursively.
|
|
44
|
+
|
|
45
|
+
This method implements dependency injection with singleton pattern,
|
|
46
|
+
automatically resolving constructor dependencies and caching instances.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
cls: The class type to resolve and instantiate
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
An instance of the requested class with all dependencies resolved
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
TypeError: If the class cannot be instantiated or is not marked as injectable
|
|
56
|
+
|
|
57
|
+
Note:
|
|
58
|
+
- Uses singleton pattern - instances are cached and reused
|
|
59
|
+
- Supports both @injectable decorated classes and simple classes
|
|
60
|
+
- Recursively resolves constructor dependencies
|
|
61
|
+
- Checks dependency_overrides for test mocking
|
|
62
|
+
"""
|
|
63
|
+
# Check for dependency override (for testing)
|
|
64
|
+
if cls in self.app.dependency_overrides:
|
|
65
|
+
override = self.app.dependency_overrides[cls]
|
|
66
|
+
# If override is callable, call it to get the instance
|
|
67
|
+
if callable(override) and not isinstance(override, type):
|
|
68
|
+
return override()
|
|
69
|
+
# If it's a class, instantiate it
|
|
70
|
+
elif isinstance(override, type):
|
|
71
|
+
return override()
|
|
72
|
+
# Otherwise return as-is
|
|
73
|
+
return override
|
|
74
|
+
|
|
75
|
+
# Return cached instance if available (singleton pattern)
|
|
76
|
+
if cls in self.app._instances_cache:
|
|
77
|
+
return self.app._instances_cache[cls]
|
|
78
|
+
|
|
79
|
+
# For non-injectable classes, try to create without arguments
|
|
80
|
+
if cls not in _registry:
|
|
81
|
+
try:
|
|
82
|
+
# Works for classes without __init__ or with no-arg __init__
|
|
83
|
+
return cls()
|
|
84
|
+
except TypeError:
|
|
85
|
+
raise TypeError(
|
|
86
|
+
f"Cannot resolve dependency '{cls.__name__}'. "
|
|
87
|
+
f"Did you forget to mark it with @injectable?"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# For injectable classes, resolve constructor dependencies
|
|
91
|
+
sig = inspect.signature(cls)
|
|
92
|
+
dependencies = {}
|
|
93
|
+
|
|
94
|
+
# Recursively resolve each constructor parameter
|
|
95
|
+
for param in sig.parameters.values():
|
|
96
|
+
if param.name != "self":
|
|
97
|
+
dependencies[param.name] = self.resolve_dependency(param.annotation)
|
|
98
|
+
|
|
99
|
+
# Create instance with resolved dependencies and cache it
|
|
100
|
+
instance = cls(**dependencies)
|
|
101
|
+
self.app._instances_cache[cls] = instance
|
|
102
|
+
return instance
|
|
103
|
+
|
|
104
|
+
async def resolve_callable_dependency(
|
|
105
|
+
self, dependency: Callable, cache: Dict, request: Request
|
|
106
|
+
) -> Any:
|
|
107
|
+
"""
|
|
108
|
+
Resolve a callable dependency (function, lambda, or class).
|
|
109
|
+
|
|
110
|
+
This method calls the dependency function to get its value, supporting
|
|
111
|
+
both sync and async functions. It also handles nested dependencies
|
|
112
|
+
if the callable has parameters with Depends() or Request annotations.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
dependency: The callable to invoke
|
|
116
|
+
cache: Per-request cache to avoid calling the same dependency twice
|
|
117
|
+
request: The current request object for injection
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The result of calling the dependency function
|
|
121
|
+
|
|
122
|
+
Note:
|
|
123
|
+
- Results are cached per-request to avoid duplicate calls
|
|
124
|
+
- Supports async callables (coroutines)
|
|
125
|
+
- Supports nested Depends() in callable parameters
|
|
126
|
+
- Automatically injects Request when parameter is annotated with Request
|
|
127
|
+
"""
|
|
128
|
+
# Check for dependency override (for testing)
|
|
129
|
+
if dependency in self.app.dependency_overrides:
|
|
130
|
+
override = self.app.dependency_overrides[dependency]
|
|
131
|
+
# If override is callable, call it
|
|
132
|
+
if callable(override):
|
|
133
|
+
result = override()
|
|
134
|
+
if asyncio.iscoroutine(result):
|
|
135
|
+
result = await result
|
|
136
|
+
return result
|
|
137
|
+
return override
|
|
138
|
+
|
|
139
|
+
# Check cache first (same callable = same result per request)
|
|
140
|
+
if dependency in cache:
|
|
141
|
+
return cache[dependency]
|
|
142
|
+
|
|
143
|
+
# Check if the dependency has its own dependencies (nested)
|
|
144
|
+
sig = inspect.signature(dependency)
|
|
145
|
+
nested_kwargs = {}
|
|
146
|
+
|
|
147
|
+
for param in sig.parameters.values():
|
|
148
|
+
# Inject Request object if parameter is annotated with Request
|
|
149
|
+
if param.annotation is Request:
|
|
150
|
+
nested_kwargs[param.name] = request
|
|
151
|
+
elif isinstance(param.default, Depends):
|
|
152
|
+
if param.default.dependency is not None:
|
|
153
|
+
# Nested callable dependency
|
|
154
|
+
nested_kwargs[param.name] = await self.resolve_callable_dependency(
|
|
155
|
+
param.default.dependency, cache, request
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
# Nested type-based dependency
|
|
159
|
+
nested_kwargs[param.name] = self.resolve_dependency(
|
|
160
|
+
param.annotation
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Call the dependency (sync or async)
|
|
164
|
+
# Note: asyncio.iscoroutinefunction doesn't work for async __call__ methods,
|
|
165
|
+
# so we check if the result is a coroutine
|
|
166
|
+
result = dependency(**nested_kwargs)
|
|
167
|
+
if asyncio.iscoroutine(result):
|
|
168
|
+
result = await result
|
|
169
|
+
|
|
170
|
+
# Cache the result for this request
|
|
171
|
+
cache[dependency] = result
|
|
172
|
+
return result
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parameter processing for Tachyon applications.
|
|
3
|
+
|
|
4
|
+
Handles extraction and validation of:
|
|
5
|
+
- Path parameters
|
|
6
|
+
- Query parameters
|
|
7
|
+
- Body parameters
|
|
8
|
+
- Header parameters
|
|
9
|
+
- Cookie parameters
|
|
10
|
+
- Form parameters
|
|
11
|
+
- File uploads
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
import msgspec
|
|
16
|
+
import typing
|
|
17
|
+
from typing import Dict, Any, Optional
|
|
18
|
+
|
|
19
|
+
from starlette.requests import Request
|
|
20
|
+
from starlette.responses import JSONResponse
|
|
21
|
+
|
|
22
|
+
from ..params import Body, Query, Path, Header, Cookie, Form, File
|
|
23
|
+
from ..models import Struct
|
|
24
|
+
from ..responses import validation_error_response
|
|
25
|
+
from ..utils import TypeConverter, TypeUtils
|
|
26
|
+
from ..background import BackgroundTasks
|
|
27
|
+
from ..di import Depends, _registry
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ParameterProcessor:
|
|
31
|
+
"""
|
|
32
|
+
Processes and extracts parameters from HTTP requests.
|
|
33
|
+
|
|
34
|
+
This class encapsulates all the complex logic for:
|
|
35
|
+
- Detecting parameter types
|
|
36
|
+
- Extracting values from request
|
|
37
|
+
- Type conversion and validation
|
|
38
|
+
- Handling optional/required parameters
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, app_instance):
|
|
42
|
+
"""
|
|
43
|
+
Initialize parameter processor.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
app_instance: The Tachyon app instance (for dependency resolution)
|
|
47
|
+
"""
|
|
48
|
+
self.app = app_instance
|
|
49
|
+
|
|
50
|
+
async def process_parameters(
|
|
51
|
+
self,
|
|
52
|
+
endpoint_func,
|
|
53
|
+
request: Request,
|
|
54
|
+
dependency_cache: Dict,
|
|
55
|
+
) -> tuple[Dict[str, Any], Optional[JSONResponse], Optional[Any]]:
|
|
56
|
+
"""
|
|
57
|
+
Process all parameters for an endpoint function.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
endpoint_func: The endpoint function to analyze
|
|
61
|
+
request: The incoming HTTP request
|
|
62
|
+
dependency_cache: Cache for callable dependencies
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (kwargs_to_inject, error_response, background_tasks)
|
|
66
|
+
- kwargs_to_inject: Dictionary of parameter name -> value
|
|
67
|
+
- error_response: JSONResponse if validation error occurred, None otherwise
|
|
68
|
+
- background_tasks: BackgroundTasks instance if requested, None otherwise
|
|
69
|
+
"""
|
|
70
|
+
kwargs_to_inject = {}
|
|
71
|
+
sig = inspect.signature(endpoint_func)
|
|
72
|
+
query_params = request.query_params
|
|
73
|
+
path_params = request.path_params
|
|
74
|
+
_raw_body = None
|
|
75
|
+
_form_data = None # Lazy-loaded form data for Form/File params
|
|
76
|
+
_background_tasks = None
|
|
77
|
+
|
|
78
|
+
# Process each parameter in the endpoint function signature
|
|
79
|
+
for param in sig.parameters.values():
|
|
80
|
+
# Check for Request object injection
|
|
81
|
+
if param.annotation is Request:
|
|
82
|
+
kwargs_to_inject[param.name] = request
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Check for BackgroundTasks injection
|
|
86
|
+
if param.annotation is BackgroundTasks:
|
|
87
|
+
if _background_tasks is None:
|
|
88
|
+
_background_tasks = BackgroundTasks()
|
|
89
|
+
kwargs_to_inject[param.name] = _background_tasks
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Determine if this parameter is a dependency
|
|
93
|
+
is_explicit_dependency = isinstance(param.default, Depends)
|
|
94
|
+
is_implicit_dependency = (
|
|
95
|
+
param.default is inspect.Parameter.empty
|
|
96
|
+
and param.annotation in _registry
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Process dependencies (explicit and implicit)
|
|
100
|
+
if is_explicit_dependency or is_implicit_dependency:
|
|
101
|
+
if (
|
|
102
|
+
is_explicit_dependency
|
|
103
|
+
and param.default.dependency is not None
|
|
104
|
+
):
|
|
105
|
+
# Depends(callable) - call the factory function
|
|
106
|
+
resolved = await self.app._dependency_resolver.resolve_callable_dependency(
|
|
107
|
+
param.default.dependency, dependency_cache, request
|
|
108
|
+
)
|
|
109
|
+
kwargs_to_inject[param.name] = resolved
|
|
110
|
+
else:
|
|
111
|
+
# Depends() or implicit - resolve by type annotation
|
|
112
|
+
target_class = param.annotation
|
|
113
|
+
kwargs_to_inject[param.name] = self.app._dependency_resolver.resolve_dependency(
|
|
114
|
+
target_class
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Process Body parameters (JSON request body)
|
|
118
|
+
elif isinstance(param.default, Body):
|
|
119
|
+
result = await self._process_body_param(
|
|
120
|
+
param, request, kwargs_to_inject
|
|
121
|
+
)
|
|
122
|
+
if result is not None: # error response
|
|
123
|
+
return kwargs_to_inject, result, _background_tasks
|
|
124
|
+
# Check if _raw_body needs updating (for subsequent body params)
|
|
125
|
+
if _raw_body is None:
|
|
126
|
+
_raw_body = True # Mark as loaded
|
|
127
|
+
|
|
128
|
+
# Process Query parameters
|
|
129
|
+
elif isinstance(param.default, Query):
|
|
130
|
+
error_response = self._process_query_param(
|
|
131
|
+
param, query_params, kwargs_to_inject
|
|
132
|
+
)
|
|
133
|
+
if error_response:
|
|
134
|
+
return kwargs_to_inject, error_response, _background_tasks
|
|
135
|
+
|
|
136
|
+
# Process Header parameters
|
|
137
|
+
elif isinstance(param.default, Header):
|
|
138
|
+
error_response = self._process_header_param(
|
|
139
|
+
param, request, kwargs_to_inject
|
|
140
|
+
)
|
|
141
|
+
if error_response:
|
|
142
|
+
return kwargs_to_inject, error_response, _background_tasks
|
|
143
|
+
|
|
144
|
+
# Process Cookie parameters
|
|
145
|
+
elif isinstance(param.default, Cookie):
|
|
146
|
+
error_response = self._process_cookie_param(
|
|
147
|
+
param, request, kwargs_to_inject
|
|
148
|
+
)
|
|
149
|
+
if error_response:
|
|
150
|
+
return kwargs_to_inject, error_response, _background_tasks
|
|
151
|
+
|
|
152
|
+
# Process Form parameters
|
|
153
|
+
elif isinstance(param.default, Form):
|
|
154
|
+
if _form_data is None:
|
|
155
|
+
_form_data = await request.form()
|
|
156
|
+
error_response = self._process_form_param(
|
|
157
|
+
param, _form_data, kwargs_to_inject
|
|
158
|
+
)
|
|
159
|
+
if error_response:
|
|
160
|
+
return kwargs_to_inject, error_response, _background_tasks
|
|
161
|
+
|
|
162
|
+
# Process File parameters
|
|
163
|
+
elif isinstance(param.default, File):
|
|
164
|
+
if _form_data is None:
|
|
165
|
+
_form_data = await request.form()
|
|
166
|
+
error_response = self._process_file_param(
|
|
167
|
+
param, _form_data, kwargs_to_inject
|
|
168
|
+
)
|
|
169
|
+
if error_response:
|
|
170
|
+
return kwargs_to_inject, error_response, _background_tasks
|
|
171
|
+
|
|
172
|
+
# Process explicit Path parameters
|
|
173
|
+
elif isinstance(param.default, Path):
|
|
174
|
+
error_response = self._process_path_param(
|
|
175
|
+
param, path_params, kwargs_to_inject
|
|
176
|
+
)
|
|
177
|
+
if error_response:
|
|
178
|
+
return kwargs_to_inject, error_response, _background_tasks
|
|
179
|
+
|
|
180
|
+
# Process implicit Path parameters
|
|
181
|
+
elif (
|
|
182
|
+
param.default is inspect.Parameter.empty
|
|
183
|
+
and param.name in path_params
|
|
184
|
+
and not is_explicit_dependency
|
|
185
|
+
and not is_implicit_dependency
|
|
186
|
+
):
|
|
187
|
+
error_response = self._process_implicit_path_param(
|
|
188
|
+
param, path_params, kwargs_to_inject
|
|
189
|
+
)
|
|
190
|
+
if error_response:
|
|
191
|
+
return kwargs_to_inject, error_response, _background_tasks
|
|
192
|
+
|
|
193
|
+
return kwargs_to_inject, None, _background_tasks
|
|
194
|
+
|
|
195
|
+
async def _process_body_param(
|
|
196
|
+
self,
|
|
197
|
+
param,
|
|
198
|
+
request: Request,
|
|
199
|
+
kwargs_to_inject: Dict,
|
|
200
|
+
) -> Optional[JSONResponse]:
|
|
201
|
+
"""Process Body parameter."""
|
|
202
|
+
model_class = param.annotation
|
|
203
|
+
if not issubclass(model_class, Struct):
|
|
204
|
+
raise TypeError(
|
|
205
|
+
"Body type must be an instance of Tachyon_api.models.Struct"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
decoder = msgspec.json.Decoder(model_class)
|
|
209
|
+
try:
|
|
210
|
+
raw_body = await request.body()
|
|
211
|
+
validated_data = decoder.decode(raw_body)
|
|
212
|
+
kwargs_to_inject[param.name] = validated_data
|
|
213
|
+
return None
|
|
214
|
+
except msgspec.ValidationError as e:
|
|
215
|
+
# Attempt to build field errors map
|
|
216
|
+
field_errors = None
|
|
217
|
+
try:
|
|
218
|
+
path = getattr(e, "path", None)
|
|
219
|
+
if path:
|
|
220
|
+
field_name = None
|
|
221
|
+
for p in reversed(path):
|
|
222
|
+
if isinstance(p, str):
|
|
223
|
+
field_name = p
|
|
224
|
+
break
|
|
225
|
+
if field_name:
|
|
226
|
+
field_errors = {field_name: [str(e)]}
|
|
227
|
+
except Exception:
|
|
228
|
+
field_errors = None
|
|
229
|
+
return validation_error_response(str(e), errors=field_errors)
|
|
230
|
+
|
|
231
|
+
def _process_query_param(
|
|
232
|
+
self,
|
|
233
|
+
param,
|
|
234
|
+
query_params,
|
|
235
|
+
kwargs_to_inject: Dict,
|
|
236
|
+
) -> Optional[JSONResponse]:
|
|
237
|
+
"""Process Query parameter."""
|
|
238
|
+
query_info = param.default
|
|
239
|
+
param_name = param.name
|
|
240
|
+
ann = param.annotation
|
|
241
|
+
origin = typing.get_origin(ann)
|
|
242
|
+
args = typing.get_args(ann)
|
|
243
|
+
|
|
244
|
+
# List[T] handling
|
|
245
|
+
if origin in (list, typing.List):
|
|
246
|
+
item_type = args[0] if args else str
|
|
247
|
+
values = []
|
|
248
|
+
# collect repeated params
|
|
249
|
+
if hasattr(query_params, "getlist"):
|
|
250
|
+
values = query_params.getlist(param_name)
|
|
251
|
+
# if not repeated, check for CSV in single value
|
|
252
|
+
if not values and param_name in query_params:
|
|
253
|
+
raw = query_params[param_name]
|
|
254
|
+
values = raw.split(",") if "," in raw else [raw]
|
|
255
|
+
# flatten CSV in any element
|
|
256
|
+
flat_values = []
|
|
257
|
+
for v in values:
|
|
258
|
+
if isinstance(v, str) and "," in v:
|
|
259
|
+
flat_values.extend(v.split(","))
|
|
260
|
+
else:
|
|
261
|
+
flat_values.append(v)
|
|
262
|
+
values = flat_values
|
|
263
|
+
if not values:
|
|
264
|
+
if query_info.default is not ...:
|
|
265
|
+
kwargs_to_inject[param_name] = query_info.default
|
|
266
|
+
return None
|
|
267
|
+
return validation_error_response(
|
|
268
|
+
f"Missing required query parameter: {param_name}"
|
|
269
|
+
)
|
|
270
|
+
# Unwrap Optional for item type
|
|
271
|
+
base_item_type, item_is_opt = TypeUtils.unwrap_optional(item_type)
|
|
272
|
+
converted_list = []
|
|
273
|
+
for v in values:
|
|
274
|
+
if item_is_opt and (v == "" or v.lower() == "null"):
|
|
275
|
+
converted_list.append(None)
|
|
276
|
+
continue
|
|
277
|
+
converted_value = TypeConverter.convert_value(
|
|
278
|
+
v, base_item_type, param_name, is_path_param=False
|
|
279
|
+
)
|
|
280
|
+
if isinstance(converted_value, JSONResponse):
|
|
281
|
+
return converted_value
|
|
282
|
+
converted_list.append(converted_value)
|
|
283
|
+
kwargs_to_inject[param_name] = converted_list
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
# Optional[T] handling for single value
|
|
287
|
+
base_type, _is_opt = TypeUtils.unwrap_optional(ann)
|
|
288
|
+
|
|
289
|
+
if param_name in query_params:
|
|
290
|
+
value_str = query_params[param_name]
|
|
291
|
+
converted_value = TypeConverter.convert_value(
|
|
292
|
+
value_str, base_type, param_name, is_path_param=False
|
|
293
|
+
)
|
|
294
|
+
if isinstance(converted_value, JSONResponse):
|
|
295
|
+
return converted_value
|
|
296
|
+
kwargs_to_inject[param_name] = converted_value
|
|
297
|
+
elif query_info.default is not ...:
|
|
298
|
+
kwargs_to_inject[param.name] = query_info.default
|
|
299
|
+
else:
|
|
300
|
+
return validation_error_response(
|
|
301
|
+
f"Missing required query parameter: {param_name}"
|
|
302
|
+
)
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def _process_header_param(
|
|
306
|
+
self,
|
|
307
|
+
param,
|
|
308
|
+
request: Request,
|
|
309
|
+
kwargs_to_inject: Dict,
|
|
310
|
+
) -> Optional[JSONResponse]:
|
|
311
|
+
"""Process Header parameter."""
|
|
312
|
+
header_info = param.default
|
|
313
|
+
# Use alias if provided, otherwise convert param name
|
|
314
|
+
if header_info.alias:
|
|
315
|
+
header_name = header_info.alias.lower()
|
|
316
|
+
else:
|
|
317
|
+
header_name = param.name.replace("_", "-").lower()
|
|
318
|
+
|
|
319
|
+
# Get header value (case-insensitive)
|
|
320
|
+
header_value = request.headers.get(header_name)
|
|
321
|
+
|
|
322
|
+
if header_value is not None:
|
|
323
|
+
kwargs_to_inject[param.name] = header_value
|
|
324
|
+
elif header_info.default is not ...:
|
|
325
|
+
kwargs_to_inject[param.name] = header_info.default
|
|
326
|
+
else:
|
|
327
|
+
return validation_error_response(
|
|
328
|
+
f"Missing required header: {header_name}"
|
|
329
|
+
)
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
def _process_cookie_param(
|
|
333
|
+
self,
|
|
334
|
+
param,
|
|
335
|
+
request: Request,
|
|
336
|
+
kwargs_to_inject: Dict,
|
|
337
|
+
) -> Optional[JSONResponse]:
|
|
338
|
+
"""Process Cookie parameter."""
|
|
339
|
+
cookie_info = param.default
|
|
340
|
+
cookie_name = cookie_info.alias or param.name
|
|
341
|
+
|
|
342
|
+
cookie_value = request.cookies.get(cookie_name)
|
|
343
|
+
|
|
344
|
+
if cookie_value is not None:
|
|
345
|
+
kwargs_to_inject[param.name] = cookie_value
|
|
346
|
+
elif cookie_info.default is not ...:
|
|
347
|
+
kwargs_to_inject[param.name] = cookie_info.default
|
|
348
|
+
else:
|
|
349
|
+
return validation_error_response(
|
|
350
|
+
f"Missing required cookie: {cookie_name}"
|
|
351
|
+
)
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
def _process_form_param(
|
|
355
|
+
self,
|
|
356
|
+
param,
|
|
357
|
+
form_data,
|
|
358
|
+
kwargs_to_inject: Dict,
|
|
359
|
+
) -> Optional[JSONResponse]:
|
|
360
|
+
"""Process Form parameter."""
|
|
361
|
+
form_info = param.default
|
|
362
|
+
field_name = form_info.alias or param.name
|
|
363
|
+
|
|
364
|
+
if field_name in form_data:
|
|
365
|
+
kwargs_to_inject[param.name] = form_data[field_name]
|
|
366
|
+
elif form_info.default is not ...:
|
|
367
|
+
kwargs_to_inject[param.name] = form_info.default
|
|
368
|
+
else:
|
|
369
|
+
return validation_error_response(
|
|
370
|
+
f"Missing required form field: {field_name}"
|
|
371
|
+
)
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
def _process_file_param(
|
|
375
|
+
self,
|
|
376
|
+
param,
|
|
377
|
+
form_data,
|
|
378
|
+
kwargs_to_inject: Dict,
|
|
379
|
+
) -> Optional[JSONResponse]:
|
|
380
|
+
"""Process File parameter."""
|
|
381
|
+
file_info = param.default
|
|
382
|
+
field_name = param.name
|
|
383
|
+
|
|
384
|
+
if field_name in form_data:
|
|
385
|
+
uploaded_file = form_data[field_name]
|
|
386
|
+
# Check if it's actually a file (UploadFile)
|
|
387
|
+
if hasattr(uploaded_file, "filename"):
|
|
388
|
+
kwargs_to_inject[param.name] = uploaded_file
|
|
389
|
+
elif file_info.default is not ...:
|
|
390
|
+
kwargs_to_inject[param.name] = file_info.default
|
|
391
|
+
else:
|
|
392
|
+
return validation_error_response(
|
|
393
|
+
f"Invalid file upload for: {field_name}"
|
|
394
|
+
)
|
|
395
|
+
elif file_info.default is not ...:
|
|
396
|
+
kwargs_to_inject[param.name] = file_info.default
|
|
397
|
+
else:
|
|
398
|
+
return validation_error_response(
|
|
399
|
+
f"Missing required file: {field_name}"
|
|
400
|
+
)
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
def _process_path_param(
|
|
404
|
+
self,
|
|
405
|
+
param,
|
|
406
|
+
path_params,
|
|
407
|
+
kwargs_to_inject: Dict,
|
|
408
|
+
) -> Optional[JSONResponse]:
|
|
409
|
+
"""Process explicit Path parameter."""
|
|
410
|
+
param_name = param.name
|
|
411
|
+
if param_name not in path_params:
|
|
412
|
+
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
|
413
|
+
|
|
414
|
+
value_str = path_params[param_name]
|
|
415
|
+
ann = param.annotation
|
|
416
|
+
origin = typing.get_origin(ann)
|
|
417
|
+
args = typing.get_args(ann)
|
|
418
|
+
|
|
419
|
+
# List[T] handling
|
|
420
|
+
if origin in (list, typing.List):
|
|
421
|
+
item_type = args[0] if args else str
|
|
422
|
+
parts = value_str.split(",") if value_str else []
|
|
423
|
+
base_item_type, item_is_opt = TypeUtils.unwrap_optional(item_type)
|
|
424
|
+
converted_list = []
|
|
425
|
+
for v in parts:
|
|
426
|
+
if item_is_opt and (v == "" or v.lower() == "null"):
|
|
427
|
+
converted_list.append(None)
|
|
428
|
+
continue
|
|
429
|
+
converted_value = TypeConverter.convert_value(
|
|
430
|
+
v, base_item_type, param_name, is_path_param=True
|
|
431
|
+
)
|
|
432
|
+
if isinstance(converted_value, JSONResponse):
|
|
433
|
+
return converted_value
|
|
434
|
+
converted_list.append(converted_value)
|
|
435
|
+
kwargs_to_inject[param_name] = converted_list
|
|
436
|
+
else:
|
|
437
|
+
converted_value = TypeConverter.convert_value(
|
|
438
|
+
value_str, ann, param_name, is_path_param=True
|
|
439
|
+
)
|
|
440
|
+
if isinstance(converted_value, JSONResponse):
|
|
441
|
+
return converted_value
|
|
442
|
+
kwargs_to_inject[param_name] = converted_value
|
|
443
|
+
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
def _process_implicit_path_param(
|
|
447
|
+
self,
|
|
448
|
+
param,
|
|
449
|
+
path_params,
|
|
450
|
+
kwargs_to_inject: Dict,
|
|
451
|
+
) -> Optional[JSONResponse]:
|
|
452
|
+
"""Process implicit Path parameter (URL path variables without Path())."""
|
|
453
|
+
param_name = param.name
|
|
454
|
+
value_str = path_params[param_name]
|
|
455
|
+
ann = param.annotation
|
|
456
|
+
origin = typing.get_origin(ann)
|
|
457
|
+
args = typing.get_args(ann)
|
|
458
|
+
|
|
459
|
+
# List[T] handling
|
|
460
|
+
if origin in (list, typing.List):
|
|
461
|
+
item_type = args[0] if args else str
|
|
462
|
+
parts = value_str.split(",") if value_str else []
|
|
463
|
+
base_item_type, item_is_opt = TypeUtils.unwrap_optional(item_type)
|
|
464
|
+
converted_list = []
|
|
465
|
+
for v in parts:
|
|
466
|
+
if item_is_opt and (v == "" or v.lower() == "null"):
|
|
467
|
+
converted_list.append(None)
|
|
468
|
+
continue
|
|
469
|
+
converted_value = TypeConverter.convert_value(
|
|
470
|
+
v, base_item_type, param_name, is_path_param=True
|
|
471
|
+
)
|
|
472
|
+
if isinstance(converted_value, JSONResponse):
|
|
473
|
+
return converted_value
|
|
474
|
+
converted_list.append(converted_value)
|
|
475
|
+
kwargs_to_inject[param_name] = converted_list
|
|
476
|
+
else:
|
|
477
|
+
converted_value = TypeConverter.convert_value(
|
|
478
|
+
value_str, ann, param_name, is_path_param=True
|
|
479
|
+
)
|
|
480
|
+
if isinstance(converted_value, JSONResponse):
|
|
481
|
+
return converted_value
|
|
482
|
+
kwargs_to_inject[param_name] = converted_value
|
|
483
|
+
|
|
484
|
+
return None
|