django-bolt 0.1.0__cp310-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.pyd +0 -0
- django_bolt/admin/__init__.py +25 -0
- django_bolt/admin/admin_detection.py +179 -0
- django_bolt/admin/asgi_bridge.py +267 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +155 -0
- django_bolt/admin/static_routes.py +111 -0
- django_bolt/api.py +1011 -0
- django_bolt/apps.py +7 -0
- django_bolt/async_collector.py +228 -0
- django_bolt/auth/README.md +464 -0
- django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
- django_bolt/auth/__init__.py +84 -0
- django_bolt/auth/backends.py +236 -0
- django_bolt/auth/guards.py +224 -0
- django_bolt/auth/jwt_utils.py +212 -0
- django_bolt/auth/revocation.py +286 -0
- django_bolt/auth/token.py +335 -0
- django_bolt/binding.py +363 -0
- django_bolt/bootstrap.py +77 -0
- django_bolt/cli.py +133 -0
- django_bolt/compression.py +104 -0
- django_bolt/decorators.py +159 -0
- django_bolt/dependencies.py +128 -0
- django_bolt/error_handlers.py +305 -0
- django_bolt/exceptions.py +294 -0
- django_bolt/health.py +129 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +357 -0
- django_bolt/logging/middleware.py +296 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +427 -0
- django_bolt/middleware/__init__.py +32 -0
- django_bolt/middleware/compiler.py +131 -0
- django_bolt/middleware/middleware.py +247 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +439 -0
- django_bolt/openapi/routes.py +152 -0
- django_bolt/openapi/schema_generator.py +581 -0
- django_bolt/openapi/spec/__init__.py +68 -0
- django_bolt/openapi/spec/base.py +74 -0
- django_bolt/openapi/spec/callback.py +24 -0
- django_bolt/openapi/spec/components.py +72 -0
- django_bolt/openapi/spec/contact.py +21 -0
- django_bolt/openapi/spec/discriminator.py +25 -0
- django_bolt/openapi/spec/encoding.py +67 -0
- django_bolt/openapi/spec/enums.py +41 -0
- django_bolt/openapi/spec/example.py +36 -0
- django_bolt/openapi/spec/external_documentation.py +21 -0
- django_bolt/openapi/spec/header.py +132 -0
- django_bolt/openapi/spec/info.py +50 -0
- django_bolt/openapi/spec/license.py +28 -0
- django_bolt/openapi/spec/link.py +66 -0
- django_bolt/openapi/spec/media_type.py +51 -0
- django_bolt/openapi/spec/oauth_flow.py +36 -0
- django_bolt/openapi/spec/oauth_flows.py +28 -0
- django_bolt/openapi/spec/open_api.py +87 -0
- django_bolt/openapi/spec/operation.py +105 -0
- django_bolt/openapi/spec/parameter.py +147 -0
- django_bolt/openapi/spec/path_item.py +78 -0
- django_bolt/openapi/spec/paths.py +27 -0
- django_bolt/openapi/spec/reference.py +38 -0
- django_bolt/openapi/spec/request_body.py +38 -0
- django_bolt/openapi/spec/response.py +48 -0
- django_bolt/openapi/spec/responses.py +44 -0
- django_bolt/openapi/spec/schema.py +678 -0
- django_bolt/openapi/spec/security_requirement.py +28 -0
- django_bolt/openapi/spec/security_scheme.py +69 -0
- django_bolt/openapi/spec/server.py +34 -0
- django_bolt/openapi/spec/server_variable.py +32 -0
- django_bolt/openapi/spec/tag.py +32 -0
- django_bolt/openapi/spec/xml.py +44 -0
- django_bolt/pagination.py +669 -0
- django_bolt/param_functions.py +49 -0
- django_bolt/params.py +337 -0
- django_bolt/request_parsing.py +128 -0
- django_bolt/responses.py +214 -0
- django_bolt/router.py +48 -0
- django_bolt/serialization.py +193 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +10 -0
- django_bolt/testing/client.py +274 -0
- django_bolt/testing/helpers.py +93 -0
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +1 -0
- django_bolt/tests/admin_tests/conftest.py +6 -0
- django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
- django_bolt/tests/admin_tests/urls.py +9 -0
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +570 -0
- django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
- django_bolt/tests/cbv/test_class_views_features.py +1173 -0
- django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
- django_bolt/tests/conftest.py +165 -0
- django_bolt/tests/test_action_decorator.py +399 -0
- django_bolt/tests/test_auth_secret_key.py +83 -0
- django_bolt/tests/test_decorator_syntax.py +159 -0
- django_bolt/tests/test_error_handling.py +481 -0
- django_bolt/tests/test_file_response.py +192 -0
- django_bolt/tests/test_global_cors.py +172 -0
- django_bolt/tests/test_guards_auth.py +441 -0
- django_bolt/tests/test_guards_integration.py +303 -0
- django_bolt/tests/test_health.py +283 -0
- django_bolt/tests/test_integration_validation.py +400 -0
- django_bolt/tests/test_json_validation.py +536 -0
- django_bolt/tests/test_jwt_auth.py +327 -0
- django_bolt/tests/test_jwt_token.py +458 -0
- django_bolt/tests/test_logging.py +837 -0
- django_bolt/tests/test_logging_merge.py +419 -0
- django_bolt/tests/test_middleware.py +492 -0
- django_bolt/tests/test_middleware_server.py +230 -0
- django_bolt/tests/test_model_viewset.py +323 -0
- django_bolt/tests/test_models.py +24 -0
- django_bolt/tests/test_pagination.py +1258 -0
- django_bolt/tests/test_parameter_validation.py +178 -0
- django_bolt/tests/test_syntax.py +626 -0
- django_bolt/tests/test_testing_utilities.py +163 -0
- django_bolt/tests/test_testing_utilities_simple.py +123 -0
- django_bolt/tests/test_viewset_unified.py +346 -0
- django_bolt/typing.py +273 -0
- django_bolt/views.py +1110 -0
- django_bolt-0.1.0.dist-info/METADATA +629 -0
- django_bolt-0.1.0.dist-info/RECORD +128 -0
- django_bolt-0.1.0.dist-info/WHEEL +4 -0
- django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
django_bolt/__init__.py
ADDED
|
@@ -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
|
+
|
django_bolt/_core.pyd
ADDED
|
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)
|