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
django_bolt/api.py ADDED
@@ -0,0 +1,1011 @@
1
+ import inspect
2
+ import msgspec
3
+ import re
4
+ import time
5
+ from typing import Any, Callable, Dict, List, Tuple, Optional, get_origin, get_args, Annotated, get_type_hints
6
+
7
+ from .bootstrap import ensure_django_ready
8
+ from django_bolt import _core
9
+ from .responses import StreamingResponse
10
+ from .exceptions import HTTPException
11
+ from .params import Param, Depends as DependsMarker
12
+ from .typing import FieldDefinition
13
+
14
+ # Import modularized components
15
+ from .binding import (
16
+ coerce_to_response_type,
17
+ coerce_to_response_type_async,
18
+ convert_primitive,
19
+ create_extractor,
20
+ )
21
+ from .typing import is_msgspec_struct, is_optional
22
+ from .request_parsing import parse_form_data
23
+ from .dependencies import resolve_dependency
24
+ from .serialization import serialize_response
25
+ from .middleware.compiler import compile_middleware_meta
26
+
27
+ Request = Dict[str, Any]
28
+ Response = Tuple[int, List[Tuple[str, str]], bytes]
29
+
30
+ # Global registry for BoltAPI instances (used by autodiscovery)
31
+ _BOLT_API_REGISTRY = []
32
+
33
+
34
+ def _extract_path_params(path: str) -> set[str]:
35
+ """
36
+ Extract path parameter names from a route pattern.
37
+
38
+ Examples:
39
+ "/users/{user_id}" -> {"user_id"}
40
+ "/posts/{post_id}/comments/{comment_id}" -> {"post_id", "comment_id"}
41
+ """
42
+ return set(re.findall(r'\{(\w+)\}', path))
43
+
44
+
45
+ def extract_parameter_value(
46
+ param: Dict[str, Any],
47
+ request: Dict[str, Any],
48
+ params_map: Dict[str, Any],
49
+ query_map: Dict[str, Any],
50
+ headers_map: Dict[str, str],
51
+ cookies_map: Dict[str, str],
52
+ form_map: Dict[str, Any],
53
+ files_map: Dict[str, Any],
54
+ meta: Dict[str, Any],
55
+ body_obj: Any,
56
+ body_loaded: bool
57
+ ) -> Tuple[Any, Any, bool]:
58
+ """
59
+ Extract value for a handler parameter (backward compatibility function).
60
+
61
+ This function maintains backward compatibility while using the new
62
+ extractor-based system internally.
63
+ """
64
+ name = param["name"]
65
+ annotation = param["annotation"]
66
+ default = param["default"]
67
+ source = param["source"]
68
+ alias = param.get("alias")
69
+ key = alias or name
70
+
71
+ # Handle different sources
72
+ if source == "path":
73
+ if key in params_map:
74
+ return convert_primitive(str(params_map[key]), annotation), body_obj, body_loaded
75
+ raise ValueError(f"Missing required path parameter: {key}")
76
+
77
+ elif source == "query":
78
+ if key in query_map:
79
+ return convert_primitive(str(query_map[key]), annotation), body_obj, body_loaded
80
+ elif default is not inspect.Parameter.empty or is_optional(annotation):
81
+ return (None if default is inspect.Parameter.empty else default), body_obj, body_loaded
82
+ raise ValueError(f"Missing required query parameter: {key}")
83
+
84
+ elif source == "header":
85
+ lower_key = key.lower()
86
+ if lower_key in headers_map:
87
+ return convert_primitive(str(headers_map[lower_key]), annotation), body_obj, body_loaded
88
+ elif default is not inspect.Parameter.empty or is_optional(annotation):
89
+ return (None if default is inspect.Parameter.empty else default), body_obj, body_loaded
90
+ raise ValueError(f"Missing required header: {key}")
91
+
92
+ elif source == "cookie":
93
+ if key in cookies_map:
94
+ return convert_primitive(str(cookies_map[key]), annotation), body_obj, body_loaded
95
+ elif default is not inspect.Parameter.empty or is_optional(annotation):
96
+ return (None if default is inspect.Parameter.empty else default), body_obj, body_loaded
97
+ raise ValueError(f"Missing required cookie: {key}")
98
+
99
+ elif source == "form":
100
+ if key in form_map:
101
+ return convert_primitive(str(form_map[key]), annotation), body_obj, body_loaded
102
+ elif default is not inspect.Parameter.empty or is_optional(annotation):
103
+ return (None if default is inspect.Parameter.empty else default), body_obj, body_loaded
104
+ raise ValueError(f"Missing required form field: {key}")
105
+
106
+ elif source == "file":
107
+ if key in files_map:
108
+ return files_map[key], body_obj, body_loaded
109
+ elif default is not inspect.Parameter.empty or is_optional(annotation):
110
+ return (None if default is inspect.Parameter.empty else default), body_obj, body_loaded
111
+ raise ValueError(f"Missing required file: {key}")
112
+
113
+ elif source == "body":
114
+ # Handle body parameter
115
+ if meta.get("body_struct_param") == name:
116
+ if not body_loaded:
117
+ body_bytes: bytes = request["body"]
118
+ if is_msgspec_struct(meta["body_struct_type"]):
119
+ from .binding import get_msgspec_decoder
120
+ from .exceptions import RequestValidationError, parse_msgspec_decode_error
121
+ decoder = get_msgspec_decoder(meta["body_struct_type"])
122
+ try:
123
+ value = decoder.decode(body_bytes)
124
+ except msgspec.ValidationError:
125
+ # Re-raise ValidationError as-is (field validation errors handled by error_handlers.py)
126
+ # IMPORTANT: Must catch ValidationError BEFORE DecodeError since ValidationError subclasses DecodeError
127
+ raise
128
+ except msgspec.DecodeError as e:
129
+ # JSON parsing error (malformed JSON) - return 422 with error details including line/column
130
+ error_detail = parse_msgspec_decode_error(e, body_bytes)
131
+ raise RequestValidationError(
132
+ errors=[error_detail],
133
+ body=body_bytes,
134
+ ) from e
135
+ else:
136
+ from .exceptions import RequestValidationError, parse_msgspec_decode_error
137
+ try:
138
+ value = msgspec.json.decode(body_bytes, type=meta["body_struct_type"])
139
+ except msgspec.ValidationError:
140
+ # Re-raise ValidationError as-is (field validation errors handled by error_handlers.py)
141
+ # IMPORTANT: Must catch ValidationError BEFORE DecodeError since ValidationError subclasses DecodeError
142
+ raise
143
+ except msgspec.DecodeError as e:
144
+ # JSON parsing error (malformed JSON) - return 422 with error details including line/column
145
+ error_detail = parse_msgspec_decode_error(e, body_bytes)
146
+ raise RequestValidationError(
147
+ errors=[error_detail],
148
+ body=body_bytes,
149
+ ) from e
150
+ return value, value, True
151
+ else:
152
+ return body_obj, body_obj, body_loaded
153
+ else:
154
+ if default is not inspect.Parameter.empty or is_optional(annotation):
155
+ return (None if default is inspect.Parameter.empty else default), body_obj, body_loaded
156
+ raise ValueError(f"Missing required parameter: {name}")
157
+
158
+ else:
159
+ # Unknown source
160
+ if default is not inspect.Parameter.empty or is_optional(annotation):
161
+ return (None if default is inspect.Parameter.empty else default), body_obj, body_loaded
162
+ raise ValueError(f"Missing required parameter: {name}")
163
+
164
+ class BoltAPI:
165
+ def __init__(
166
+ self,
167
+ prefix: str = "",
168
+ middleware: Optional[List[Any]] = None,
169
+ middleware_config: Optional[Dict[str, Any]] = None,
170
+ enable_logging: bool = True,
171
+ logging_config: Optional[Any] = None,
172
+ compression: Optional[Any] = None,
173
+ openapi_config: Optional[Any] = None,
174
+ ) -> None:
175
+ self._routes: List[Tuple[str, str, int, Callable]] = []
176
+ self._handlers: Dict[int, Callable] = {}
177
+ self._handler_meta: Dict[Callable, Dict[str, Any]] = {}
178
+ self._handler_middleware: Dict[int, Dict[str, Any]] = {} # Middleware metadata per handler
179
+ self._next_handler_id = 0
180
+ self.prefix = prefix.rstrip("/") # Remove trailing slash
181
+
182
+ # Global middleware configuration
183
+ self.middleware = middleware or []
184
+ self.middleware_config = middleware_config or {}
185
+
186
+ # Logging configuration (opt-in, setup happens at server startup)
187
+ self.enable_logging = enable_logging
188
+ self._logging_middleware = None
189
+
190
+ if self.enable_logging:
191
+ # Create logging middleware (actual logging setup happens at server startup)
192
+ if logging_config is not None:
193
+ from .logging.middleware import LoggingMiddleware
194
+ self._logging_middleware = LoggingMiddleware(logging_config)
195
+ else:
196
+ # Use default logging configuration
197
+ from .logging.middleware import create_logging_middleware
198
+ self._logging_middleware = create_logging_middleware()
199
+
200
+ # Compression configuration
201
+ # compression=None means disabled, not providing compression arg means default enabled
202
+ if compression is False:
203
+ # Explicitly disabled
204
+ self.compression = None
205
+ elif compression is None:
206
+ # Not provided, use default
207
+ from .compression import CompressionConfig
208
+ self.compression = CompressionConfig()
209
+ else:
210
+ # Custom config provided
211
+ self.compression = compression
212
+
213
+ # OpenAPI configuration - enabled by default with sensible defaults
214
+ if openapi_config is None:
215
+ # Create default OpenAPI config
216
+ from .openapi import OpenAPIConfig, SwaggerRenderPlugin, RedocRenderPlugin, ScalarRenderPlugin, RapidocRenderPlugin, StoplightRenderPlugin, JsonRenderPlugin, YamlRenderPlugin
217
+ try:
218
+ # Try to get Django project name from settings
219
+ from django.conf import settings
220
+ title = getattr(settings, 'PROJECT_NAME', None) or getattr(settings, 'SITE_NAME', None) or "API"
221
+ except:
222
+ title = "API"
223
+
224
+ self.openapi_config = OpenAPIConfig(
225
+ title=title,
226
+ version="1.0.0",
227
+ path="/docs",
228
+ render_plugins=[
229
+ SwaggerRenderPlugin(path="/"),
230
+ RedocRenderPlugin(path="/redoc"),
231
+ ScalarRenderPlugin(path="/scalar"),
232
+ RapidocRenderPlugin(path="/rapidoc"),
233
+ StoplightRenderPlugin(path="/stoplight"),
234
+ ]
235
+ )
236
+ else:
237
+ self.openapi_config = openapi_config
238
+
239
+ self._openapi_schema: Optional[Dict[str, Any]] = None
240
+ self._openapi_routes_registered = False
241
+
242
+ # Django admin configuration (controlled by --no-admin flag)
243
+ self._admin_routes_registered = False
244
+ self._static_routes_registered = False
245
+ self._asgi_handler = None
246
+
247
+ # Register this instance globally for autodiscovery
248
+ _BOLT_API_REGISTRY.append(self)
249
+
250
+ def get(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
251
+ return self._route_decorator("GET", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
252
+
253
+ def post(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
254
+ return self._route_decorator("POST", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
255
+
256
+ def put(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
257
+ return self._route_decorator("PUT", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
258
+
259
+ def patch(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
260
+ return self._route_decorator("PATCH", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
261
+
262
+ def delete(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
263
+ return self._route_decorator("DELETE", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
264
+
265
+ def head(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
266
+ return self._route_decorator("HEAD", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
267
+
268
+ def options(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
269
+ return self._route_decorator("OPTIONS", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
270
+
271
+ def view(
272
+ self,
273
+ path: str,
274
+ *,
275
+ methods: Optional[List[str]] = None,
276
+ guards: Optional[List[Any]] = None,
277
+ auth: Optional[List[Any]] = None,
278
+ status_code: Optional[int] = None
279
+ ):
280
+ """
281
+ Register a class-based view as a decorator.
282
+
283
+ Usage:
284
+ @api.view("/users")
285
+ class UserView(APIView):
286
+ async def get(self) -> list[User]:
287
+ return User.objects.all()[:10]
288
+
289
+ This method discovers available HTTP method handlers from the view class
290
+ and registers them with the router. It supports the same parameter extraction,
291
+ dependency injection, guards, and authentication as function-based handlers.
292
+
293
+ Args:
294
+ path: URL path pattern (e.g., "/users/{user_id}")
295
+ methods: Optional list of HTTP methods to register (defaults to all implemented methods)
296
+ guards: Optional per-route guard overrides (merged with class-level guards)
297
+ auth: Optional per-route auth overrides (merged with class-level auth)
298
+ status_code: Optional per-route status code override
299
+
300
+ Returns:
301
+ Decorator function that registers the view class
302
+
303
+ Raises:
304
+ ValueError: If view class doesn't implement any requested methods
305
+ """
306
+ from .views import APIView
307
+
308
+ def decorator(view_cls: type) -> type:
309
+ # Validate that view_cls is an APIView subclass
310
+ if not issubclass(view_cls, APIView):
311
+ raise TypeError(
312
+ f"View class {view_cls.__name__} must inherit from APIView"
313
+ )
314
+
315
+ # Determine which methods to register
316
+ if methods is None:
317
+ # Auto-discover all implemented methods
318
+ available_methods = view_cls.get_allowed_methods()
319
+ if not available_methods:
320
+ raise ValueError(
321
+ f"View class {view_cls.__name__} does not implement any HTTP methods"
322
+ )
323
+ methods_to_register = [m.lower() for m in available_methods]
324
+ else:
325
+ # Validate requested methods are implemented
326
+ methods_to_register = [m.lower() for m in methods]
327
+ available_methods = {m.lower() for m in view_cls.get_allowed_methods()}
328
+ for method in methods_to_register:
329
+ if method not in available_methods:
330
+ raise ValueError(
331
+ f"View class {view_cls.__name__} does not implement method '{method}'"
332
+ )
333
+
334
+ # Register each method
335
+ for method in methods_to_register:
336
+ method_upper = method.upper()
337
+
338
+ # Create handler using as_view()
339
+ handler = view_cls.as_view(method)
340
+
341
+ # Merge guards: route-level overrides class-level
342
+ merged_guards = guards
343
+ if merged_guards is None and hasattr(handler, '__bolt_guards__'):
344
+ merged_guards = handler.__bolt_guards__
345
+
346
+ # Merge auth: route-level overrides class-level
347
+ merged_auth = auth
348
+ if merged_auth is None and hasattr(handler, '__bolt_auth__'):
349
+ merged_auth = handler.__bolt_auth__
350
+
351
+ # Merge status_code: route-level overrides class-level
352
+ merged_status_code = status_code
353
+ if merged_status_code is None and hasattr(handler, '__bolt_status_code__'):
354
+ merged_status_code = handler.__bolt_status_code__
355
+
356
+ # Register using existing route decorator
357
+ route_decorator = self._route_decorator(
358
+ method_upper,
359
+ path,
360
+ response_model=None, # Use method's return annotation
361
+ status_code=merged_status_code,
362
+ guards=merged_guards,
363
+ auth=merged_auth
364
+ )
365
+
366
+ # Apply decorator to register the handler
367
+ route_decorator(handler)
368
+
369
+ # Scan for custom action methods (methods decorated with @action)
370
+ # Note: api.view() doesn't have base path context for @action decorator
371
+ # Custom actions with @action should use api.viewset() instead
372
+ self._register_custom_actions(view_cls, base_path=None, lookup_field=None)
373
+
374
+ return view_cls
375
+
376
+ return decorator
377
+
378
+ def viewset(
379
+ self,
380
+ path: str,
381
+ *,
382
+ guards: Optional[List[Any]] = None,
383
+ auth: Optional[List[Any]] = None,
384
+ status_code: Optional[int] = None,
385
+ lookup_field: str = "pk"
386
+ ):
387
+ """
388
+ Register a ViewSet with automatic CRUD route generation as a decorator.
389
+
390
+ Usage:
391
+ @api.viewset("/users")
392
+ class UserViewSet(ViewSet):
393
+ async def list(self) -> list[User]:
394
+ return User.objects.all()[:100]
395
+
396
+ async def retrieve(self, id: int) -> User:
397
+ return await User.objects.aget(id=id)
398
+
399
+ @action(methods=["POST"], detail=True)
400
+ async def activate(self, id: int):
401
+ user = await User.objects.aget(id=id)
402
+ user.is_active = True
403
+ await user.asave()
404
+ return user
405
+
406
+ This method auto-generates routes for standard DRF-style actions:
407
+ - list: GET /path (200 OK)
408
+ - create: POST /path (201 Created)
409
+ - retrieve: GET /path/{pk} (200 OK)
410
+ - update: PUT /path/{pk} (200 OK)
411
+ - partial_update: PATCH /path/{pk} (200 OK)
412
+ - destroy: DELETE /path/{pk} (204 No Content)
413
+
414
+ Args:
415
+ path: Base URL path (e.g., "/users")
416
+ guards: Optional guards to apply to all routes
417
+ auth: Optional auth backends to apply to all routes
418
+ status_code: Optional default status code (overrides action-specific defaults)
419
+ lookup_field: Field name for object lookup (default: "pk")
420
+
421
+ Returns:
422
+ Decorator function that registers the viewset
423
+ """
424
+ from .views import ViewSet
425
+ from .status_codes import HTTP_201_CREATED, HTTP_204_NO_CONTENT
426
+
427
+ def decorator(viewset_cls: type) -> type:
428
+ # Validate that viewset_cls is a ViewSet subclass
429
+ if not issubclass(viewset_cls, ViewSet):
430
+ raise TypeError(
431
+ f"ViewSet class {viewset_cls.__name__} must inherit from ViewSet"
432
+ )
433
+
434
+ # Use lookup_field from ViewSet class if not provided
435
+ actual_lookup_field = lookup_field
436
+ if actual_lookup_field == "pk" and hasattr(viewset_cls, 'lookup_field'):
437
+ actual_lookup_field = viewset_cls.lookup_field
438
+
439
+ # Define standard action mappings with HTTP-compliant status codes
440
+ # Format: action_name: (method, path, action_override, default_status_code)
441
+ action_routes = {
442
+ # Collection routes (no pk)
443
+ 'list': ('GET', path, None, None),
444
+ 'create': ('POST', path, None, HTTP_201_CREATED),
445
+
446
+ # Detail routes (with pk)
447
+ 'retrieve': ('GET', f"{path}/{{{actual_lookup_field}}}", 'retrieve', None),
448
+ 'update': ('PUT', f"{path}/{{{actual_lookup_field}}}", 'update', None),
449
+ 'partial_update': ('PATCH', f"{path}/{{{actual_lookup_field}}}", 'partial_update', None),
450
+ 'destroy': ('DELETE', f"{path}/{{{actual_lookup_field}}}", 'destroy', HTTP_204_NO_CONTENT),
451
+ }
452
+
453
+ # Register routes for each implemented action
454
+ for action_name, (http_method, route_path, action_override, action_status_code) in action_routes.items():
455
+ # Check if the viewset implements this action
456
+ if not hasattr(viewset_cls, action_name):
457
+ continue
458
+
459
+ action_method = getattr(viewset_cls, action_name)
460
+ if not inspect.iscoroutinefunction(action_method):
461
+ continue
462
+
463
+ # Use action name (e.g., "list") not HTTP method name (e.g., "get")
464
+ handler = viewset_cls.as_view(http_method.lower(), action=action_override or action_name)
465
+
466
+ # Merge guards and auth
467
+ merged_guards = guards
468
+ if merged_guards is None and hasattr(handler, '__bolt_guards__'):
469
+ merged_guards = handler.__bolt_guards__
470
+
471
+ merged_auth = auth
472
+ if merged_auth is None and hasattr(handler, '__bolt_auth__'):
473
+ merged_auth = handler.__bolt_auth__
474
+
475
+ # Status code priority: explicit status_code param > handler attribute > action default
476
+ merged_status_code = status_code
477
+ if merged_status_code is None and hasattr(handler, '__bolt_status_code__'):
478
+ merged_status_code = handler.__bolt_status_code__
479
+ if merged_status_code is None:
480
+ merged_status_code = action_status_code
481
+
482
+ # Register the route
483
+ route_decorator = self._route_decorator(
484
+ http_method,
485
+ route_path,
486
+ response_model=None,
487
+ status_code=merged_status_code,
488
+ guards=merged_guards,
489
+ auth=merged_auth
490
+ )
491
+ route_decorator(handler)
492
+
493
+ # Scan for custom actions (@action decorator)
494
+ self._register_custom_actions(viewset_cls, base_path=path, lookup_field=actual_lookup_field)
495
+
496
+ return viewset_cls
497
+
498
+ return decorator
499
+
500
+ def _register_custom_actions(self, view_cls: type, base_path: Optional[str], lookup_field: Optional[str]):
501
+ """
502
+ Scan a ViewSet class for custom action methods and register them.
503
+
504
+ Custom actions are methods decorated with @action decorator.
505
+
506
+ Args:
507
+ view_cls: The ViewSet class to scan
508
+ base_path: Base path for the ViewSet (e.g., "/users")
509
+ lookup_field: Lookup field name for detail actions (e.g., "id", "pk")
510
+ """
511
+ import inspect
512
+ import types
513
+ from .decorators import ActionHandler
514
+
515
+ # Get class-level auth and guards (if any)
516
+ class_auth = getattr(view_cls, 'auth', None)
517
+ class_guards = getattr(view_cls, 'guards', None)
518
+
519
+ # Scan all attributes in the class
520
+ for name in dir(view_cls):
521
+ # Skip private attributes and standard action methods
522
+ if name.startswith('_') or name.lower() in [
523
+ 'get', 'post', 'put', 'patch', 'delete', 'head', 'options',
524
+ 'list', 'retrieve', 'create', 'update', 'partial_update', 'destroy'
525
+ ]:
526
+ continue
527
+
528
+ attr = getattr(view_cls, name)
529
+
530
+ # Check if it's an ActionHandler instance (decorated with @action)
531
+ if isinstance(attr, ActionHandler):
532
+ # Validate that we have base_path for auto-generation
533
+ if base_path is None:
534
+ raise ValueError(
535
+ f"Custom action {view_cls.__name__}.{name} uses @action decorator, "
536
+ f"but ViewSet was registered with api.view() instead of api.viewset(). "
537
+ f"Use api.viewset() for automatic action path generation."
538
+ )
539
+
540
+ # Extract the unbound function from the ActionHandler
541
+ unbound_fn = attr.fn
542
+
543
+ # Auto-generate route path based on detail flag
544
+ if attr.detail:
545
+ # Instance-level action: /base_path/{lookup_field}/action_name
546
+ # Example: /users/{id}/activate
547
+ action_path = f"{base_path}/{{{lookup_field}}}/{attr.path}"
548
+ else:
549
+ # Collection-level action: /base_path/action_name
550
+ # Example: /users/active
551
+ action_path = f"{base_path}/{attr.path}"
552
+
553
+ # Register route for each HTTP method
554
+ for http_method in attr.methods:
555
+ # Create a wrapper that calls the method as an instance method
556
+ async def custom_action_handler(
557
+ *args,
558
+ __unbound_fn=unbound_fn,
559
+ __view_cls=view_cls,
560
+ **kwargs
561
+ ):
562
+ """Wrapper for custom action method."""
563
+ view = __view_cls()
564
+ # Bind the unbound method to the view instance
565
+ bound_method = types.MethodType(__unbound_fn, view)
566
+ return await bound_method(*args, **kwargs)
567
+
568
+ # Preserve signature and annotations from original method
569
+ sig = inspect.signature(unbound_fn)
570
+ params = list(sig.parameters.values())[1:] # Skip 'self'
571
+ custom_action_handler.__signature__ = sig.replace(parameters=params)
572
+ custom_action_handler.__annotations__ = {
573
+ k: v for k, v in unbound_fn.__annotations__.items() if k != 'self'
574
+ }
575
+ custom_action_handler.__name__ = f"{view_cls.__name__}.{name}"
576
+ custom_action_handler.__doc__ = unbound_fn.__doc__
577
+ custom_action_handler.__module__ = unbound_fn.__module__
578
+
579
+ # Merge class-level auth/guards with action-specific auth/guards
580
+ # Action-specific takes precedence if explicitly set
581
+ final_auth = attr.auth if attr.auth is not None else class_auth
582
+ final_guards = attr.guards if attr.guards is not None else class_guards
583
+
584
+ # Register the custom action
585
+ decorator = self._route_decorator(
586
+ http_method,
587
+ action_path,
588
+ response_model=attr.response_model,
589
+ status_code=attr.status_code,
590
+ guards=final_guards,
591
+ auth=final_auth
592
+ )
593
+ decorator(custom_action_handler)
594
+
595
+ def _route_decorator(self, method: str, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
596
+ def decorator(fn: Callable):
597
+ # Enforce async handlers
598
+ if not inspect.iscoroutinefunction(fn):
599
+ raise TypeError(f"Handler {fn.__name__} must be async. Use 'async def' instead of 'def'")
600
+
601
+ handler_id = self._next_handler_id
602
+ self._next_handler_id += 1
603
+
604
+ # Apply prefix to path (conversion happens in Rust)
605
+ full_path = self.prefix + path if self.prefix else path
606
+
607
+ self._routes.append((method, full_path, handler_id, fn))
608
+ self._handlers[handler_id] = fn
609
+
610
+ # Pre-compile lightweight binder for this handler with HTTP method validation
611
+ meta = self._compile_binder(fn, method, full_path)
612
+ # Allow explicit response model override
613
+ if response_model is not None:
614
+ meta["response_type"] = response_model
615
+ if status_code is not None:
616
+ meta["default_status_code"] = int(status_code)
617
+ self._handler_meta[fn] = meta
618
+
619
+ # Compile middleware metadata for this handler (including guards and auth)
620
+ middleware_meta = compile_middleware_meta(
621
+ fn, method, full_path,
622
+ self.middleware, self.middleware_config,
623
+ guards=guards, auth=auth
624
+ )
625
+ if middleware_meta:
626
+ self._handler_middleware[handler_id] = middleware_meta
627
+
628
+ return fn
629
+ return decorator
630
+
631
+ def _compile_binder(self, fn: Callable, http_method: str = "", path: str = "") -> Dict[str, Any]:
632
+ """
633
+ Compile parameter binding metadata for a handler function.
634
+
635
+ This method:
636
+ 1. Parses function signature and type hints
637
+ 2. Creates FieldDefinition for each parameter
638
+ 3. Infers parameter sources (path, query, body, etc.)
639
+ 4. Validates HTTP method compatibility
640
+ 5. Pre-compiles extractors for performance
641
+
642
+ Args:
643
+ fn: Handler function
644
+ http_method: HTTP method (GET, POST, etc.)
645
+ path: Route path pattern
646
+
647
+ Returns:
648
+ Metadata dictionary for parameter binding
649
+
650
+ Raises:
651
+ TypeError: If GET/HEAD/DELETE/OPTIONS handlers have body parameters
652
+ """
653
+ sig = inspect.signature(fn)
654
+ type_hints = get_type_hints(fn, include_extras=True)
655
+
656
+ # Extract path parameters from route pattern
657
+ path_params = _extract_path_params(path)
658
+
659
+ meta: Dict[str, Any] = {
660
+ "sig": sig,
661
+ "fields": [],
662
+ "path_params": path_params,
663
+ "http_method": http_method,
664
+ }
665
+
666
+ # Quick path: single parameter that looks like request
667
+ params = list(sig.parameters.values())
668
+ if len(params) == 1 and params[0].name in {"request", "req"}:
669
+ meta["mode"] = "request_only"
670
+ return meta
671
+
672
+ # Parse each parameter into FieldDefinition
673
+ field_definitions: List[FieldDefinition] = []
674
+
675
+ for param in params:
676
+ name = param.name
677
+ annotation = type_hints.get(name, param.annotation)
678
+
679
+ # Extract explicit markers from Annotated or default
680
+ explicit_marker = None
681
+
682
+ # Check Annotated[T, ...]
683
+ origin = get_origin(annotation)
684
+ if origin is Annotated:
685
+ args = get_args(annotation)
686
+ annotation = args[0] if args else annotation # Unwrap to get actual type
687
+ for meta_val in args[1:]:
688
+ if isinstance(meta_val, (Param, DependsMarker)):
689
+ explicit_marker = meta_val
690
+ break
691
+
692
+ # Check default value for marker
693
+ if explicit_marker is None and isinstance(param.default, (Param, DependsMarker)):
694
+ explicit_marker = param.default
695
+
696
+ # Create FieldDefinition with inference
697
+ field = FieldDefinition.from_parameter(
698
+ parameter=param,
699
+ annotation=annotation,
700
+ path_params=path_params,
701
+ http_method=http_method,
702
+ explicit_marker=explicit_marker,
703
+ )
704
+
705
+ field_definitions.append(field)
706
+
707
+ # HTTP Method Validation: Ensure GET/HEAD/DELETE/OPTIONS don't have body params
708
+ body_fields = [f for f in field_definitions if f.source == "body"]
709
+ if http_method in ("GET", "HEAD", "DELETE", "OPTIONS") and body_fields:
710
+ param_names = [f.name for f in body_fields]
711
+ raise TypeError(
712
+ f"Handler {fn.__name__} for {http_method} {path} cannot have body parameters.\n"
713
+ f"Found body parameters: {param_names}\n"
714
+ f"Solutions:\n"
715
+ f" 1. Change HTTP method to POST/PUT/PATCH\n"
716
+ f" 2. Use Query() marker for query parameters\n"
717
+ f" 3. Use simple types (str, int) which auto-infer as query params"
718
+ )
719
+
720
+ # Convert FieldDefinitions to dict format for backward compatibility
721
+ # (We'll optimize this away in Phase 4)
722
+ for field in field_definitions:
723
+ meta["fields"].append({
724
+ "name": field.name,
725
+ "annotation": field.annotation,
726
+ "default": field.default,
727
+ "kind": field.kind,
728
+ "source": field.source,
729
+ "alias": field.alias,
730
+ "embed": field.embed,
731
+ "dependency": field.dependency,
732
+ "field_def": field, # Store FieldDefinition for future use
733
+ })
734
+
735
+ # Detect single body parameter for fast path
736
+ if len(body_fields) == 1:
737
+ body_field = body_fields[0]
738
+ if body_field.is_msgspec_struct:
739
+ meta["body_struct_param"] = body_field.name
740
+ meta["body_struct_type"] = body_field.annotation
741
+
742
+ # Capture return type for response validation/serialization
743
+ if sig.return_annotation is not inspect._empty:
744
+ meta["response_type"] = sig.return_annotation
745
+
746
+ meta["mode"] = "mixed"
747
+
748
+ # Maintain backward compatibility with old "params" key
749
+ meta["params"] = meta["fields"]
750
+
751
+ # Performance: Check if handler needs form/file parsing
752
+ # This allows us to skip expensive form parsing for 95% of endpoints
753
+ needs_form_parsing = any(f.source in ("form", "file") for f in field_definitions)
754
+ meta["needs_form_parsing"] = needs_form_parsing
755
+
756
+ return meta
757
+
758
+ async def _build_handler_arguments(self, meta: Dict[str, Any], request: Dict[str, Any]) -> Tuple[List[Any], Dict[str, Any]]:
759
+ """Build arguments for handler invocation."""
760
+ args: List[Any] = []
761
+ kwargs: Dict[str, Any] = {}
762
+
763
+ # Access PyRequest mappings
764
+ params_map = request["params"]
765
+ query_map = request["query"]
766
+ headers_map = request.get("headers", {})
767
+ cookies_map = request.get("cookies", {})
768
+
769
+ # Parse form/multipart data ONLY if handler uses Form() or File() parameters
770
+ # This optimization skips parsing for 95% of endpoints (JSON/GET endpoints)
771
+ if meta.get("needs_form_parsing", False):
772
+ form_map, files_map = parse_form_data(request, headers_map)
773
+ else:
774
+ form_map, files_map = {}, {}
775
+
776
+ # Body decode cache
777
+ body_obj: Any = None
778
+ body_loaded: bool = False
779
+ dep_cache: Dict[Any, Any] = {}
780
+
781
+ for p in meta["params"]:
782
+ name = p["name"]
783
+ source = p["source"]
784
+ depends_marker = p.get("dependency")
785
+
786
+ if source == "request":
787
+ value = request
788
+ elif source == "dependency":
789
+ dep_fn = depends_marker.dependency if depends_marker else None
790
+ if dep_fn is None:
791
+ raise ValueError(f"Depends for parameter {name} requires a callable")
792
+ value = await resolve_dependency(
793
+ dep_fn, depends_marker, request, dep_cache,
794
+ params_map, query_map, headers_map, cookies_map,
795
+ self._handler_meta, self._compile_binder,
796
+ meta.get("http_method", ""), meta.get("path", "")
797
+ )
798
+ else:
799
+ value, body_obj, body_loaded = extract_parameter_value(
800
+ p, request, params_map, query_map, headers_map, cookies_map,
801
+ form_map, files_map, meta, body_obj, body_loaded
802
+ )
803
+
804
+ # Respect positional-only/keyword-only kinds
805
+ if p["kind"] in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
806
+ args.append(value)
807
+ else:
808
+ kwargs[name] = value
809
+
810
+ return args, kwargs
811
+
812
+
813
+ def _handle_http_exception(self, he: HTTPException) -> Response:
814
+ """Handle HTTPException and return response."""
815
+ try:
816
+ body = msgspec.json.encode({"detail": he.detail})
817
+ headers = [("content-type", "application/json")]
818
+ except Exception:
819
+ body = str(he.detail).encode()
820
+ headers = [("content-type", "text/plain; charset=utf-8")]
821
+
822
+ if he.headers:
823
+ headers.extend([(k.lower(), v) for k, v in he.headers.items()])
824
+
825
+ return int(he.status_code), headers, body
826
+
827
+ def _handle_generic_exception(self, e: Exception, request: Dict[str, Any] = None) -> Response:
828
+ """Handle generic exception using error_handlers module."""
829
+ from . import error_handlers
830
+ # Use the error handler which respects Django DEBUG setting
831
+ return error_handlers.handle_exception(e, debug=False, request=request) # debug will be checked dynamically
832
+
833
+ async def _dispatch(self, handler: Callable, request: Dict[str, Any], handler_id: int = None) -> Response:
834
+ """Async dispatch that calls the handler and returns response tuple.
835
+
836
+ Args:
837
+ handler: The route handler function
838
+ request: The request dictionary
839
+ handler_id: Handler ID to lookup original API (for merged APIs)
840
+ """
841
+ # For merged APIs, use the original API's logging middleware
842
+ # This preserves per-API logging, auth, and middleware config (Litestar-style)
843
+ logging_middleware = self._logging_middleware
844
+ if handler_id is not None and hasattr(self, '_handler_api_map'):
845
+ original_api = self._handler_api_map.get(handler_id)
846
+ if original_api and original_api._logging_middleware:
847
+ logging_middleware = original_api._logging_middleware
848
+
849
+ # Start timing only if we might log
850
+ start_time = None
851
+ if logging_middleware:
852
+ # Determine if INFO logs are enabled or a slow-only threshold exists
853
+ logger = logging_middleware.logger
854
+ should_time = False
855
+ try:
856
+ if logger.isEnabledFor(__import__('logging').INFO):
857
+ should_time = True
858
+ except Exception:
859
+ pass
860
+ if not should_time:
861
+ # If slow-only is configured, we still need timing
862
+ should_time = bool(getattr(logging_middleware.config, 'min_duration_ms', None))
863
+ if should_time:
864
+ start_time = time.time()
865
+
866
+ # Log request if logging enabled (DEBUG-level guard happens inside)
867
+ logging_middleware.log_request(request)
868
+
869
+ try:
870
+ meta = self._handler_meta.get(handler)
871
+ if meta is None:
872
+ meta = self._compile_binder(handler)
873
+ self._handler_meta[handler] = meta
874
+
875
+ # Fast path for request-only handlers
876
+ if meta.get("mode") == "request_only":
877
+ result = await handler(request)
878
+ else:
879
+ # Build handler arguments
880
+ args, kwargs = await self._build_handler_arguments(meta, request)
881
+ result = await handler(*args, **kwargs)
882
+
883
+ # Serialize response
884
+ response = await serialize_response(result, meta)
885
+
886
+ # Log response if logging enabled
887
+ if logging_middleware and start_time is not None:
888
+ duration = time.time() - start_time
889
+ status_code = response[0] if isinstance(response, tuple) else 200
890
+ logging_middleware.log_response(request, status_code, duration)
891
+
892
+ return response
893
+
894
+ except HTTPException as he:
895
+ # Log exception if logging enabled
896
+ if logging_middleware and start_time is not None:
897
+ duration = time.time() - start_time
898
+ logging_middleware.log_response(request, he.status_code, duration)
899
+
900
+ return self._handle_http_exception(he)
901
+ except Exception as e:
902
+ # Log exception if logging enabled
903
+ if logging_middleware:
904
+ logging_middleware.log_exception(request, e, exc_info=True)
905
+
906
+ return self._handle_generic_exception(e, request=request)
907
+
908
+ def _get_openapi_schema(self) -> Dict[str, Any]:
909
+ """Get or generate OpenAPI schema.
910
+
911
+ Returns:
912
+ OpenAPI schema as dictionary.
913
+ """
914
+ if self._openapi_schema is None:
915
+ from .openapi.schema_generator import SchemaGenerator
916
+
917
+ generator = SchemaGenerator(self, self.openapi_config)
918
+ openapi = generator.generate()
919
+ self._openapi_schema = openapi.to_schema()
920
+
921
+ return self._openapi_schema
922
+
923
+ def _register_openapi_routes(self) -> None:
924
+ """Register OpenAPI documentation routes.
925
+
926
+ Delegates to OpenAPIRouteRegistrar for cleaner separation of concerns.
927
+ """
928
+ from .openapi.routes import OpenAPIRouteRegistrar
929
+
930
+ registrar = OpenAPIRouteRegistrar(self)
931
+ registrar.register_routes()
932
+
933
+ def _register_admin_routes(self, host: str = "localhost", port: int = 8000) -> None:
934
+ """Register Django admin routes via ASGI bridge.
935
+
936
+ Delegates to AdminRouteRegistrar for cleaner separation of concerns.
937
+
938
+ Args:
939
+ host: Server hostname for ASGI scope
940
+ port: Server port for ASGI scope
941
+ """
942
+ from .admin.routes import AdminRouteRegistrar
943
+
944
+ registrar = AdminRouteRegistrar(self)
945
+ registrar.register_routes(host, port)
946
+
947
+ def _register_static_routes(self) -> None:
948
+ """Register static file serving routes for Django admin.
949
+
950
+ Delegates to StaticRouteRegistrar for cleaner separation of concerns.
951
+ """
952
+ from .admin.static_routes import StaticRouteRegistrar
953
+
954
+ registrar = StaticRouteRegistrar(self)
955
+ registrar.register_routes()
956
+
957
+ def serve(self, host: str = "0.0.0.0", port: int = 8000) -> None:
958
+ """Start the async server with registered routes"""
959
+ info = ensure_django_ready()
960
+ print(
961
+ f"[django-bolt] Django setup: mode={info.get('mode')} debug={info.get('debug')}\n"
962
+ f"[django-bolt] DB: {info.get('database')} name={info.get('database_name')}\n"
963
+ f"[django-bolt] Settings: {info.get('settings_module') or 'embedded'}"
964
+ )
965
+
966
+ # Register Django admin routes if enabled
967
+ if self.enable_admin:
968
+ self._register_admin_routes(host, port)
969
+ if self._admin_routes_registered:
970
+ from .admin.admin_detection import detect_admin_url_prefix
971
+ admin_prefix = detect_admin_url_prefix() or 'admin'
972
+ print(f"[django-bolt] Django admin available at http://{host}:{port}/{admin_prefix}/")
973
+
974
+ # Also register static file routes for admin
975
+ self._register_static_routes()
976
+ if self._static_routes_registered:
977
+ print(f"[django-bolt] Static files serving enabled")
978
+
979
+ # Register OpenAPI routes if configured
980
+ if self.openapi_config:
981
+ self._register_openapi_routes()
982
+ print(f"[django-bolt] OpenAPI docs available at http://{host}:{port}{self.openapi_config.path}")
983
+
984
+ # Register all routes with Rust router
985
+ rust_routes = [
986
+ (method, path, handler_id, handler)
987
+ for method, path, handler_id, handler in self._routes
988
+ ]
989
+
990
+ # Register routes in Rust
991
+ _core.register_routes(rust_routes)
992
+
993
+ # Register middleware metadata if any exists
994
+ if self._handler_middleware:
995
+ middleware_data = [
996
+ (handler_id, meta)
997
+ for handler_id, meta in self._handler_middleware.items()
998
+ ]
999
+ _core.register_middleware_metadata(middleware_data)
1000
+ print(f"[django-bolt] Registered middleware for {len(middleware_data)} handlers")
1001
+
1002
+ print(f"[django-bolt] Registered {len(self._routes)} routes")
1003
+ print(f"[django-bolt] Starting async server on http://{host}:{port}")
1004
+
1005
+ # Get compression config
1006
+ compression_config = None
1007
+ if self.compression is not None:
1008
+ compression_config = self.compression.to_rust_config()
1009
+
1010
+ # Start async server
1011
+ _core.start_server_async(self._dispatch, host, port, compression_config)