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,147 @@
1
+ from .api import BoltAPI
2
+ from .responses import Response, JSON, StreamingResponse
3
+ from .compression import CompressionConfig
4
+
5
+ # Views module
6
+ from .views import (
7
+ APIView,
8
+ ViewSet,
9
+ ModelViewSet,
10
+ ReadOnlyModelViewSet,
11
+ ListMixin,
12
+ RetrieveMixin,
13
+ CreateMixin,
14
+ UpdateMixin,
15
+ PartialUpdateMixin,
16
+ DestroyMixin,
17
+ )
18
+
19
+ # Pagination module
20
+ from .pagination import (
21
+ PaginationBase,
22
+ PageNumberPagination,
23
+ LimitOffsetPagination,
24
+ CursorPagination,
25
+ PaginatedResponse,
26
+ paginate,
27
+ )
28
+
29
+ # Decorators module
30
+ from .decorators import action
31
+
32
+ # Auth module
33
+ from .auth import (
34
+ # Authentication backends
35
+ JWTAuthentication,
36
+ APIKeyAuthentication,
37
+ SessionAuthentication,
38
+ AuthContext,
39
+ # Guards/Permissions
40
+ AllowAny,
41
+ IsAuthenticated,
42
+ IsAdminUser,
43
+ IsStaff,
44
+ HasPermission,
45
+ HasAnyPermission,
46
+ HasAllPermissions,
47
+ # JWT Token & Utilities
48
+ Token,
49
+ create_jwt_for_user,
50
+ get_current_user,
51
+ extract_user_id_from_context,
52
+ get_auth_context,
53
+ )
54
+
55
+ # Middleware module
56
+ from .middleware import (
57
+ Middleware,
58
+ MiddlewareGroup,
59
+ MiddlewareConfig,
60
+ middleware,
61
+ rate_limit,
62
+ cors,
63
+ skip_middleware,
64
+ no_compress,
65
+ CORSMiddleware,
66
+ RateLimitMiddleware,
67
+ )
68
+
69
+ # OpenAPI module
70
+ from .openapi import (
71
+ OpenAPIConfig,
72
+ SwaggerRenderPlugin,
73
+ RedocRenderPlugin,
74
+ ScalarRenderPlugin,
75
+ RapidocRenderPlugin,
76
+ StoplightRenderPlugin,
77
+ JsonRenderPlugin,
78
+ YamlRenderPlugin,
79
+ )
80
+
81
+ __all__ = [
82
+ "BoltAPI",
83
+ "Response",
84
+ "JSON",
85
+ "StreamingResponse",
86
+ "CompressionConfig",
87
+ # Views
88
+ "APIView",
89
+ "ViewSet",
90
+ "ModelViewSet",
91
+ "ReadOnlyModelViewSet",
92
+ "ListMixin",
93
+ "RetrieveMixin",
94
+ "CreateMixin",
95
+ "UpdateMixin",
96
+ "PartialUpdateMixin",
97
+ "DestroyMixin",
98
+ # Pagination
99
+ "PaginationBase",
100
+ "PageNumberPagination",
101
+ "LimitOffsetPagination",
102
+ "CursorPagination",
103
+ "PaginatedResponse",
104
+ "paginate",
105
+ # Decorators
106
+ "action",
107
+ # Auth - Authentication
108
+ "JWTAuthentication",
109
+ "APIKeyAuthentication",
110
+ "SessionAuthentication",
111
+ "AuthContext",
112
+ # Auth - Guards/Permissions
113
+ "AllowAny",
114
+ "IsAuthenticated",
115
+ "IsAdminUser",
116
+ "IsStaff",
117
+ "HasPermission",
118
+ "HasAnyPermission",
119
+ "HasAllPermissions",
120
+ # Middleware
121
+ "middleware",
122
+ "rate_limit",
123
+ "cors",
124
+ "skip_middleware",
125
+ "no_compress",
126
+ "CORSMiddleware",
127
+ "RateLimitMiddleware",
128
+ # Auth - JWT Token & Utilities
129
+ "Token",
130
+ "create_jwt_for_user",
131
+ "get_current_user",
132
+ "extract_user_id_from_context",
133
+ "get_auth_context",
134
+ # OpenAPI
135
+ "OpenAPIConfig",
136
+ "SwaggerRenderPlugin",
137
+ "RedocRenderPlugin",
138
+ "ScalarRenderPlugin",
139
+ "RapidocRenderPlugin",
140
+ "StoplightRenderPlugin",
141
+ "JsonRenderPlugin",
142
+ "YamlRenderPlugin",
143
+ ]
144
+
145
+ default_app_config = 'django_bolt.apps.DjangoBoltConfig'
146
+
147
+
Binary file
@@ -0,0 +1,25 @@
1
+ """
2
+ Django admin integration for django-bolt.
3
+
4
+ This module provides ASGI bridge functionality to integrate Django's admin
5
+ interface with django-bolt's high-performance routing system.
6
+ """
7
+
8
+ from .admin_detection import (
9
+ is_admin_installed,
10
+ detect_admin_url_prefix,
11
+ get_admin_route_patterns,
12
+ should_enable_admin,
13
+ get_admin_info,
14
+ )
15
+
16
+ from .asgi_bridge import ASGIFallbackHandler
17
+
18
+ __all__ = [
19
+ "is_admin_installed",
20
+ "detect_admin_url_prefix",
21
+ "get_admin_route_patterns",
22
+ "should_enable_admin",
23
+ "get_admin_info",
24
+ "ASGIFallbackHandler",
25
+ ]
@@ -0,0 +1,179 @@
1
+ """
2
+ Utilities for detecting and configuring Django admin integration.
3
+ """
4
+
5
+ from typing import List, Optional, Tuple
6
+
7
+
8
+ def is_admin_installed() -> bool:
9
+ """
10
+ Check if Django admin is installed and configured.
11
+
12
+ Returns:
13
+ True if admin is in INSTALLED_APPS, False otherwise
14
+ """
15
+ try:
16
+ from django.conf import settings
17
+ return 'django.contrib.admin' in settings.INSTALLED_APPS
18
+ except Exception:
19
+ return False
20
+
21
+
22
+ def detect_admin_url_prefix() -> Optional[str]:
23
+ """
24
+ Detect the URL prefix for Django admin by parsing urlpatterns.
25
+
26
+ Returns:
27
+ Admin URL prefix (e.g., 'admin' or 'dashboard') or None if not found
28
+ """
29
+ if not is_admin_installed():
30
+ return None
31
+
32
+ try:
33
+ from django.conf import settings
34
+ from django.urls import get_resolver
35
+ import re
36
+
37
+ # Get root URL resolver
38
+ resolver = get_resolver(getattr(settings, 'ROOT_URLCONF', None))
39
+
40
+ # Search for admin patterns
41
+ for url_pattern in resolver.url_patterns:
42
+ # Check if this is admin.site.urls
43
+ if hasattr(url_pattern, 'app_name') and url_pattern.app_name == 'admin':
44
+ # Extract the pattern prefix
45
+ pattern_str = str(url_pattern.pattern)
46
+ # Remove trailing slash and special regex chars
47
+ prefix = pattern_str.rstrip('/^$')
48
+ return prefix if prefix else 'admin'
49
+
50
+ # Also check URLResolver with admin urlconf
51
+ if hasattr(url_pattern, 'urlconf_name'):
52
+ urlconf = url_pattern.urlconf_name
53
+ # Check if urlconf module contains admin.site
54
+ if hasattr(urlconf, '__name__') and 'admin' in str(urlconf.__name__):
55
+ pattern_str = str(url_pattern.pattern)
56
+ prefix = pattern_str.rstrip('/^$')
57
+ return prefix if prefix else 'admin'
58
+
59
+ # Check if urlconf is a list containing admin patterns
60
+ if isinstance(urlconf, (list, tuple)):
61
+ for sub_pattern in urlconf:
62
+ if hasattr(sub_pattern, 'callback') and hasattr(sub_pattern.callback, '__module__'):
63
+ if 'admin' in sub_pattern.callback.__module__:
64
+ pattern_str = str(url_pattern.pattern)
65
+ prefix = pattern_str.rstrip('/^$')
66
+ return prefix if prefix else 'admin'
67
+
68
+ except Exception as e:
69
+ # If detection fails, log warning and return default
70
+ import sys
71
+ print(f"[django-bolt] Warning: Could not auto-detect admin URL prefix: {e}", file=sys.stderr)
72
+
73
+ # Default fallback
74
+ return 'admin'
75
+
76
+
77
+ def get_admin_route_patterns() -> List[Tuple[str, List[str]]]:
78
+ """
79
+ Get route patterns to register for Django admin.
80
+
81
+ Returns:
82
+ List of (path_pattern, methods) tuples for admin routes
83
+ Example: [('/admin/{path:path}', ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])]
84
+ """
85
+ if not is_admin_installed():
86
+ return []
87
+
88
+ # Detect admin URL prefix
89
+ admin_prefix = detect_admin_url_prefix()
90
+ if not admin_prefix:
91
+ return []
92
+
93
+ # Build catch-all pattern for admin routes
94
+ # Use {path:path} syntax for catch-all parameter
95
+ admin_pattern = f'/{admin_prefix}/{{path:path}}'
96
+
97
+ # Admin needs to handle common HTTP methods
98
+ # Only use methods supported by django-bolt's router (GET, POST, PUT, PATCH, DELETE)
99
+ methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
100
+
101
+ # Also add exact /admin/ route (without trailing path)
102
+ admin_root = f'/{admin_prefix}/'
103
+
104
+ return [
105
+ (admin_root, methods),
106
+ (admin_pattern, methods),
107
+ ]
108
+
109
+
110
+ def get_static_url_prefix() -> Optional[str]:
111
+ """
112
+ Get the STATIC_URL prefix from Django settings.
113
+
114
+ Returns:
115
+ Static URL prefix (e.g., 'static') or None if not configured
116
+ """
117
+ try:
118
+ from django.conf import settings
119
+ if hasattr(settings, 'STATIC_URL') and settings.STATIC_URL:
120
+ static_url = settings.STATIC_URL
121
+ # Remove leading/trailing slashes
122
+ return static_url.strip('/')
123
+ except Exception:
124
+ pass
125
+
126
+ return None
127
+
128
+
129
+ def should_enable_admin() -> bool:
130
+ """
131
+ Determine if admin should be auto-enabled.
132
+
133
+ Returns:
134
+ True if admin is installed and can be enabled, False otherwise
135
+ """
136
+ if not is_admin_installed():
137
+ return False
138
+
139
+ # Check if required dependencies are installed
140
+ try:
141
+ from django.conf import settings
142
+
143
+ required_apps = [
144
+ 'django.contrib.auth',
145
+ 'django.contrib.contenttypes',
146
+ 'django.contrib.sessions',
147
+ ]
148
+
149
+ for app in required_apps:
150
+ if app not in settings.INSTALLED_APPS:
151
+ import sys
152
+ print(
153
+ f"[django-bolt] Warning: Django admin is installed but {app} is missing. "
154
+ f"Admin integration disabled.",
155
+ file=sys.stderr
156
+ )
157
+ return False
158
+
159
+ return True
160
+
161
+ except Exception as e:
162
+ import sys
163
+ print(f"[django-bolt] Warning: Could not check admin dependencies: {e}", file=sys.stderr)
164
+ return False
165
+
166
+
167
+ def get_admin_info() -> dict:
168
+ """
169
+ Get information about Django admin configuration.
170
+
171
+ Returns:
172
+ Dict with admin configuration details
173
+ """
174
+ return {
175
+ 'installed': is_admin_installed(),
176
+ 'enabled': should_enable_admin(),
177
+ 'url_prefix': detect_admin_url_prefix(),
178
+ 'static_url': get_static_url_prefix(),
179
+ }
@@ -0,0 +1,267 @@
1
+ """
2
+ ASGI bridge for Django admin integration.
3
+
4
+ Converts django-bolt's PyRequest to ASGI scope and channels,
5
+ allowing Django's ASGI application (with full middleware stack)
6
+ to handle requests for admin routes.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ from typing import Any, Dict, List, Tuple
12
+ from urllib.parse import parse_qs, urlencode
13
+
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def actix_to_asgi_scope(request: Dict[str, Any], server_host: str = "localhost", server_port: int = 8000) -> Dict[str, Any]:
19
+ """
20
+ Convert django-bolt PyRequest dict to ASGI3 scope dict.
21
+
22
+ Args:
23
+ request: PyRequest dict with method, path, headers, body, etc.
24
+ server_host: Server hostname (from settings or command args)
25
+ server_port: Server port (from command args)
26
+
27
+ Returns:
28
+ ASGI3 scope dict compatible with Django's ASGI application
29
+ """
30
+ method = request.get("method", "GET")
31
+ path = request.get("path", "/")
32
+ query_params = request.get("query", {})
33
+ headers_dict = request.get("headers", {})
34
+
35
+ # Build query string from query params dict
36
+ query_string = ""
37
+ if query_params:
38
+ # Convert dict to URL-encoded query string
39
+ query_string = urlencode(sorted(query_params.items()))
40
+
41
+ # Convert headers dict to ASGI headers format: [(b"name", b"value")]
42
+ headers = []
43
+ for name, value in headers_dict.items():
44
+ # Headers are already lowercase from Rust
45
+ headers.append((name.encode("latin1"), value.encode("latin1")))
46
+
47
+ # Add host header if not present
48
+ has_host = any(name == b"host" for name, _ in headers)
49
+ if not has_host:
50
+ headers.append((b"host", f"{server_host}:{server_port}".encode("latin1")))
51
+
52
+ # Determine scheme (http/https) from headers
53
+ scheme = "http"
54
+ forwarded_proto = headers_dict.get("x-forwarded-proto", "")
55
+ if forwarded_proto == "https":
56
+ scheme = "https"
57
+
58
+ # Get client address from headers or use placeholder
59
+ client_host = headers_dict.get("x-forwarded-for", "127.0.0.1")
60
+ if "," in client_host:
61
+ client_host = client_host.split(",")[0].strip()
62
+ client_port = 0 # Unknown client port
63
+
64
+ # Build ASGI3 scope
65
+ scope = {
66
+ "type": "http",
67
+ "asgi": {
68
+ "version": "3.0",
69
+ "spec_version": "2.3",
70
+ },
71
+ "http_version": "1.1",
72
+ "method": method.upper(),
73
+ "scheme": scheme,
74
+ "path": path,
75
+ "raw_path": path.encode("utf-8"),
76
+ "query_string": query_string.encode("latin1"),
77
+ "root_path": "",
78
+ "headers": headers,
79
+ "server": (server_host, server_port),
80
+ "client": (client_host, client_port),
81
+ "extensions": {},
82
+ }
83
+
84
+ return scope
85
+
86
+
87
+ def create_receive_callable(body: bytes):
88
+ """
89
+ Create ASGI3 receive callable that sends request body.
90
+
91
+ Args:
92
+ body: Request body bytes
93
+
94
+ Returns:
95
+ Async callable that implements ASGI receive channel
96
+ """
97
+ sent = False
98
+
99
+ async def receive():
100
+ nonlocal sent
101
+ if not sent:
102
+ sent = True
103
+ return {
104
+ "type": "http.request",
105
+ "body": body,
106
+ "more_body": False,
107
+ }
108
+ # After body is sent, wait forever (never disconnect)
109
+ # This allows Django's ASGI handler to complete the response
110
+ # Django uses asyncio.wait() with FIRST_COMPLETED, racing between
111
+ # listen_for_disconnect() and process_request(). If we return
112
+ # disconnect immediately, it aborts the request.
113
+ await asyncio.Event().wait() # Wait forever
114
+ return {"type": "http.disconnect"}
115
+
116
+ return receive
117
+
118
+
119
+ def create_send_callable(response_holder: Dict[str, Any]):
120
+ """
121
+ Create ASGI3 send callable that collects response.
122
+
123
+ Args:
124
+ response_holder: Dict to populate with status, headers, body
125
+
126
+ Returns:
127
+ Async callable that implements ASGI send channel
128
+ """
129
+ async def send(message: Dict[str, Any]):
130
+ msg_type = message.get("type")
131
+
132
+ if msg_type == "http.response.start":
133
+ # Collect status and headers
134
+ response_holder["status"] = message.get("status", 200)
135
+
136
+ # Convert headers from bytes tuples to string tuples
137
+ headers = []
138
+ for name, value in message.get("headers", []):
139
+ if isinstance(name, bytes):
140
+ name = name.decode("latin1")
141
+ if isinstance(value, bytes):
142
+ value = value.decode("latin1")
143
+ headers.append((name, value))
144
+
145
+ response_holder["headers"] = headers
146
+
147
+ elif msg_type == "http.response.body":
148
+ # Collect body (may be sent in chunks)
149
+ body_chunk = message.get("body", b"")
150
+
151
+ if isinstance(body_chunk, memoryview):
152
+ body_chunk = body_chunk.tobytes()
153
+ elif isinstance(body_chunk, bytearray):
154
+ body_chunk = bytes(body_chunk)
155
+
156
+ if (not body_chunk) and "text" in message:
157
+ text_chunk = message.get("text", "")
158
+ if text_chunk:
159
+ body_chunk = text_chunk.encode("utf-8")
160
+
161
+ if "body" not in response_holder:
162
+ response_holder["body"] = body_chunk
163
+ else:
164
+ response_holder["body"] += body_chunk
165
+
166
+ # Check if more body is coming
167
+ more_body = message.get("more_body", False)
168
+ response_holder["more_body"] = more_body
169
+
170
+ return send
171
+
172
+
173
+ class ASGIFallbackHandler:
174
+ """
175
+ Handler that bridges django-bolt requests to Django's ASGI application.
176
+
177
+ This allows Django admin and other Django views to work with django-bolt
178
+ by converting the request format and applying Django's middleware stack.
179
+ """
180
+
181
+ def __init__(self, server_host: str = "localhost", server_port: int = 8000):
182
+ """
183
+ Initialize ASGI handler.
184
+
185
+ Args:
186
+ server_host: Server hostname for ASGI scope
187
+ server_port: Server port for ASGI scope
188
+ """
189
+ self.server_host = server_host
190
+ self.server_port = server_port
191
+ self._asgi_app = None
192
+
193
+ def _get_asgi_app(self):
194
+ """Lazy-load Django ASGI application with middleware."""
195
+ if self._asgi_app is None:
196
+ from django.core.asgi import get_asgi_application
197
+
198
+ # Ensure Django is configured
199
+ from ..bootstrap import ensure_django_ready
200
+ ensure_django_ready()
201
+
202
+ try:
203
+ self._asgi_app = get_asgi_application()
204
+ except Exception:
205
+ raise
206
+
207
+ return self._asgi_app
208
+
209
+ async def handle_request(self, request: Dict[str, Any]) -> Tuple[int, List[Tuple[str, str]], bytes]:
210
+ """
211
+ Handle request by converting to ASGI and calling Django app.
212
+
213
+ Args:
214
+ request: PyRequest dict from django-bolt
215
+
216
+ Returns:
217
+ Response tuple: (status_code, headers, body)
218
+ """
219
+ # Get Django ASGI app
220
+ asgi_app = self._get_asgi_app()
221
+
222
+ # Convert request to ASGI scope
223
+ scope = actix_to_asgi_scope(request, self.server_host, self.server_port)
224
+
225
+ # Create ASGI receive channel with request body
226
+ body = request.get("body", b"")
227
+ if isinstance(body, list):
228
+ # Body is already bytes from PyRequest
229
+ body = bytes(body)
230
+ receive = create_receive_callable(body)
231
+
232
+ # Create ASGI send channel to collect response
233
+ response_holder = {
234
+ "status": 200,
235
+ "headers": [],
236
+ "body": b"",
237
+ "more_body": False,
238
+ }
239
+ send = create_send_callable(response_holder)
240
+
241
+ # Call Django ASGI application
242
+ try:
243
+ await asgi_app(scope, receive, send)
244
+ except Exception as e:
245
+ # Handle errors by returning 500 response
246
+ import traceback
247
+ error_body = f"ASGI Handler Error: {str(e)}\n\n{traceback.format_exc()}"
248
+ return (
249
+ 500,
250
+ [("content-type", "text/plain; charset=utf-8")],
251
+ error_body.encode("utf-8")
252
+ )
253
+
254
+ # Wait for response body if streaming
255
+ # (In case more_body was True, though Django admin typically doesn't stream)
256
+ max_wait = 100 # Max iterations to wait for body completion
257
+ wait_count = 0
258
+ while response_holder.get("more_body", False) and wait_count < max_wait:
259
+ await asyncio.sleep(0.001) # Small delay to allow body completion
260
+ wait_count += 1
261
+
262
+ # Extract response
263
+ status = response_holder.get("status", 200)
264
+ headers = response_holder.get("headers", [])
265
+ body = response_holder.get("body", b"")
266
+
267
+ return (status, headers, body)