django-bolt 0.1.0__cp310-abi3-win_amd64.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 django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.pyd +0 -0
- django_bolt/admin/__init__.py +25 -0
- django_bolt/admin/admin_detection.py +179 -0
- django_bolt/admin/asgi_bridge.py +267 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +155 -0
- django_bolt/admin/static_routes.py +111 -0
- django_bolt/api.py +1011 -0
- django_bolt/apps.py +7 -0
- django_bolt/async_collector.py +228 -0
- django_bolt/auth/README.md +464 -0
- django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
- django_bolt/auth/__init__.py +84 -0
- django_bolt/auth/backends.py +236 -0
- django_bolt/auth/guards.py +224 -0
- django_bolt/auth/jwt_utils.py +212 -0
- django_bolt/auth/revocation.py +286 -0
- django_bolt/auth/token.py +335 -0
- django_bolt/binding.py +363 -0
- django_bolt/bootstrap.py +77 -0
- django_bolt/cli.py +133 -0
- django_bolt/compression.py +104 -0
- django_bolt/decorators.py +159 -0
- django_bolt/dependencies.py +128 -0
- django_bolt/error_handlers.py +305 -0
- django_bolt/exceptions.py +294 -0
- django_bolt/health.py +129 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +357 -0
- django_bolt/logging/middleware.py +296 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +427 -0
- django_bolt/middleware/__init__.py +32 -0
- django_bolt/middleware/compiler.py +131 -0
- django_bolt/middleware/middleware.py +247 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +439 -0
- django_bolt/openapi/routes.py +152 -0
- django_bolt/openapi/schema_generator.py +581 -0
- django_bolt/openapi/spec/__init__.py +68 -0
- django_bolt/openapi/spec/base.py +74 -0
- django_bolt/openapi/spec/callback.py +24 -0
- django_bolt/openapi/spec/components.py +72 -0
- django_bolt/openapi/spec/contact.py +21 -0
- django_bolt/openapi/spec/discriminator.py +25 -0
- django_bolt/openapi/spec/encoding.py +67 -0
- django_bolt/openapi/spec/enums.py +41 -0
- django_bolt/openapi/spec/example.py +36 -0
- django_bolt/openapi/spec/external_documentation.py +21 -0
- django_bolt/openapi/spec/header.py +132 -0
- django_bolt/openapi/spec/info.py +50 -0
- django_bolt/openapi/spec/license.py +28 -0
- django_bolt/openapi/spec/link.py +66 -0
- django_bolt/openapi/spec/media_type.py +51 -0
- django_bolt/openapi/spec/oauth_flow.py +36 -0
- django_bolt/openapi/spec/oauth_flows.py +28 -0
- django_bolt/openapi/spec/open_api.py +87 -0
- django_bolt/openapi/spec/operation.py +105 -0
- django_bolt/openapi/spec/parameter.py +147 -0
- django_bolt/openapi/spec/path_item.py +78 -0
- django_bolt/openapi/spec/paths.py +27 -0
- django_bolt/openapi/spec/reference.py +38 -0
- django_bolt/openapi/spec/request_body.py +38 -0
- django_bolt/openapi/spec/response.py +48 -0
- django_bolt/openapi/spec/responses.py +44 -0
- django_bolt/openapi/spec/schema.py +678 -0
- django_bolt/openapi/spec/security_requirement.py +28 -0
- django_bolt/openapi/spec/security_scheme.py +69 -0
- django_bolt/openapi/spec/server.py +34 -0
- django_bolt/openapi/spec/server_variable.py +32 -0
- django_bolt/openapi/spec/tag.py +32 -0
- django_bolt/openapi/spec/xml.py +44 -0
- django_bolt/pagination.py +669 -0
- django_bolt/param_functions.py +49 -0
- django_bolt/params.py +337 -0
- django_bolt/request_parsing.py +128 -0
- django_bolt/responses.py +214 -0
- django_bolt/router.py +48 -0
- django_bolt/serialization.py +193 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +10 -0
- django_bolt/testing/client.py +274 -0
- django_bolt/testing/helpers.py +93 -0
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +1 -0
- django_bolt/tests/admin_tests/conftest.py +6 -0
- django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
- django_bolt/tests/admin_tests/urls.py +9 -0
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +570 -0
- django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
- django_bolt/tests/cbv/test_class_views_features.py +1173 -0
- django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
- django_bolt/tests/conftest.py +165 -0
- django_bolt/tests/test_action_decorator.py +399 -0
- django_bolt/tests/test_auth_secret_key.py +83 -0
- django_bolt/tests/test_decorator_syntax.py +159 -0
- django_bolt/tests/test_error_handling.py +481 -0
- django_bolt/tests/test_file_response.py +192 -0
- django_bolt/tests/test_global_cors.py +172 -0
- django_bolt/tests/test_guards_auth.py +441 -0
- django_bolt/tests/test_guards_integration.py +303 -0
- django_bolt/tests/test_health.py +283 -0
- django_bolt/tests/test_integration_validation.py +400 -0
- django_bolt/tests/test_json_validation.py +536 -0
- django_bolt/tests/test_jwt_auth.py +327 -0
- django_bolt/tests/test_jwt_token.py +458 -0
- django_bolt/tests/test_logging.py +837 -0
- django_bolt/tests/test_logging_merge.py +419 -0
- django_bolt/tests/test_middleware.py +492 -0
- django_bolt/tests/test_middleware_server.py +230 -0
- django_bolt/tests/test_model_viewset.py +323 -0
- django_bolt/tests/test_models.py +24 -0
- django_bolt/tests/test_pagination.py +1258 -0
- django_bolt/tests/test_parameter_validation.py +178 -0
- django_bolt/tests/test_syntax.py +626 -0
- django_bolt/tests/test_testing_utilities.py +163 -0
- django_bolt/tests/test_testing_utilities_simple.py +123 -0
- django_bolt/tests/test_viewset_unified.py +346 -0
- django_bolt/typing.py +273 -0
- django_bolt/views.py +1110 -0
- django_bolt-0.1.0.dist-info/METADATA +629 -0
- django_bolt-0.1.0.dist-info/RECORD +128 -0
- django_bolt-0.1.0.dist-info/WHEEL +4 -0
- django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import msgspec
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, get_type_hints, get_origin, get_args, Annotated
|
|
6
|
+
|
|
7
|
+
from ..typing import is_msgspec_struct, is_optional
|
|
8
|
+
from ..params import Param
|
|
9
|
+
from .spec import (
|
|
10
|
+
OpenAPI,
|
|
11
|
+
Operation,
|
|
12
|
+
PathItem,
|
|
13
|
+
Parameter,
|
|
14
|
+
RequestBody,
|
|
15
|
+
OpenAPIResponse,
|
|
16
|
+
OpenAPIMediaType,
|
|
17
|
+
Schema,
|
|
18
|
+
Reference,
|
|
19
|
+
SecurityRequirement,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ..api import BoltAPI
|
|
24
|
+
from .config import OpenAPIConfig
|
|
25
|
+
|
|
26
|
+
__all__ = ("SchemaGenerator",)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SchemaGenerator:
|
|
30
|
+
"""Generate OpenAPI schema from BoltAPI routes."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, api: BoltAPI, config: OpenAPIConfig) -> None:
|
|
33
|
+
"""Initialize schema generator.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
api: BoltAPI instance to generate schema for.
|
|
37
|
+
config: OpenAPI configuration.
|
|
38
|
+
"""
|
|
39
|
+
self.api = api
|
|
40
|
+
self.config = config
|
|
41
|
+
self.schemas: Dict[str, Schema] = {} # Component schemas registry
|
|
42
|
+
|
|
43
|
+
def generate(self) -> OpenAPI:
|
|
44
|
+
"""Generate complete OpenAPI schema.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
OpenAPI schema object.
|
|
48
|
+
"""
|
|
49
|
+
openapi = self.config.to_openapi_schema()
|
|
50
|
+
|
|
51
|
+
# Generate path items from routes
|
|
52
|
+
paths: Dict[str, PathItem] = {}
|
|
53
|
+
for method, path, handler_id, handler in self.api._routes:
|
|
54
|
+
# Skip OpenAPI docs routes (always excluded)
|
|
55
|
+
if path.startswith(self.config.path):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Skip paths based on exclude_paths configuration
|
|
59
|
+
should_exclude = False
|
|
60
|
+
for exclude_prefix in self.config.exclude_paths:
|
|
61
|
+
if path.startswith(exclude_prefix):
|
|
62
|
+
should_exclude = True
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
if should_exclude:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
if path not in paths:
|
|
69
|
+
paths[path] = PathItem()
|
|
70
|
+
|
|
71
|
+
# Get handler metadata
|
|
72
|
+
meta = self.api._handler_meta.get(handler, {})
|
|
73
|
+
|
|
74
|
+
# Create operation
|
|
75
|
+
operation = self._create_operation(
|
|
76
|
+
handler=handler,
|
|
77
|
+
method=method,
|
|
78
|
+
path=path,
|
|
79
|
+
meta=meta,
|
|
80
|
+
handler_id=handler_id,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Add operation to path item
|
|
84
|
+
method_lower = method.lower()
|
|
85
|
+
setattr(paths[path], method_lower, operation)
|
|
86
|
+
|
|
87
|
+
openapi.paths = paths
|
|
88
|
+
|
|
89
|
+
# Add component schemas
|
|
90
|
+
if self.schemas:
|
|
91
|
+
openapi.components.schemas = self.schemas
|
|
92
|
+
|
|
93
|
+
return openapi
|
|
94
|
+
|
|
95
|
+
def _create_operation(
|
|
96
|
+
self,
|
|
97
|
+
handler: Any,
|
|
98
|
+
method: str,
|
|
99
|
+
path: str,
|
|
100
|
+
meta: Dict[str, Any],
|
|
101
|
+
handler_id: int,
|
|
102
|
+
) -> Operation:
|
|
103
|
+
"""Create OpenAPI Operation for a route handler.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
handler: Handler function.
|
|
107
|
+
method: HTTP method.
|
|
108
|
+
path: Route path.
|
|
109
|
+
meta: Handler metadata from BoltAPI.
|
|
110
|
+
handler_id: Handler ID.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Operation object.
|
|
114
|
+
"""
|
|
115
|
+
# Get description from docstring
|
|
116
|
+
description = None
|
|
117
|
+
summary = None
|
|
118
|
+
if self.config.use_handler_docstrings and handler.__doc__:
|
|
119
|
+
doc = inspect.cleandoc(handler.__doc__)
|
|
120
|
+
lines = doc.split("\n", 1)
|
|
121
|
+
summary = lines[0]
|
|
122
|
+
if len(lines) > 1:
|
|
123
|
+
description = lines[1].strip()
|
|
124
|
+
|
|
125
|
+
# Extract parameters
|
|
126
|
+
parameters = self._extract_parameters(meta, path)
|
|
127
|
+
|
|
128
|
+
# Extract request body
|
|
129
|
+
request_body = self._extract_request_body(meta)
|
|
130
|
+
|
|
131
|
+
# Extract responses (pass handler_id for auth error responses)
|
|
132
|
+
responses = self._extract_responses(meta, handler_id)
|
|
133
|
+
|
|
134
|
+
# Extract security requirements
|
|
135
|
+
security = self._extract_security(handler_id)
|
|
136
|
+
|
|
137
|
+
# Extract tags (use handler module or class name)
|
|
138
|
+
tags = self._extract_tags(handler)
|
|
139
|
+
|
|
140
|
+
operation = Operation(
|
|
141
|
+
summary=summary,
|
|
142
|
+
description=description,
|
|
143
|
+
parameters=parameters or None,
|
|
144
|
+
request_body=request_body,
|
|
145
|
+
responses=responses,
|
|
146
|
+
security=security,
|
|
147
|
+
tags=tags,
|
|
148
|
+
operation_id=f"{method.lower()}_{handler.__name__}",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return operation
|
|
152
|
+
|
|
153
|
+
def _extract_parameters(
|
|
154
|
+
self, meta: Dict[str, Any], path: str
|
|
155
|
+
) -> List[Parameter]:
|
|
156
|
+
"""Extract OpenAPI parameters from handler metadata.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
meta: Handler metadata.
|
|
160
|
+
path: Route path.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of Parameter objects.
|
|
164
|
+
"""
|
|
165
|
+
parameters: List[Parameter] = []
|
|
166
|
+
fields = meta.get("fields", [])
|
|
167
|
+
|
|
168
|
+
for field in fields:
|
|
169
|
+
source = field.get("source")
|
|
170
|
+
name = field.get("name")
|
|
171
|
+
alias = field.get("alias") or name
|
|
172
|
+
annotation = field.get("annotation")
|
|
173
|
+
default = field.get("default")
|
|
174
|
+
|
|
175
|
+
# Skip request, body, form, file, and dependency parameters
|
|
176
|
+
if source in ("request", "body", "form", "file", "dependency"):
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Map source to OpenAPI parameter location
|
|
180
|
+
param_in = {
|
|
181
|
+
"path": "path",
|
|
182
|
+
"query": "query",
|
|
183
|
+
"header": "header",
|
|
184
|
+
"cookie": "cookie",
|
|
185
|
+
}.get(source)
|
|
186
|
+
|
|
187
|
+
if not param_in:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
# Determine if required
|
|
191
|
+
required = (
|
|
192
|
+
param_in == "path" # Path params always required
|
|
193
|
+
or (default == inspect.Parameter.empty and not is_optional(annotation))
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Get schema for parameter type
|
|
197
|
+
schema = self._type_to_schema(annotation)
|
|
198
|
+
|
|
199
|
+
parameter = Parameter(
|
|
200
|
+
name=alias,
|
|
201
|
+
param_in=param_in,
|
|
202
|
+
required=required,
|
|
203
|
+
schema=schema,
|
|
204
|
+
description=f"Parameter {alias}",
|
|
205
|
+
)
|
|
206
|
+
parameters.append(parameter)
|
|
207
|
+
|
|
208
|
+
return parameters
|
|
209
|
+
|
|
210
|
+
def _extract_request_body(self, meta: Dict[str, Any]) -> Optional[RequestBody]:
|
|
211
|
+
"""Extract OpenAPI RequestBody from handler metadata.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
meta: Handler metadata.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
RequestBody object or None.
|
|
218
|
+
"""
|
|
219
|
+
body_param = meta.get("body_struct_param")
|
|
220
|
+
body_type = meta.get("body_struct_type")
|
|
221
|
+
|
|
222
|
+
if not body_param or not body_type:
|
|
223
|
+
# Check for form/file fields
|
|
224
|
+
fields = meta.get("fields", [])
|
|
225
|
+
form_fields = [f for f in fields if f.get("source") in ("form", "file")]
|
|
226
|
+
|
|
227
|
+
if form_fields:
|
|
228
|
+
# Multipart form data
|
|
229
|
+
properties = {}
|
|
230
|
+
required = []
|
|
231
|
+
for field in form_fields:
|
|
232
|
+
name = field.get("alias") or field.get("name")
|
|
233
|
+
annotation = field.get("annotation")
|
|
234
|
+
default = field.get("default")
|
|
235
|
+
source = field.get("source")
|
|
236
|
+
|
|
237
|
+
if source == "file":
|
|
238
|
+
# File upload
|
|
239
|
+
schema = Schema(type="string", format="binary")
|
|
240
|
+
else:
|
|
241
|
+
schema = self._type_to_schema(annotation)
|
|
242
|
+
|
|
243
|
+
properties[name] = schema
|
|
244
|
+
|
|
245
|
+
if default == inspect.Parameter.empty and not is_optional(annotation):
|
|
246
|
+
required.append(name)
|
|
247
|
+
|
|
248
|
+
schema = Schema(
|
|
249
|
+
type="object",
|
|
250
|
+
properties=properties,
|
|
251
|
+
required=required or None,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return RequestBody(
|
|
255
|
+
description="Form data",
|
|
256
|
+
content={
|
|
257
|
+
"multipart/form-data": OpenAPIMediaType(schema=schema),
|
|
258
|
+
"application/x-www-form-urlencoded": OpenAPIMediaType(schema=schema),
|
|
259
|
+
},
|
|
260
|
+
required=bool(required),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
# JSON request body
|
|
266
|
+
schema = self._type_to_schema(body_type, register_component=True)
|
|
267
|
+
|
|
268
|
+
return RequestBody(
|
|
269
|
+
description=f"Request body for {body_param}",
|
|
270
|
+
content={
|
|
271
|
+
"application/json": OpenAPIMediaType(schema=schema),
|
|
272
|
+
},
|
|
273
|
+
required=True,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def _extract_responses(
|
|
277
|
+
self, meta: Dict[str, Any], handler_id: int
|
|
278
|
+
) -> Dict[str, OpenAPIResponse]:
|
|
279
|
+
"""Extract OpenAPI responses from handler metadata.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
meta: Handler metadata.
|
|
283
|
+
handler_id: Handler ID for checking authentication requirements.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dictionary mapping status codes to Response objects.
|
|
287
|
+
"""
|
|
288
|
+
responses: Dict[str, OpenAPIResponse] = {}
|
|
289
|
+
|
|
290
|
+
# Get response type
|
|
291
|
+
response_type = meta.get("response_type")
|
|
292
|
+
default_status = meta.get("default_status_code", 200)
|
|
293
|
+
|
|
294
|
+
# Add successful response
|
|
295
|
+
if response_type and response_type != inspect._empty:
|
|
296
|
+
schema = self._type_to_schema(response_type, register_component=True)
|
|
297
|
+
|
|
298
|
+
responses[str(default_status)] = OpenAPIResponse(
|
|
299
|
+
description="Successful response",
|
|
300
|
+
content={
|
|
301
|
+
"application/json": OpenAPIMediaType(schema=schema),
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
# Default response
|
|
306
|
+
responses["200"] = OpenAPIResponse(
|
|
307
|
+
description="Successful response",
|
|
308
|
+
content={
|
|
309
|
+
"application/json": OpenAPIMediaType(
|
|
310
|
+
schema=Schema(type="object")
|
|
311
|
+
),
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Add common error responses if enabled in config
|
|
316
|
+
if self.config.include_error_responses:
|
|
317
|
+
# Check if request body is present (for 422 validation errors)
|
|
318
|
+
has_request_body = meta.get("body_struct_param") or any(
|
|
319
|
+
f.get("source") in ("body", "form", "file")
|
|
320
|
+
for f in meta.get("fields", [])
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if has_request_body:
|
|
324
|
+
# 422 Unprocessable Entity - validation errors
|
|
325
|
+
responses["422"] = OpenAPIResponse(
|
|
326
|
+
description="Validation Error - Request data failed validation",
|
|
327
|
+
content={
|
|
328
|
+
"application/json": OpenAPIMediaType(
|
|
329
|
+
schema=self._get_validation_error_schema()
|
|
330
|
+
),
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return responses
|
|
335
|
+
|
|
336
|
+
def _get_validation_error_schema(self) -> Schema:
|
|
337
|
+
"""Get schema for 422 validation error responses.
|
|
338
|
+
|
|
339
|
+
FastAPI-compatible format: {"detail": [array of validation errors]}
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Schema for validation errors matching FastAPI format.
|
|
343
|
+
"""
|
|
344
|
+
return Schema(
|
|
345
|
+
type="object",
|
|
346
|
+
properties={
|
|
347
|
+
"detail": Schema(
|
|
348
|
+
type="array",
|
|
349
|
+
description="List of validation errors",
|
|
350
|
+
items=Schema(
|
|
351
|
+
type="object",
|
|
352
|
+
properties={
|
|
353
|
+
"type": Schema(
|
|
354
|
+
type="string",
|
|
355
|
+
description="Error type",
|
|
356
|
+
example="validation_error",
|
|
357
|
+
),
|
|
358
|
+
"loc": Schema(
|
|
359
|
+
type="array",
|
|
360
|
+
description="Location of the error (field path)",
|
|
361
|
+
items=Schema(
|
|
362
|
+
one_of=[
|
|
363
|
+
Schema(type="string"),
|
|
364
|
+
Schema(type="integer"),
|
|
365
|
+
]
|
|
366
|
+
),
|
|
367
|
+
example=["body", "is_active"],
|
|
368
|
+
),
|
|
369
|
+
"msg": Schema(
|
|
370
|
+
type="string",
|
|
371
|
+
description="Error message",
|
|
372
|
+
example="Expected `bool`, got `int`",
|
|
373
|
+
),
|
|
374
|
+
"input": Schema(
|
|
375
|
+
description="The input value that caused the error (optional)",
|
|
376
|
+
),
|
|
377
|
+
},
|
|
378
|
+
required=["type", "loc", "msg"],
|
|
379
|
+
),
|
|
380
|
+
),
|
|
381
|
+
},
|
|
382
|
+
required=["detail"],
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def _extract_security(self, handler_id: int) -> Optional[List[SecurityReq]]:
|
|
386
|
+
"""Extract security requirements from handler middleware.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
handler_id: Handler ID.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
List of SecurityRequirement objects or None.
|
|
393
|
+
"""
|
|
394
|
+
middleware_meta = self.api._handler_middleware.get(handler_id, {})
|
|
395
|
+
auth_config = middleware_meta.get("auth")
|
|
396
|
+
|
|
397
|
+
if not auth_config:
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
# Convert auth backends to security requirements
|
|
401
|
+
security: List[SecurityReq] = []
|
|
402
|
+
for auth_backend in auth_config:
|
|
403
|
+
backend_name = auth_backend.__class__.__name__
|
|
404
|
+
|
|
405
|
+
if "JWT" in backend_name:
|
|
406
|
+
security.append({"BearerAuth": []})
|
|
407
|
+
elif "APIKey" in backend_name:
|
|
408
|
+
security.append({"ApiKeyAuth": []})
|
|
409
|
+
elif "Session" in backend_name:
|
|
410
|
+
security.append({"SessionAuth": []})
|
|
411
|
+
|
|
412
|
+
return security or None
|
|
413
|
+
|
|
414
|
+
def _extract_tags(self, handler: Any) -> Optional[List[str]]:
|
|
415
|
+
"""Extract tags for grouping operations.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
handler: Handler function.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
List of tag names or None.
|
|
422
|
+
"""
|
|
423
|
+
# Use module name as tag
|
|
424
|
+
if hasattr(handler, "__module__"):
|
|
425
|
+
module_parts = handler.__module__.split(".")
|
|
426
|
+
if len(module_parts) > 0:
|
|
427
|
+
# Use last part of module name (e.g., "users" from "myapp.api.users")
|
|
428
|
+
tag = module_parts[-1]
|
|
429
|
+
if tag == "api" and len(module_parts) > 1:
|
|
430
|
+
# If last part is "api", use the second-to-last part
|
|
431
|
+
# e.g., "users.api" -> "users"
|
|
432
|
+
tag = module_parts[-2]
|
|
433
|
+
if tag != "api": # Skip generic "api" tag
|
|
434
|
+
return [tag.capitalize()]
|
|
435
|
+
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
def _type_to_schema(
|
|
439
|
+
self, type_annotation: Any, register_component: bool = False
|
|
440
|
+
) -> Schema | Reference:
|
|
441
|
+
"""Convert Python type annotation to OpenAPI Schema.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
type_annotation: Python type annotation.
|
|
445
|
+
register_component: Whether to register complex types as components.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Schema or Reference object.
|
|
449
|
+
"""
|
|
450
|
+
# Handle None/empty
|
|
451
|
+
if type_annotation is None or type_annotation == inspect._empty:
|
|
452
|
+
return Schema(type="object")
|
|
453
|
+
|
|
454
|
+
# Handle msgspec type info objects (IntType, StrType, BoolType, etc.)
|
|
455
|
+
type_name = type(type_annotation).__name__
|
|
456
|
+
if hasattr(type_annotation, '__class__') and type_name.endswith('Type'):
|
|
457
|
+
# Map msgspec type objects to OpenAPI schemas
|
|
458
|
+
msgspec_type_map = {
|
|
459
|
+
'IntType': Schema(type="integer"),
|
|
460
|
+
'StrType': Schema(type="string"),
|
|
461
|
+
'FloatType': Schema(type="number"),
|
|
462
|
+
'BoolType': Schema(type="boolean"),
|
|
463
|
+
'BytesType': Schema(type="string", format="binary"),
|
|
464
|
+
'DateTimeType': Schema(type="string", format="date-time"),
|
|
465
|
+
'DateType': Schema(type="string", format="date"),
|
|
466
|
+
'TimeType': Schema(type="string", format="time"),
|
|
467
|
+
'UUIDType': Schema(type="string", format="uuid"),
|
|
468
|
+
}
|
|
469
|
+
if type_name in msgspec_type_map:
|
|
470
|
+
return msgspec_type_map[type_name]
|
|
471
|
+
# For list/array types from msgspec
|
|
472
|
+
if type_name == 'ListType':
|
|
473
|
+
item_type = getattr(type_annotation, 'item_type', None)
|
|
474
|
+
if item_type:
|
|
475
|
+
item_schema = self._type_to_schema(item_type, register_component=register_component)
|
|
476
|
+
return Schema(type="array", items=item_schema)
|
|
477
|
+
return Schema(type="array", items=Schema(type="object"))
|
|
478
|
+
# For dict types from msgspec
|
|
479
|
+
if type_name == 'DictType':
|
|
480
|
+
return Schema(type="object", additional_properties=True)
|
|
481
|
+
|
|
482
|
+
# Unwrap Optional
|
|
483
|
+
origin = get_origin(type_annotation)
|
|
484
|
+
args = get_args(type_annotation)
|
|
485
|
+
|
|
486
|
+
if origin is Annotated:
|
|
487
|
+
# Unwrap Annotated[T, ...]
|
|
488
|
+
type_annotation = args[0]
|
|
489
|
+
origin = get_origin(type_annotation)
|
|
490
|
+
args = get_args(type_annotation)
|
|
491
|
+
|
|
492
|
+
# Handle Optional[T] -> T
|
|
493
|
+
if is_optional(type_annotation):
|
|
494
|
+
# Get the non-None type
|
|
495
|
+
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
496
|
+
if non_none_args:
|
|
497
|
+
type_annotation = non_none_args[0]
|
|
498
|
+
origin = get_origin(type_annotation)
|
|
499
|
+
args = get_args(type_annotation)
|
|
500
|
+
|
|
501
|
+
# Handle msgspec.Struct
|
|
502
|
+
if is_msgspec_struct(type_annotation):
|
|
503
|
+
if register_component:
|
|
504
|
+
return self._struct_to_component_schema(type_annotation)
|
|
505
|
+
else:
|
|
506
|
+
return self._struct_to_schema(type_annotation)
|
|
507
|
+
|
|
508
|
+
# Handle list/List
|
|
509
|
+
if origin in (list, List):
|
|
510
|
+
item_type = args[0] if args else Any
|
|
511
|
+
item_schema = self._type_to_schema(item_type, register_component=register_component)
|
|
512
|
+
return Schema(type="array", items=item_schema)
|
|
513
|
+
|
|
514
|
+
# Handle dict/Dict
|
|
515
|
+
if origin in (dict, Dict):
|
|
516
|
+
return Schema(type="object", additional_properties=True)
|
|
517
|
+
|
|
518
|
+
# Handle primitive types
|
|
519
|
+
type_map = {
|
|
520
|
+
str: Schema(type="string"),
|
|
521
|
+
int: Schema(type="integer"),
|
|
522
|
+
float: Schema(type="number"),
|
|
523
|
+
bool: Schema(type="boolean"),
|
|
524
|
+
bytes: Schema(type="string", format="binary"),
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
for py_type, schema in type_map.items():
|
|
528
|
+
if type_annotation == py_type:
|
|
529
|
+
return schema
|
|
530
|
+
|
|
531
|
+
# Default to generic object
|
|
532
|
+
return Schema(type="object")
|
|
533
|
+
|
|
534
|
+
def _struct_to_schema(self, struct_type: type) -> Schema:
|
|
535
|
+
"""Convert msgspec.Struct to inline OpenAPI Schema.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
struct_type: msgspec.Struct type.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Schema object.
|
|
542
|
+
"""
|
|
543
|
+
struct_info = msgspec.inspect.type_info(struct_type)
|
|
544
|
+
properties = {}
|
|
545
|
+
required = []
|
|
546
|
+
|
|
547
|
+
for field in struct_info.fields:
|
|
548
|
+
field_name = field.encode_name
|
|
549
|
+
field_type = field.type
|
|
550
|
+
|
|
551
|
+
# Get schema for field type
|
|
552
|
+
field_schema = self._type_to_schema(field_type, register_component=False)
|
|
553
|
+
properties[field_name] = field_schema
|
|
554
|
+
|
|
555
|
+
# Check if required
|
|
556
|
+
if field.required and field.default == msgspec.NODEFAULT:
|
|
557
|
+
required.append(field_name)
|
|
558
|
+
|
|
559
|
+
return Schema(
|
|
560
|
+
type="object",
|
|
561
|
+
properties=properties,
|
|
562
|
+
required=required or None,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def _struct_to_component_schema(self, struct_type: type) -> Reference:
|
|
566
|
+
"""Convert msgspec.Struct to component schema and return reference.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
struct_type: msgspec.Struct type.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Reference to component schema.
|
|
573
|
+
"""
|
|
574
|
+
schema_name = struct_type.__name__
|
|
575
|
+
|
|
576
|
+
# Check if already registered
|
|
577
|
+
if schema_name not in self.schemas:
|
|
578
|
+
# Register the schema
|
|
579
|
+
self.schemas[schema_name] = self._struct_to_schema(struct_type)
|
|
580
|
+
|
|
581
|
+
return Reference(ref=f"#/components/schemas/{schema_name}")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from .base import BaseSchemaObject
|
|
2
|
+
from .callback import Callback
|
|
3
|
+
from .components import Components
|
|
4
|
+
from .contact import Contact
|
|
5
|
+
from .discriminator import Discriminator
|
|
6
|
+
from .encoding import Encoding
|
|
7
|
+
from .enums import OpenAPIFormat, OpenAPIType
|
|
8
|
+
from .example import Example
|
|
9
|
+
from .external_documentation import ExternalDocumentation
|
|
10
|
+
from .header import OpenAPIHeader
|
|
11
|
+
from .info import Info
|
|
12
|
+
from .license import License
|
|
13
|
+
from .link import Link
|
|
14
|
+
from .media_type import OpenAPIMediaType
|
|
15
|
+
from .oauth_flow import OAuthFlow
|
|
16
|
+
from .oauth_flows import OAuthFlows
|
|
17
|
+
from .open_api import OpenAPI
|
|
18
|
+
from .operation import Operation
|
|
19
|
+
from .parameter import Parameter
|
|
20
|
+
from .path_item import PathItem
|
|
21
|
+
from .paths import Paths
|
|
22
|
+
from .reference import Reference
|
|
23
|
+
from .request_body import RequestBody
|
|
24
|
+
from .response import OpenAPIResponse
|
|
25
|
+
from .responses import Responses
|
|
26
|
+
from .schema import Schema
|
|
27
|
+
from .security_requirement import SecurityRequirement
|
|
28
|
+
from .security_scheme import SecurityScheme
|
|
29
|
+
from .server import Server
|
|
30
|
+
from .server_variable import ServerVariable
|
|
31
|
+
from .tag import Tag
|
|
32
|
+
from .xml import XML
|
|
33
|
+
|
|
34
|
+
__all__ = (
|
|
35
|
+
"XML",
|
|
36
|
+
"BaseSchemaObject",
|
|
37
|
+
"Callback",
|
|
38
|
+
"Components",
|
|
39
|
+
"Contact",
|
|
40
|
+
"Discriminator",
|
|
41
|
+
"Encoding",
|
|
42
|
+
"Example",
|
|
43
|
+
"ExternalDocumentation",
|
|
44
|
+
"Info",
|
|
45
|
+
"License",
|
|
46
|
+
"Link",
|
|
47
|
+
"OAuthFlow",
|
|
48
|
+
"OAuthFlows",
|
|
49
|
+
"OpenAPI",
|
|
50
|
+
"OpenAPIFormat",
|
|
51
|
+
"OpenAPIHeader",
|
|
52
|
+
"OpenAPIMediaType",
|
|
53
|
+
"OpenAPIResponse",
|
|
54
|
+
"OpenAPIType",
|
|
55
|
+
"Operation",
|
|
56
|
+
"Parameter",
|
|
57
|
+
"PathItem",
|
|
58
|
+
"Paths",
|
|
59
|
+
"Reference",
|
|
60
|
+
"RequestBody",
|
|
61
|
+
"Responses",
|
|
62
|
+
"Schema",
|
|
63
|
+
"SecurityRequirement",
|
|
64
|
+
"SecurityScheme",
|
|
65
|
+
"Server",
|
|
66
|
+
"ServerVariable",
|
|
67
|
+
"Tag",
|
|
68
|
+
)
|