tachyon-api 0.5.10__py3-none-any.whl → 0.6.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.
Potentially problematic release.
This version of tachyon-api might be problematic. Click here for more details.
- tachyon_api/__init__.py +6 -6
- tachyon_api/core/__init__.py +7 -0
- tachyon_api/core/app.py +355 -0
- tachyon_api/dependencies/__init__.py +8 -0
- tachyon_api/dependencies/resolver.py +78 -0
- tachyon_api/features/__init__.py +30 -0
- tachyon_api/middlewares/__init__.py +13 -1
- tachyon_api/middlewares/manager.py +70 -0
- tachyon_api/openapi/__init__.py +27 -0
- tachyon_api/openapi/builder.py +315 -0
- tachyon_api/{openapi.py → openapi/schema.py} +1 -1
- tachyon_api/processing/__init__.py +8 -0
- tachyon_api/processing/parameters.py +308 -0
- tachyon_api/processing/responses.py +144 -0
- tachyon_api/routing/__init__.py +8 -0
- tachyon_api/{router.py → routing/router.py} +1 -1
- tachyon_api/routing/routes.py +243 -0
- tachyon_api/schemas/__init__.py +17 -0
- tachyon_api/utils/type_converter.py +1 -1
- {tachyon_api-0.5.10.dist-info → tachyon_api-0.6.0.dist-info}/METADATA +8 -5
- tachyon_api-0.6.0.dist-info/RECORD +33 -0
- tachyon_api/app.py +0 -820
- tachyon_api-0.5.10.dist-info/RECORD +0 -20
- /tachyon_api/{di.py → dependencies/injection.py} +0 -0
- /tachyon_api/{cache.py → features/cache.py} +0 -0
- /tachyon_api/{models.py → schemas/models.py} +0 -0
- /tachyon_api/{params.py → schemas/parameters.py} +0 -0
- /tachyon_api/{responses.py → schemas/responses.py} +0 -0
- {tachyon_api-0.5.10.dist-info → tachyon_api-0.6.0.dist-info}/LICENSE +0 -0
- {tachyon_api-0.5.10.dist-info → tachyon_api-0.6.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI Documentation Builder for Tachyon API
|
|
3
|
+
|
|
4
|
+
This module handles the generation and management of OpenAPI documentation
|
|
5
|
+
for the Tachyon framework, including schema generation, parameter processing,
|
|
6
|
+
and documentation endpoint setup.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from typing import Any, Dict, Type, Union, Callable
|
|
11
|
+
from starlette.responses import HTMLResponse
|
|
12
|
+
|
|
13
|
+
from ..schemas.models import Struct
|
|
14
|
+
from ..schemas.parameters import Body, Query, Path
|
|
15
|
+
from .schema import (
|
|
16
|
+
OpenAPIGenerator,
|
|
17
|
+
OpenAPIConfig,
|
|
18
|
+
build_components_for_struct,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OpenAPIBuilder:
|
|
23
|
+
"""
|
|
24
|
+
Handles OpenAPI documentation generation and management.
|
|
25
|
+
|
|
26
|
+
This class centralizes all OpenAPI-related functionality, including
|
|
27
|
+
schema generation, parameter processing, and documentation endpoint setup.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self, openapi_config: OpenAPIConfig, openapi_generator: OpenAPIGenerator
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the OpenAPI builder.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
openapi_config: OpenAPI configuration
|
|
38
|
+
openapi_generator: OpenAPI generator instance
|
|
39
|
+
"""
|
|
40
|
+
self.openapi_config = openapi_config
|
|
41
|
+
self.openapi_generator = openapi_generator
|
|
42
|
+
|
|
43
|
+
def generate_openapi_for_route(
|
|
44
|
+
self, path: str, method: str, endpoint_func: Callable, **kwargs
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Generate OpenAPI documentation for a specific route.
|
|
48
|
+
|
|
49
|
+
This method analyzes the endpoint function signature and generates appropriate
|
|
50
|
+
OpenAPI schema entries for parameters, request body, and responses.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
path: URL path pattern
|
|
54
|
+
method: HTTP method
|
|
55
|
+
endpoint_func: The endpoint function
|
|
56
|
+
**kwargs: Additional route metadata (summary, description, tags, etc.)
|
|
57
|
+
"""
|
|
58
|
+
sig = inspect.signature(endpoint_func)
|
|
59
|
+
|
|
60
|
+
# Ensure common error schemas exist in components
|
|
61
|
+
self.openapi_generator.add_schema(
|
|
62
|
+
"ValidationErrorResponse",
|
|
63
|
+
{
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"success": {"type": "boolean"},
|
|
67
|
+
"error": {"type": "string"},
|
|
68
|
+
"code": {"type": "string"},
|
|
69
|
+
"errors": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"additionalProperties": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": {"type": "string"},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
"required": ["success", "error", "code"],
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
self.openapi_generator.add_schema(
|
|
81
|
+
"ResponseValidationError",
|
|
82
|
+
{
|
|
83
|
+
"type": "object",
|
|
84
|
+
"properties": {
|
|
85
|
+
"success": {"type": "boolean"},
|
|
86
|
+
"error": {"type": "string"},
|
|
87
|
+
"detail": {"type": "string"},
|
|
88
|
+
"code": {"type": "string"},
|
|
89
|
+
},
|
|
90
|
+
"required": ["success", "error", "code"],
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Build the OpenAPI operation object
|
|
95
|
+
operation = {
|
|
96
|
+
"summary": kwargs.get(
|
|
97
|
+
"summary", self._generate_summary_from_function(endpoint_func)
|
|
98
|
+
),
|
|
99
|
+
"description": kwargs.get("description", endpoint_func.__doc__ or ""),
|
|
100
|
+
"responses": {
|
|
101
|
+
"200": {
|
|
102
|
+
"description": "Successful Response",
|
|
103
|
+
"content": {"application/json": {"schema": {"type": "object"}}},
|
|
104
|
+
},
|
|
105
|
+
"422": {
|
|
106
|
+
"description": "Validation Error",
|
|
107
|
+
"content": {
|
|
108
|
+
"application/json": {
|
|
109
|
+
"schema": {
|
|
110
|
+
"$ref": "#/components/schemas/ValidationErrorResponse"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"500": {
|
|
116
|
+
"description": "Response Validation Error",
|
|
117
|
+
"content": {
|
|
118
|
+
"application/json": {
|
|
119
|
+
"schema": {
|
|
120
|
+
"$ref": "#/components/schemas/ResponseValidationError"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# If a response_model is provided and is a Struct, use it for the 200 response schema
|
|
129
|
+
response_model = kwargs.get("response_model")
|
|
130
|
+
if response_model is not None and issubclass(response_model, Struct):
|
|
131
|
+
comps = build_components_for_struct(response_model)
|
|
132
|
+
for name, schema in comps.items():
|
|
133
|
+
self.openapi_generator.add_schema(name, schema)
|
|
134
|
+
operation["responses"]["200"]["content"]["application/json"]["schema"] = {
|
|
135
|
+
"$ref": f"#/components/schemas/{response_model.__name__}"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Add tags if provided
|
|
139
|
+
if "tags" in kwargs:
|
|
140
|
+
operation["tags"] = kwargs["tags"]
|
|
141
|
+
|
|
142
|
+
# Process parameters from function signature
|
|
143
|
+
parameters = []
|
|
144
|
+
request_body_schema = None
|
|
145
|
+
|
|
146
|
+
for param in sig.parameters.values():
|
|
147
|
+
# Skip dependency parameters
|
|
148
|
+
if isinstance(
|
|
149
|
+
param.default, (Body.__class__, Query.__class__, Path.__class__)
|
|
150
|
+
) or (
|
|
151
|
+
param.default is inspect.Parameter.empty
|
|
152
|
+
and param.annotation.__name__ in ["Depends"]
|
|
153
|
+
):
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# Process query parameters
|
|
157
|
+
elif isinstance(param.default, Query):
|
|
158
|
+
parameters.append(
|
|
159
|
+
{
|
|
160
|
+
"name": param.name,
|
|
161
|
+
"in": "query",
|
|
162
|
+
"required": param.default.default is ...,
|
|
163
|
+
"schema": self._build_param_openapi_schema(param.annotation),
|
|
164
|
+
"description": getattr(param.default, "description", ""),
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Process path parameters
|
|
169
|
+
elif isinstance(param.default, Path) or self._is_path_parameter(
|
|
170
|
+
param.name, path
|
|
171
|
+
):
|
|
172
|
+
parameters.append(
|
|
173
|
+
{
|
|
174
|
+
"name": param.name,
|
|
175
|
+
"in": "path",
|
|
176
|
+
"required": True,
|
|
177
|
+
"schema": self._build_param_openapi_schema(param.annotation),
|
|
178
|
+
"description": getattr(param.default, "description", "")
|
|
179
|
+
if isinstance(param.default, Path)
|
|
180
|
+
else "",
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Process body parameters
|
|
185
|
+
elif isinstance(param.default, Body):
|
|
186
|
+
model_class = param.annotation
|
|
187
|
+
if issubclass(model_class, Struct):
|
|
188
|
+
comps = build_components_for_struct(model_class)
|
|
189
|
+
for name, schema in comps.items():
|
|
190
|
+
self.openapi_generator.add_schema(name, schema)
|
|
191
|
+
|
|
192
|
+
request_body_schema = {
|
|
193
|
+
"content": {
|
|
194
|
+
"application/json": {
|
|
195
|
+
"schema": {
|
|
196
|
+
"$ref": f"#/components/schemas/{model_class.__name__}"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"required": True,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Add parameters to operation if any exist
|
|
204
|
+
if parameters:
|
|
205
|
+
operation["parameters"] = parameters
|
|
206
|
+
|
|
207
|
+
if request_body_schema:
|
|
208
|
+
operation["requestBody"] = request_body_schema
|
|
209
|
+
|
|
210
|
+
self.openapi_generator.add_path(path, method, operation)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _generate_summary_from_function(func: Callable) -> str:
|
|
214
|
+
"""Generate a human-readable summary from function name."""
|
|
215
|
+
return func.__name__.replace("_", " ").title()
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _is_path_parameter(param_name: str, path: str) -> bool:
|
|
219
|
+
"""Check if a parameter name corresponds to a path parameter in the URL."""
|
|
220
|
+
return f"{{{param_name}}}" in path
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _get_openapi_type(python_type: Type) -> str:
|
|
224
|
+
"""Convert Python type to OpenAPI schema type."""
|
|
225
|
+
type_map: Dict[Type, str] = {
|
|
226
|
+
int: "integer",
|
|
227
|
+
str: "string",
|
|
228
|
+
bool: "boolean",
|
|
229
|
+
float: "number",
|
|
230
|
+
}
|
|
231
|
+
return type_map.get(python_type, "string")
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _build_param_openapi_schema(python_type: Type) -> Dict[str, Any]:
|
|
235
|
+
"""Build OpenAPI schema for parameter types, supporting Optional[T] and List[T]."""
|
|
236
|
+
import typing
|
|
237
|
+
|
|
238
|
+
origin = typing.get_origin(python_type)
|
|
239
|
+
args = typing.get_args(python_type)
|
|
240
|
+
nullable = False
|
|
241
|
+
# Optional[T]
|
|
242
|
+
if origin is Union and args:
|
|
243
|
+
non_none = [a for a in args if a is not type(None)] # noqa: E721
|
|
244
|
+
if len(non_none) == 1:
|
|
245
|
+
python_type = non_none[0]
|
|
246
|
+
nullable = True
|
|
247
|
+
# List[T] (and List[Optional[T]])
|
|
248
|
+
origin = typing.get_origin(python_type)
|
|
249
|
+
args = typing.get_args(python_type)
|
|
250
|
+
if origin in (list, typing.List):
|
|
251
|
+
item_type = args[0] if args else str
|
|
252
|
+
# Unwrap Optional in items for List[Optional[T]]
|
|
253
|
+
item_origin = typing.get_origin(item_type)
|
|
254
|
+
item_args = typing.get_args(item_type)
|
|
255
|
+
item_nullable = False
|
|
256
|
+
if item_origin is Union and item_args:
|
|
257
|
+
item_non_none = [a for a in item_args if a is not type(None)] # noqa: E721
|
|
258
|
+
if len(item_non_none) == 1:
|
|
259
|
+
item_type = item_non_none[0]
|
|
260
|
+
item_nullable = True
|
|
261
|
+
schema = {
|
|
262
|
+
"type": "array",
|
|
263
|
+
"items": {"type": OpenAPIBuilder._get_openapi_type(item_type)},
|
|
264
|
+
}
|
|
265
|
+
if item_nullable:
|
|
266
|
+
schema["items"]["nullable"] = True
|
|
267
|
+
else:
|
|
268
|
+
schema = {"type": OpenAPIBuilder._get_openapi_type(python_type)}
|
|
269
|
+
if nullable:
|
|
270
|
+
schema["nullable"] = True
|
|
271
|
+
return schema
|
|
272
|
+
|
|
273
|
+
def setup_docs(self, app):
|
|
274
|
+
"""
|
|
275
|
+
Setup OpenAPI documentation endpoints.
|
|
276
|
+
|
|
277
|
+
This method registers the routes for serving OpenAPI JSON schema,
|
|
278
|
+
Swagger UI, and ReDoc documentation interfaces.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
app: The Tachyon application instance
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
# OpenAPI JSON schema endpoint
|
|
285
|
+
@app.get(self.openapi_config.openapi_url, include_in_schema=False)
|
|
286
|
+
def get_openapi_schema():
|
|
287
|
+
"""Serve the OpenAPI JSON schema."""
|
|
288
|
+
return self.openapi_generator.get_openapi_schema()
|
|
289
|
+
|
|
290
|
+
# Scalar API Reference documentation endpoint (default for /docs)
|
|
291
|
+
@app.get(self.openapi_config.docs_url, include_in_schema=False)
|
|
292
|
+
def get_scalar_docs():
|
|
293
|
+
"""Serve the Scalar API Reference documentation interface."""
|
|
294
|
+
html = self.openapi_generator.get_scalar_html(
|
|
295
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
296
|
+
)
|
|
297
|
+
return HTMLResponse(html)
|
|
298
|
+
|
|
299
|
+
# Swagger UI documentation endpoint (legacy support)
|
|
300
|
+
@app.get("/swagger", include_in_schema=False)
|
|
301
|
+
def get_swagger_ui():
|
|
302
|
+
"""Serve the Swagger UI documentation interface."""
|
|
303
|
+
html = self.openapi_generator.get_swagger_ui_html(
|
|
304
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
305
|
+
)
|
|
306
|
+
return HTMLResponse(html)
|
|
307
|
+
|
|
308
|
+
# ReDoc documentation endpoint
|
|
309
|
+
@app.get(self.openapi_config.redoc_url, include_in_schema=False)
|
|
310
|
+
def get_redoc():
|
|
311
|
+
"""Serve the ReDoc documentation interface."""
|
|
312
|
+
html = self.openapi_generator.get_redoc_html(
|
|
313
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
314
|
+
)
|
|
315
|
+
return HTMLResponse(html)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parameter Processing System for Tachyon API
|
|
3
|
+
|
|
4
|
+
This module handles the processing and validation of different parameter types
|
|
5
|
+
(Body, Query, Path) for endpoint functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import inspect
|
|
9
|
+
import typing
|
|
10
|
+
from typing import Any, Union
|
|
11
|
+
from starlette.responses import JSONResponse
|
|
12
|
+
|
|
13
|
+
import msgspec
|
|
14
|
+
|
|
15
|
+
from ..schemas.models import Struct
|
|
16
|
+
from ..schemas.parameters import Body, Query, Path
|
|
17
|
+
from ..schemas.responses import validation_error_response
|
|
18
|
+
from ..utils.type_converter import TypeConverter
|
|
19
|
+
from ..utils.type_utils import TypeUtils
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _NotProcessed:
|
|
23
|
+
"""Sentinel value to indicate that a parameter was not processed by ParameterProcessor."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ParameterProcessor:
|
|
29
|
+
"""
|
|
30
|
+
Handles processing and validation of endpoint parameters.
|
|
31
|
+
|
|
32
|
+
This class processes Body, Query, and Path parameters, performing type conversion,
|
|
33
|
+
validation, and error handling for each parameter type.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
async def process_body_parameter(
|
|
38
|
+
param, model_class, _raw_body, request
|
|
39
|
+
) -> Union[Any, JSONResponse]:
|
|
40
|
+
"""
|
|
41
|
+
Process a Body parameter from the request.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
param: The parameter object from function signature
|
|
45
|
+
model_class: The expected model class (must be a Struct)
|
|
46
|
+
_raw_body: Cached raw body data
|
|
47
|
+
request: The Starlette request object
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Validated body data or JSONResponse with validation error
|
|
51
|
+
"""
|
|
52
|
+
if not issubclass(model_class, Struct):
|
|
53
|
+
raise TypeError(
|
|
54
|
+
"Body type must be an instance of Tachyon_api.models.Struct"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
decoder = msgspec.json.Decoder(model_class)
|
|
58
|
+
try:
|
|
59
|
+
if _raw_body is None:
|
|
60
|
+
_raw_body = await request.body()
|
|
61
|
+
validated_data = decoder.decode(_raw_body)
|
|
62
|
+
return validated_data
|
|
63
|
+
except msgspec.ValidationError as e:
|
|
64
|
+
# Attempt to build field errors map using e.path
|
|
65
|
+
field_errors = None
|
|
66
|
+
try:
|
|
67
|
+
path = getattr(e, "path", None)
|
|
68
|
+
if path:
|
|
69
|
+
# Choose last string-ish path element as field name
|
|
70
|
+
field_name = None
|
|
71
|
+
for p in reversed(path):
|
|
72
|
+
if isinstance(p, str):
|
|
73
|
+
field_name = p
|
|
74
|
+
break
|
|
75
|
+
if field_name:
|
|
76
|
+
field_errors = {field_name: [str(e)]}
|
|
77
|
+
except Exception:
|
|
78
|
+
field_errors = None
|
|
79
|
+
return validation_error_response(str(e), errors=field_errors)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def process_query_parameter(param, query_params) -> Union[Any, JSONResponse, None]:
|
|
83
|
+
"""
|
|
84
|
+
Process a Query parameter from the request.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
param: The parameter object from function signature
|
|
88
|
+
query_params: The query parameters from the request
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Converted parameter value, None (for missing optional), or JSONResponse with error
|
|
92
|
+
"""
|
|
93
|
+
query_info = param.default
|
|
94
|
+
param_name = param.name
|
|
95
|
+
|
|
96
|
+
# Determine typing for advanced cases
|
|
97
|
+
ann = param.annotation
|
|
98
|
+
origin = typing.get_origin(ann)
|
|
99
|
+
args = typing.get_args(ann)
|
|
100
|
+
|
|
101
|
+
# List[T] handling
|
|
102
|
+
if origin in (list, typing.List):
|
|
103
|
+
item_type = args[0] if args else str
|
|
104
|
+
values = []
|
|
105
|
+
# collect repeated params
|
|
106
|
+
if hasattr(query_params, "getlist"):
|
|
107
|
+
values = query_params.getlist(param_name)
|
|
108
|
+
# if not repeated, check for CSV in single value
|
|
109
|
+
if not values and param_name in query_params:
|
|
110
|
+
raw = query_params[param_name]
|
|
111
|
+
values = raw.split(",") if "," in raw else [raw]
|
|
112
|
+
# flatten CSV in any element
|
|
113
|
+
flat_values = []
|
|
114
|
+
for v in values:
|
|
115
|
+
if isinstance(v, str) and "," in v:
|
|
116
|
+
flat_values.extend(v.split(","))
|
|
117
|
+
else:
|
|
118
|
+
flat_values.append(v)
|
|
119
|
+
values = flat_values
|
|
120
|
+
if not values:
|
|
121
|
+
if query_info.default is not ...:
|
|
122
|
+
return query_info.default
|
|
123
|
+
return validation_error_response(
|
|
124
|
+
f"Missing required query parameter: {param_name}"
|
|
125
|
+
)
|
|
126
|
+
# Unwrap Optional for item type
|
|
127
|
+
base_item_type, item_is_opt = TypeUtils.unwrap_optional(item_type)
|
|
128
|
+
converted_list = []
|
|
129
|
+
for v in values:
|
|
130
|
+
if item_is_opt and (v == "" or v.lower() == "null"):
|
|
131
|
+
converted_list.append(None)
|
|
132
|
+
continue
|
|
133
|
+
converted_value = TypeConverter.convert_value(
|
|
134
|
+
v, base_item_type, param_name, is_path_param=False
|
|
135
|
+
)
|
|
136
|
+
if isinstance(converted_value, JSONResponse):
|
|
137
|
+
return converted_value
|
|
138
|
+
converted_list.append(converted_value)
|
|
139
|
+
return converted_list
|
|
140
|
+
|
|
141
|
+
# Optional[T] handling for single value
|
|
142
|
+
base_type, _is_opt = TypeUtils.unwrap_optional(ann)
|
|
143
|
+
|
|
144
|
+
if param_name in query_params:
|
|
145
|
+
value_str = query_params[param_name]
|
|
146
|
+
converted_value = TypeConverter.convert_value(
|
|
147
|
+
value_str, base_type, param_name, is_path_param=False
|
|
148
|
+
)
|
|
149
|
+
if isinstance(converted_value, JSONResponse):
|
|
150
|
+
return converted_value
|
|
151
|
+
return converted_value
|
|
152
|
+
|
|
153
|
+
elif query_info.default is not ...:
|
|
154
|
+
return query_info.default
|
|
155
|
+
else:
|
|
156
|
+
return validation_error_response(
|
|
157
|
+
f"Missing required query parameter: {param_name}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def process_explicit_path_parameter(param, path_params) -> Union[Any, JSONResponse]:
|
|
162
|
+
"""
|
|
163
|
+
Process an explicit Path parameter (with Path() annotation).
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
param: The parameter object from function signature
|
|
167
|
+
path_params: The path parameters from the request
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Converted parameter value or JSONResponse with error
|
|
171
|
+
"""
|
|
172
|
+
param_name = param.name
|
|
173
|
+
if param_name in path_params:
|
|
174
|
+
value_str = path_params[param_name]
|
|
175
|
+
# Support List[T] in path params via CSV
|
|
176
|
+
ann = param.annotation
|
|
177
|
+
origin = typing.get_origin(ann)
|
|
178
|
+
args = typing.get_args(ann)
|
|
179
|
+
if origin in (list, typing.List):
|
|
180
|
+
item_type = args[0] if args else str
|
|
181
|
+
parts = value_str.split(",") if value_str else []
|
|
182
|
+
# Unwrap Optional for item type
|
|
183
|
+
base_item_type, item_is_opt = TypeUtils.unwrap_optional(item_type)
|
|
184
|
+
converted_list = []
|
|
185
|
+
for v in parts:
|
|
186
|
+
if item_is_opt and (v == "" or v.lower() == "null"):
|
|
187
|
+
converted_list.append(None)
|
|
188
|
+
continue
|
|
189
|
+
converted_value = TypeConverter.convert_value(
|
|
190
|
+
v, base_item_type, param_name, is_path_param=True
|
|
191
|
+
)
|
|
192
|
+
if isinstance(converted_value, JSONResponse):
|
|
193
|
+
return converted_value
|
|
194
|
+
converted_list.append(converted_value)
|
|
195
|
+
return converted_list
|
|
196
|
+
else:
|
|
197
|
+
converted_value = TypeConverter.convert_value(
|
|
198
|
+
value_str, ann, param_name, is_path_param=True
|
|
199
|
+
)
|
|
200
|
+
# Return 404 if conversion failed
|
|
201
|
+
if isinstance(converted_value, JSONResponse):
|
|
202
|
+
return converted_value
|
|
203
|
+
return converted_value
|
|
204
|
+
else:
|
|
205
|
+
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def process_implicit_path_parameter(
|
|
209
|
+
param, path_params
|
|
210
|
+
) -> Union[Any, JSONResponse, None]:
|
|
211
|
+
"""
|
|
212
|
+
Process an implicit Path parameter (URL path variable without Path()).
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
param: The parameter object from function signature
|
|
216
|
+
path_params: The path parameters from the request
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Converted parameter value, None (not processed), or JSONResponse with error
|
|
220
|
+
"""
|
|
221
|
+
param_name = param.name
|
|
222
|
+
value_str = path_params[param_name]
|
|
223
|
+
# Support List[T] via CSV
|
|
224
|
+
ann = param.annotation
|
|
225
|
+
origin = typing.get_origin(ann)
|
|
226
|
+
args = typing.get_args(ann)
|
|
227
|
+
if origin in (list, typing.List):
|
|
228
|
+
item_type = args[0] if args else str
|
|
229
|
+
parts = value_str.split(",") if value_str else []
|
|
230
|
+
# Unwrap Optional for item type
|
|
231
|
+
base_item_type, item_is_opt = TypeUtils.unwrap_optional(item_type)
|
|
232
|
+
converted_list = []
|
|
233
|
+
for v in parts:
|
|
234
|
+
if item_is_opt and (v == "" or v.lower() == "null"):
|
|
235
|
+
converted_list.append(None)
|
|
236
|
+
continue
|
|
237
|
+
converted_value = TypeConverter.convert_value(
|
|
238
|
+
v, base_item_type, param_name, is_path_param=True
|
|
239
|
+
)
|
|
240
|
+
if isinstance(converted_value, JSONResponse):
|
|
241
|
+
return converted_value
|
|
242
|
+
converted_list.append(converted_value)
|
|
243
|
+
return converted_list
|
|
244
|
+
else:
|
|
245
|
+
converted_value = TypeConverter.convert_value(
|
|
246
|
+
value_str, ann, param_name, is_path_param=True
|
|
247
|
+
)
|
|
248
|
+
# Return 404 if conversion failed
|
|
249
|
+
if isinstance(converted_value, JSONResponse):
|
|
250
|
+
return converted_value
|
|
251
|
+
return converted_value
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
async def process_parameter(
|
|
255
|
+
cls,
|
|
256
|
+
param,
|
|
257
|
+
request,
|
|
258
|
+
path_params,
|
|
259
|
+
query_params,
|
|
260
|
+
_raw_body,
|
|
261
|
+
is_explicit_dependency,
|
|
262
|
+
is_implicit_dependency,
|
|
263
|
+
) -> Union[Any, JSONResponse, None]:
|
|
264
|
+
"""
|
|
265
|
+
Process a single parameter based on its type and annotations.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
param: The parameter object from function signature
|
|
269
|
+
request: The Starlette request object
|
|
270
|
+
path_params: Path parameters from the request
|
|
271
|
+
query_params: Query parameters from the request
|
|
272
|
+
_raw_body: Cached raw body data
|
|
273
|
+
is_explicit_dependency: Whether this is an explicit dependency
|
|
274
|
+
is_implicit_dependency: Whether this is an implicit dependency
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Parameter value, JSONResponse (error), or None (not processed)
|
|
278
|
+
"""
|
|
279
|
+
# Process Body parameters (JSON request body)
|
|
280
|
+
if isinstance(param.default, Body):
|
|
281
|
+
model_class = param.annotation
|
|
282
|
+
result = await cls.process_body_parameter(
|
|
283
|
+
param, model_class, _raw_body, request
|
|
284
|
+
)
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
# Process Query parameters (URL query string)
|
|
288
|
+
elif isinstance(param.default, Query):
|
|
289
|
+
result = cls.process_query_parameter(param, query_params)
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
# Process explicit Path parameters (with Path() annotation)
|
|
293
|
+
elif isinstance(param.default, Path):
|
|
294
|
+
result = cls.process_explicit_path_parameter(param, path_params)
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
# Process implicit Path parameters (URL path variables without Path())
|
|
298
|
+
elif (
|
|
299
|
+
param.default is inspect.Parameter.empty
|
|
300
|
+
and param.name in path_params
|
|
301
|
+
and not is_explicit_dependency
|
|
302
|
+
and not is_implicit_dependency
|
|
303
|
+
):
|
|
304
|
+
result = cls.process_implicit_path_parameter(param, path_params)
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
# Parameter not processed by this processor
|
|
308
|
+
return _NotProcessed()
|