django-bolt 0.1.0__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.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.

Files changed (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.abi3.so +0 -0
  3. django_bolt/admin/__init__.py +25 -0
  4. django_bolt/admin/admin_detection.py +179 -0
  5. django_bolt/admin/asgi_bridge.py +267 -0
  6. django_bolt/admin/routes.py +91 -0
  7. django_bolt/admin/static.py +155 -0
  8. django_bolt/admin/static_routes.py +111 -0
  9. django_bolt/api.py +1011 -0
  10. django_bolt/apps.py +7 -0
  11. django_bolt/async_collector.py +228 -0
  12. django_bolt/auth/README.md +464 -0
  13. django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
  14. django_bolt/auth/__init__.py +84 -0
  15. django_bolt/auth/backends.py +236 -0
  16. django_bolt/auth/guards.py +224 -0
  17. django_bolt/auth/jwt_utils.py +212 -0
  18. django_bolt/auth/revocation.py +286 -0
  19. django_bolt/auth/token.py +335 -0
  20. django_bolt/binding.py +363 -0
  21. django_bolt/bootstrap.py +77 -0
  22. django_bolt/cli.py +133 -0
  23. django_bolt/compression.py +104 -0
  24. django_bolt/decorators.py +159 -0
  25. django_bolt/dependencies.py +128 -0
  26. django_bolt/error_handlers.py +305 -0
  27. django_bolt/exceptions.py +294 -0
  28. django_bolt/health.py +129 -0
  29. django_bolt/logging/__init__.py +6 -0
  30. django_bolt/logging/config.py +357 -0
  31. django_bolt/logging/middleware.py +296 -0
  32. django_bolt/management/__init__.py +1 -0
  33. django_bolt/management/commands/__init__.py +0 -0
  34. django_bolt/management/commands/runbolt.py +427 -0
  35. django_bolt/middleware/__init__.py +32 -0
  36. django_bolt/middleware/compiler.py +131 -0
  37. django_bolt/middleware/middleware.py +247 -0
  38. django_bolt/openapi/__init__.py +23 -0
  39. django_bolt/openapi/config.py +196 -0
  40. django_bolt/openapi/plugins.py +439 -0
  41. django_bolt/openapi/routes.py +152 -0
  42. django_bolt/openapi/schema_generator.py +581 -0
  43. django_bolt/openapi/spec/__init__.py +68 -0
  44. django_bolt/openapi/spec/base.py +74 -0
  45. django_bolt/openapi/spec/callback.py +24 -0
  46. django_bolt/openapi/spec/components.py +72 -0
  47. django_bolt/openapi/spec/contact.py +21 -0
  48. django_bolt/openapi/spec/discriminator.py +25 -0
  49. django_bolt/openapi/spec/encoding.py +67 -0
  50. django_bolt/openapi/spec/enums.py +41 -0
  51. django_bolt/openapi/spec/example.py +36 -0
  52. django_bolt/openapi/spec/external_documentation.py +21 -0
  53. django_bolt/openapi/spec/header.py +132 -0
  54. django_bolt/openapi/spec/info.py +50 -0
  55. django_bolt/openapi/spec/license.py +28 -0
  56. django_bolt/openapi/spec/link.py +66 -0
  57. django_bolt/openapi/spec/media_type.py +51 -0
  58. django_bolt/openapi/spec/oauth_flow.py +36 -0
  59. django_bolt/openapi/spec/oauth_flows.py +28 -0
  60. django_bolt/openapi/spec/open_api.py +87 -0
  61. django_bolt/openapi/spec/operation.py +105 -0
  62. django_bolt/openapi/spec/parameter.py +147 -0
  63. django_bolt/openapi/spec/path_item.py +78 -0
  64. django_bolt/openapi/spec/paths.py +27 -0
  65. django_bolt/openapi/spec/reference.py +38 -0
  66. django_bolt/openapi/spec/request_body.py +38 -0
  67. django_bolt/openapi/spec/response.py +48 -0
  68. django_bolt/openapi/spec/responses.py +44 -0
  69. django_bolt/openapi/spec/schema.py +678 -0
  70. django_bolt/openapi/spec/security_requirement.py +28 -0
  71. django_bolt/openapi/spec/security_scheme.py +69 -0
  72. django_bolt/openapi/spec/server.py +34 -0
  73. django_bolt/openapi/spec/server_variable.py +32 -0
  74. django_bolt/openapi/spec/tag.py +32 -0
  75. django_bolt/openapi/spec/xml.py +44 -0
  76. django_bolt/pagination.py +669 -0
  77. django_bolt/param_functions.py +49 -0
  78. django_bolt/params.py +337 -0
  79. django_bolt/request_parsing.py +128 -0
  80. django_bolt/responses.py +214 -0
  81. django_bolt/router.py +48 -0
  82. django_bolt/serialization.py +193 -0
  83. django_bolt/status_codes.py +321 -0
  84. django_bolt/testing/__init__.py +10 -0
  85. django_bolt/testing/client.py +274 -0
  86. django_bolt/testing/helpers.py +93 -0
  87. django_bolt/tests/__init__.py +0 -0
  88. django_bolt/tests/admin_tests/__init__.py +1 -0
  89. django_bolt/tests/admin_tests/conftest.py +6 -0
  90. django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
  91. django_bolt/tests/admin_tests/urls.py +9 -0
  92. django_bolt/tests/cbv/__init__.py +0 -0
  93. django_bolt/tests/cbv/test_class_views.py +570 -0
  94. django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
  95. django_bolt/tests/cbv/test_class_views_features.py +1173 -0
  96. django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
  97. django_bolt/tests/conftest.py +165 -0
  98. django_bolt/tests/test_action_decorator.py +399 -0
  99. django_bolt/tests/test_auth_secret_key.py +83 -0
  100. django_bolt/tests/test_decorator_syntax.py +159 -0
  101. django_bolt/tests/test_error_handling.py +481 -0
  102. django_bolt/tests/test_file_response.py +192 -0
  103. django_bolt/tests/test_global_cors.py +172 -0
  104. django_bolt/tests/test_guards_auth.py +441 -0
  105. django_bolt/tests/test_guards_integration.py +303 -0
  106. django_bolt/tests/test_health.py +283 -0
  107. django_bolt/tests/test_integration_validation.py +400 -0
  108. django_bolt/tests/test_json_validation.py +536 -0
  109. django_bolt/tests/test_jwt_auth.py +327 -0
  110. django_bolt/tests/test_jwt_token.py +458 -0
  111. django_bolt/tests/test_logging.py +837 -0
  112. django_bolt/tests/test_logging_merge.py +419 -0
  113. django_bolt/tests/test_middleware.py +492 -0
  114. django_bolt/tests/test_middleware_server.py +230 -0
  115. django_bolt/tests/test_model_viewset.py +323 -0
  116. django_bolt/tests/test_models.py +24 -0
  117. django_bolt/tests/test_pagination.py +1258 -0
  118. django_bolt/tests/test_parameter_validation.py +178 -0
  119. django_bolt/tests/test_syntax.py +626 -0
  120. django_bolt/tests/test_testing_utilities.py +163 -0
  121. django_bolt/tests/test_testing_utilities_simple.py +123 -0
  122. django_bolt/tests/test_viewset_unified.py +346 -0
  123. django_bolt/typing.py +273 -0
  124. django_bolt/views.py +1110 -0
  125. django_bolt-0.1.0.dist-info/METADATA +629 -0
  126. django_bolt-0.1.0.dist-info/RECORD +128 -0
  127. django_bolt-0.1.0.dist-info/WHEEL +4 -0
  128. 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
+ )