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.

Files changed (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.pyd +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,427 @@
1
+ import importlib
2
+ import sys
3
+ from django.core.management.base import BaseCommand, CommandError
4
+ from django.conf import settings
5
+ from django.apps import apps
6
+
7
+ from django_bolt.api import BoltAPI
8
+ from django_bolt import _core
9
+
10
+
11
+ class Command(BaseCommand):
12
+ help = "Run Django-Bolt server with autodiscovered APIs"
13
+
14
+ def add_arguments(self, parser):
15
+ parser.add_argument(
16
+ "--host", default="0.0.0.0", help="Host to bind to (default: 0.0.0)"
17
+ )
18
+ parser.add_argument(
19
+ "--port", type=int, default=8000, help="Port to bind to (default: 8000)"
20
+ )
21
+ parser.add_argument(
22
+ "--processes", type=int, default=1, help="Number of processes (default: 1)"
23
+ )
24
+ parser.add_argument(
25
+ "--workers", type=int, default=2, help="Workers per process (default: 2)"
26
+ )
27
+ parser.add_argument(
28
+ "--no-admin",
29
+ action="store_true",
30
+ help="Disable Django admin integration (admin enabled by default)",
31
+ )
32
+ parser.add_argument(
33
+ "--dev",
34
+ action="store_true",
35
+ help="Enable auto-reload on file changes (development mode)"
36
+ )
37
+
38
+ def handle(self, *args, **options):
39
+ processes = options['processes']
40
+ dev_mode = options.get('dev', False)
41
+
42
+ # Dev mode: force single process + single worker + enable auto-reload
43
+ if dev_mode:
44
+ options['workers'] = 1
45
+ if processes > 1:
46
+ self.stdout.write(
47
+ self.style.WARNING(
48
+ "[django-bolt] Dev mode enabled: forcing --processes=1 for auto-reload"
49
+ )
50
+ )
51
+ options['processes'] = 1
52
+
53
+ self.stdout.write(
54
+ self.style.SUCCESS("[django-bolt] 🔥 Development mode enabled (auto-reload on file changes)")
55
+ )
56
+ self.run_with_autoreload(options)
57
+ else:
58
+ # Production mode (current logic)
59
+ if processes > 1:
60
+ self.start_multiprocess(options)
61
+ else:
62
+ self.start_single_process(options)
63
+
64
+ def run_with_autoreload(self, options):
65
+ """Run server with auto-reload using Django's autoreload system"""
66
+ try:
67
+ from django.utils import autoreload
68
+ except ImportError:
69
+ self.stdout.write(
70
+ self.style.ERROR(
71
+ "[django-bolt] Error: Django autoreload not available. "
72
+ "Upgrade Django or use --no-dev mode."
73
+ )
74
+ )
75
+ import sys
76
+ sys.exit(1)
77
+
78
+ # Use Django's autoreload system which is optimized
79
+ # It only restarts the Python interpreter when necessary
80
+ # and reuses the same process for faster reloads
81
+ def run_server():
82
+ self.start_single_process(options)
83
+
84
+ autoreload.run_with_reloader(run_server)
85
+
86
+ def start_multiprocess(self, options):
87
+ """Start multiple processes with SO_REUSEPORT"""
88
+ import os
89
+ import sys
90
+ import signal
91
+
92
+ processes = options['processes']
93
+ self.stdout.write(f"[django-bolt] Starting {processes} processes with SO_REUSEPORT")
94
+
95
+ # Store child PIDs for cleanup
96
+ child_pids = []
97
+
98
+ def signal_handler(signum, frame):
99
+ self.stdout.write("\n[django-bolt] Shutting down processes...")
100
+ for pid in child_pids:
101
+ try:
102
+ os.kill(pid, signal.SIGTERM)
103
+ except ProcessLookupError:
104
+ pass
105
+ sys.exit(0)
106
+
107
+ signal.signal(signal.SIGINT, signal_handler)
108
+ signal.signal(signal.SIGTERM, signal_handler)
109
+
110
+ # Fork processes
111
+ for i in range(processes):
112
+ pid = os.fork()
113
+ if pid == 0:
114
+ # Child process
115
+ os.environ['DJANGO_BOLT_REUSE_PORT'] = '1'
116
+ os.environ['DJANGO_BOLT_PROCESS_ID'] = str(i)
117
+ self.start_single_process(options, process_id=i)
118
+ sys.exit(0)
119
+ else:
120
+ # Parent process
121
+ child_pids.append(pid)
122
+ self.stdout.write(f"[django-bolt] Started process {i} (PID: {pid})")
123
+
124
+ # Parent waits for children
125
+ try:
126
+ while True:
127
+ pid, status = os.wait()
128
+ self.stdout.write(f"[django-bolt] Process {pid} exited with status {status}")
129
+ if pid in child_pids:
130
+ child_pids.remove(pid)
131
+ if not child_pids:
132
+ break
133
+ except KeyboardInterrupt:
134
+ pass
135
+
136
+ def start_single_process(self, options, process_id=None):
137
+ """Start a single process server"""
138
+ # Setup Django logging once at server startup (one-shot, respects existing LOGGING)
139
+ from django_bolt.logging.config import setup_django_logging
140
+ setup_django_logging()
141
+
142
+ # Initialize FileResponse settings cache once at server startup
143
+ from django_bolt.responses import initialize_file_response_settings
144
+ initialize_file_response_settings()
145
+
146
+ if process_id is not None:
147
+ self.stdout.write(f"[django-bolt] Process {process_id}: Starting autodiscovery...")
148
+ else:
149
+ self.stdout.write("[django-bolt] Starting autodiscovery...")
150
+
151
+ # Autodiscover BoltAPI instances
152
+ apis = self.autodiscover_apis()
153
+
154
+ if not apis:
155
+ self.stdout.write(
156
+ self.style.WARNING("No BoltAPI instances found. Create api.py files with api = BoltAPI()")
157
+ )
158
+ return
159
+
160
+ # Merge all APIs and collect routes FIRST
161
+ merged_api = self.merge_apis(apis)
162
+
163
+ # Register OpenAPI routes AFTER merging (so schema includes all routes)
164
+ openapi_enabled = False
165
+ openapi_config = None
166
+
167
+ # Find first API with OpenAPI config
168
+ for api_path, api in apis:
169
+ if api.openapi_config:
170
+ openapi_config = api.openapi_config
171
+ openapi_enabled = True
172
+ break
173
+
174
+ # Register OpenAPI routes on merged API if any API had OpenAPI enabled
175
+ if openapi_enabled and openapi_config:
176
+ # Transfer OpenAPI config to merged API
177
+ merged_api.openapi_config = openapi_config
178
+ merged_api._register_openapi_routes()
179
+
180
+ if process_id is not None:
181
+ self.stdout.write(f"[django-bolt] Process {process_id}: OpenAPI docs enabled at {openapi_config.path}")
182
+ else:
183
+ self.stdout.write(self.style.SUCCESS(f"[django-bolt] OpenAPI docs enabled at {openapi_config.path}"))
184
+
185
+ # Register Django admin routes if not disabled
186
+ # Admin is controlled solely by --no-admin command-line flag
187
+ admin_enabled = not options.get('no_admin', False)
188
+
189
+ if admin_enabled:
190
+ # Register admin routes
191
+ merged_api._register_admin_routes(options['host'], options['port'])
192
+
193
+ if merged_api._admin_routes_registered:
194
+ from django_bolt.admin.admin_detection import detect_admin_url_prefix
195
+ admin_prefix = detect_admin_url_prefix() or 'admin'
196
+ if process_id is not None:
197
+ self.stdout.write(f"[django-bolt] Process {process_id}: Django admin enabled at http://{options['host']}:{options['port']}/{admin_prefix}/")
198
+ else:
199
+ self.stdout.write(self.style.SUCCESS(f"[django-bolt] Django admin enabled at http://{options['host']}:{options['port']}/{admin_prefix}/"))
200
+
201
+ # Also register static file routes for admin
202
+ merged_api._register_static_routes()
203
+ if merged_api._static_routes_registered:
204
+ if process_id is not None:
205
+ self.stdout.write(f"[django-bolt] Process {process_id}: Static files serving enabled")
206
+ else:
207
+ self.stdout.write("[django-bolt] Static files serving enabled")
208
+
209
+ if process_id is not None:
210
+ self.stdout.write(f"[django-bolt] Process {process_id}: Found {len(merged_api._routes)} routes from {len(apis)} APIs")
211
+ else:
212
+ self.stdout.write(
213
+ self.style.SUCCESS(f"[django-bolt] Found {len(merged_api._routes)} routes")
214
+ )
215
+
216
+ # Register routes with Rust
217
+ rust_routes = []
218
+ for method, path, handler_id, handler in merged_api._routes:
219
+ # Ensure matchit path syntax
220
+ from django_bolt.api import BoltAPI
221
+ convert = getattr(merged_api, "_convert_path", None)
222
+ norm_path = convert(path) if callable(convert) else path
223
+ rust_routes.append((method, norm_path, handler_id, handler))
224
+
225
+ _core.register_routes(rust_routes)
226
+
227
+ # Register middleware metadata if present
228
+ if merged_api._handler_middleware:
229
+ middleware_data = [
230
+ (handler_id, meta)
231
+ for handler_id, meta in merged_api._handler_middleware.items()
232
+ ]
233
+ _core.register_middleware_metadata(middleware_data)
234
+ if process_id is not None:
235
+ self.stdout.write(f"[django-bolt] Process {process_id}: Registered middleware for {len(middleware_data)} handlers")
236
+ else:
237
+ self.stdout.write(f"[django-bolt] Registered middleware for {len(middleware_data)} handlers")
238
+
239
+ if process_id is not None:
240
+ self.stdout.write(self.style.SUCCESS(f"[django-bolt] Process {process_id}: Starting server on http://{options['host']}:{options['port']}"))
241
+ self.stdout.write(f"[django-bolt] Process {process_id}: Workers: {options['workers']}")
242
+ else:
243
+ self.stdout.write(self.style.SUCCESS(f"[django-bolt] Starting server on http://{options['host']}:{options['port']}"))
244
+ self.stdout.write(f"[django-bolt] Workers: {options['workers']}, Processes: {options['processes']}")
245
+ self.stdout.write(self.style.SUCCESS(f"[django-bolt] OpenAPI docs enabled at http://{options['host']}:{options['port']}/docs/"))
246
+ # Set environment variable for Rust to read worker count
247
+ import os
248
+ os.environ['DJANGO_BOLT_WORKERS'] = str(options['workers'])
249
+
250
+ # Determine compression config (server-level in Actix)
251
+ # Priority: Django setting > first API with compression config
252
+ compression_config = None
253
+ if hasattr(settings, 'BOLT_COMPRESSION'):
254
+ # Use Django setting if provided (highest priority)
255
+ if settings.BOLT_COMPRESSION is not None and settings.BOLT_COMPRESSION is not False:
256
+ compression_config = settings.BOLT_COMPRESSION.to_rust_config()
257
+ else:
258
+ # Check if any API has compression configured
259
+ for api_path, api in apis:
260
+ if hasattr(api, 'compression') and api.compression is not None:
261
+ compression_config = api.compression.to_rust_config()
262
+ break
263
+
264
+ # Start the server
265
+ _core.start_server_async(merged_api._dispatch, options["host"], options["port"], compression_config)
266
+
267
+ def autodiscover_apis(self):
268
+ """Discover BoltAPI instances from installed apps.
269
+
270
+ Deduplicates by object identity to ensure each handler uses the FIRST
271
+ API instance created (with correct config), not duplicates from re-imports.
272
+ """
273
+ apis = []
274
+
275
+ # Check explicit settings first
276
+ if hasattr(settings, 'BOLT_API'):
277
+ for api_path in settings.BOLT_API:
278
+ api = self.import_api(api_path)
279
+ if api:
280
+ apis.append((api_path, api))
281
+ return self._deduplicate_apis(apis)
282
+
283
+ # Try project-level API first (common pattern)
284
+ project_name = settings.ROOT_URLCONF.split('.')[0] # Extract project name from ROOT_URLCONF
285
+ project_candidates = [
286
+ f"{project_name}.api:api",
287
+ f"{project_name}.bolt_api:api",
288
+ ]
289
+
290
+ for candidate in project_candidates:
291
+ api = self.import_api(candidate)
292
+ if api:
293
+ apis.append((candidate, api))
294
+
295
+ # Track which apps we've already imported (to avoid duplicates)
296
+ imported_apps = {api_path.split(':')[0].split('.')[0] for api_path, _ in apis}
297
+
298
+ # Autodiscover from installed apps
299
+ for app_config in apps.get_app_configs():
300
+ # Skip django_bolt itself
301
+ if app_config.name == 'django_bolt':
302
+ continue
303
+
304
+ # Skip if we already imported this app at project level
305
+ app_base = app_config.name.split('.')[0]
306
+ if app_base in imported_apps:
307
+ continue
308
+
309
+ # Check if app config has bolt_api hint
310
+ if hasattr(app_config, 'bolt_api'):
311
+ api = self.import_api(app_config.bolt_api)
312
+ if api:
313
+ apis.append((app_config.bolt_api, api))
314
+ continue
315
+
316
+ # Try standard locations
317
+ app_name = app_config.name
318
+ candidates = [
319
+ f"{app_name}.api:api",
320
+ f"{app_name}.bolt_api:api",
321
+ ]
322
+
323
+ for candidate in candidates:
324
+ api = self.import_api(candidate)
325
+ if api:
326
+ apis.append((candidate, api))
327
+ break # Only take first match per app
328
+
329
+ return self._deduplicate_apis(apis)
330
+
331
+ def _deduplicate_apis(self, apis):
332
+ """Deduplicate APIs by object identity.
333
+
334
+ This ensures each handler uses the FIRST API instance created (with original
335
+ config), not duplicates from module re-imports. Critical for preserving
336
+ per-API logging, auth, and middleware configs.
337
+ """
338
+ seen_ids = set()
339
+ deduplicated = []
340
+ for api_path, api in apis:
341
+ api_id = id(api)
342
+ if api_id not in seen_ids:
343
+ seen_ids.add(api_id)
344
+ deduplicated.append((api_path, api))
345
+ else:
346
+ self.stdout.write(f"[django-bolt] Skipped duplicate API instance from {api_path}")
347
+ return deduplicated
348
+
349
+ def import_api(self, dotted_path):
350
+ """Import a BoltAPI instance from dotted path like 'myapp.api:api'"""
351
+ try:
352
+ if ':' not in dotted_path:
353
+ return None
354
+
355
+ module_path, attr_name = dotted_path.split(':', 1)
356
+ module = importlib.import_module(module_path)
357
+
358
+ if not hasattr(module, attr_name):
359
+ return None
360
+
361
+ api = getattr(module, attr_name)
362
+
363
+ # Verify it's a BoltAPI instance
364
+ if isinstance(api, BoltAPI):
365
+ return api
366
+
367
+ except (ImportError, AttributeError, ValueError):
368
+ pass
369
+
370
+ return None
371
+
372
+ def merge_apis(self, apis):
373
+ """Merge multiple BoltAPI instances into one, preserving per-API context.
374
+
375
+ Uses Litestar-style approach: each handler maintains reference to its original
376
+ API instance, allowing it to use that API's logging, auth, and middleware config.
377
+ """
378
+ if len(apis) == 1:
379
+ return apis[0][1] # Return the single API
380
+
381
+ # Create a new merged API without logging (handlers will use their original APIs)
382
+ merged = BoltAPI(enable_logging=False)
383
+ route_map = {} # Track conflicts
384
+
385
+ # Map handler_id -> original API instance (preserves per-API context)
386
+ merged._handler_api_map = {}
387
+
388
+ # Track next available handler_id to avoid collisions
389
+ next_handler_id = 0
390
+
391
+ for api_path, api in apis:
392
+ self.stdout.write(f"[django-bolt] Merging API from {api_path}")
393
+
394
+ for method, path, old_handler_id, handler in api._routes:
395
+ route_key = f"{method} {path}"
396
+
397
+ if route_key in route_map:
398
+ raise CommandError(
399
+ f"Route conflict: {route_key} defined in both "
400
+ f"{route_map[route_key]} and {api_path}"
401
+ )
402
+
403
+ # CRITICAL: Assign NEW unique handler_id to avoid collisions
404
+ # Each API starts handler_ids at 0, so we must renumber during merge
405
+ new_handler_id = next_handler_id
406
+ next_handler_id += 1
407
+
408
+ route_map[route_key] = api_path
409
+ merged._routes.append((method, path, new_handler_id, handler))
410
+ merged._handlers[new_handler_id] = handler
411
+
412
+ # CRITICAL: Store reference to original API for this handler
413
+ # This preserves logging, auth, middleware, and all per-API config
414
+ merged._handler_api_map[new_handler_id] = api
415
+
416
+ # Merge handler metadata
417
+ if handler in api._handler_meta:
418
+ merged._handler_meta[handler] = api._handler_meta[handler]
419
+
420
+ # Merge middleware metadata (use NEW handler_id)
421
+ if old_handler_id in api._handler_middleware:
422
+ merged._handler_middleware[new_handler_id] = api._handler_middleware[old_handler_id]
423
+
424
+ # Update next handler ID
425
+ merged._next_handler_id = next_handler_id
426
+
427
+ return merged
@@ -0,0 +1,32 @@
1
+ """
2
+ Django-Bolt Middleware System.
3
+
4
+ Provides decorators and classes for adding middleware to routes.
5
+ Middleware can be global or per-route.
6
+ """
7
+
8
+ from .middleware import (
9
+ Middleware,
10
+ MiddlewareGroup,
11
+ MiddlewareConfig,
12
+ middleware,
13
+ rate_limit,
14
+ cors,
15
+ skip_middleware,
16
+ no_compress,
17
+ CORSMiddleware,
18
+ RateLimitMiddleware,
19
+ )
20
+
21
+ __all__ = [
22
+ "Middleware",
23
+ "MiddlewareGroup",
24
+ "MiddlewareConfig",
25
+ "middleware",
26
+ "rate_limit",
27
+ "cors",
28
+ "skip_middleware",
29
+ "no_compress",
30
+ "CORSMiddleware",
31
+ "RateLimitMiddleware",
32
+ ]
@@ -0,0 +1,131 @@
1
+ """Middleware compilation utilities."""
2
+ from typing import Any, Callable, Dict, List, Optional, Set
3
+ from ..auth.backends import get_default_authentication_classes
4
+ from ..auth.guards import get_default_permission_classes
5
+
6
+
7
+ def compile_middleware_meta(
8
+ handler: Callable,
9
+ method: str,
10
+ path: str,
11
+ global_middleware: List[Any],
12
+ global_middleware_config: Dict[str, Any],
13
+ guards: Optional[List[Any]] = None,
14
+ auth: Optional[List[Any]] = None
15
+ ) -> Optional[Dict[str, Any]]:
16
+ """Compile middleware metadata for a handler, including guards and auth."""
17
+ # Check for handler-specific middleware
18
+ handler_middleware = []
19
+ skip_middleware: Set[str] = set()
20
+
21
+ if hasattr(handler, '__bolt_middleware__'):
22
+ handler_middleware = handler.__bolt_middleware__
23
+
24
+ if hasattr(handler, '__bolt_skip_middleware__'):
25
+ skip_middleware = handler.__bolt_skip_middleware__
26
+
27
+ # Merge global and handler middleware
28
+ all_middleware = []
29
+
30
+ # Add global middleware first
31
+ for mw in global_middleware:
32
+ mw_dict = middleware_to_dict(mw)
33
+ if mw_dict and mw_dict.get('type') not in skip_middleware:
34
+ all_middleware.append(mw_dict)
35
+
36
+ # Add global config-based middleware
37
+ if global_middleware_config:
38
+ for mw_type, config in global_middleware_config.items():
39
+ if mw_type not in skip_middleware:
40
+ mw_dict = {'type': mw_type}
41
+ mw_dict.update(config)
42
+ all_middleware.append(mw_dict)
43
+
44
+ # Add handler-specific middleware
45
+ for mw in handler_middleware:
46
+ mw_dict = middleware_to_dict(mw)
47
+ if mw_dict:
48
+ all_middleware.append(mw_dict)
49
+
50
+ # Compile authentication backends
51
+ auth_backends = []
52
+ if auth is not None:
53
+ # Per-route auth override
54
+ for auth_backend in auth:
55
+ if hasattr(auth_backend, 'to_metadata'):
56
+ auth_backends.append(auth_backend.to_metadata())
57
+ else:
58
+ # Use global default authentication classes
59
+ for auth_backend in get_default_authentication_classes():
60
+ if hasattr(auth_backend, 'to_metadata'):
61
+ auth_backends.append(auth_backend.to_metadata())
62
+
63
+ # Compile guards/permissions
64
+ guard_list = []
65
+ if guards is not None:
66
+ # Per-route guards override
67
+ for guard in guards:
68
+ # Check if it's an instance with to_metadata method
69
+ if hasattr(guard, 'to_metadata') and callable(getattr(guard, 'to_metadata', None)):
70
+ try:
71
+ # Try calling as instance method
72
+ guard_list.append(guard.to_metadata())
73
+ except TypeError:
74
+ # If it fails, might be a class, try instantiating
75
+ try:
76
+ instance = guard()
77
+ guard_list.append(instance.to_metadata())
78
+ except Exception:
79
+ pass
80
+ elif isinstance(guard, type):
81
+ # It's a class reference, instantiate it
82
+ try:
83
+ instance = guard()
84
+ if hasattr(instance, 'to_metadata'):
85
+ guard_list.append(instance.to_metadata())
86
+ except Exception:
87
+ pass
88
+ else:
89
+ # Use global default permission classes
90
+ for guard in get_default_permission_classes():
91
+ if hasattr(guard, 'to_metadata'):
92
+ guard_list.append(guard.to_metadata())
93
+
94
+ # Only include metadata if something is configured
95
+ # Note: include result even when only skip flags are present so Rust can
96
+ # honor route-level skips like `compression`.
97
+ if not all_middleware and not auth_backends and not guard_list and not skip_middleware:
98
+ return None
99
+
100
+ result = {
101
+ 'method': method,
102
+ 'path': path
103
+ }
104
+
105
+ if all_middleware:
106
+ result['middleware'] = all_middleware
107
+
108
+ # Always include skip flags if present (even without middleware/auth/guards)
109
+ if skip_middleware:
110
+ result['skip'] = list(skip_middleware)
111
+
112
+ if auth_backends:
113
+ result['auth_backends'] = auth_backends
114
+
115
+ if guard_list:
116
+ result['guards'] = guard_list
117
+
118
+ return result
119
+
120
+
121
+ def middleware_to_dict(mw: Any) -> Optional[Dict[str, Any]]:
122
+ """Convert middleware specification to dictionary."""
123
+ if isinstance(mw, dict):
124
+ return mw
125
+ elif hasattr(mw, '__dict__'):
126
+ # Convert middleware object to dict
127
+ return {
128
+ 'type': mw.__class__.__name__.lower().replace('middleware', ''),
129
+ **mw.__dict__
130
+ }
131
+ return None