django-bolt 0.4.2__cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.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.
- django_bolt/__init__.py +256 -0
- django_bolt/_core.abi3.so +0 -0
- django_bolt/_json.py +174 -0
- django_bolt/admin/__init__.py +24 -0
- django_bolt/admin/admin_detection.py +182 -0
- django_bolt/admin/asgi_bridge.py +274 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +152 -0
- django_bolt/admin/static_routes.py +113 -0
- django_bolt/analysis.py +412 -0
- django_bolt/api.py +2191 -0
- django_bolt/apps.py +7 -0
- django_bolt/auth/__init__.py +96 -0
- django_bolt/auth/anonymous.py +27 -0
- django_bolt/auth/backends.py +357 -0
- django_bolt/auth/guards.py +225 -0
- django_bolt/auth/jwt_utils.py +217 -0
- django_bolt/auth/revocation.py +284 -0
- django_bolt/auth/token.py +329 -0
- django_bolt/auth/user_loader.py +139 -0
- django_bolt/binding.py +554 -0
- django_bolt/bootstrap.py +84 -0
- django_bolt/cli.py +141 -0
- django_bolt/concurrency.py +61 -0
- django_bolt/decorators.py +172 -0
- django_bolt/dependencies.py +176 -0
- django_bolt/error_handlers.py +387 -0
- django_bolt/exceptions.py +295 -0
- django_bolt/health.py +136 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +360 -0
- django_bolt/logging/middleware.py +298 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +521 -0
- django_bolt/middleware/__init__.py +101 -0
- django_bolt/middleware/compiler.py +188 -0
- django_bolt/middleware/compression.py +70 -0
- django_bolt/middleware/django_adapter.py +964 -0
- django_bolt/middleware/django_loader.py +147 -0
- django_bolt/middleware/middleware.py +587 -0
- django_bolt/middleware_response.py +53 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +449 -0
- django_bolt/openapi/routes.py +168 -0
- django_bolt/openapi/schema_generator.py +799 -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 +103 -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 +680 -0
- django_bolt/openapi/spec/security_requirement.py +27 -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 +63 -0
- django_bolt/params.py +338 -0
- django_bolt/request.py +87 -0
- django_bolt/request_parsing.py +134 -0
- django_bolt/responses.py +245 -0
- django_bolt/router.py +218 -0
- django_bolt/serialization.py +379 -0
- django_bolt/serializers/__init__.py +144 -0
- django_bolt/serializers/base.py +1607 -0
- django_bolt/serializers/decorators.py +263 -0
- django_bolt/serializers/fields.py +328 -0
- django_bolt/serializers/helpers.py +168 -0
- django_bolt/serializers/nested.py +255 -0
- django_bolt/serializers/types.py +540 -0
- django_bolt/shortcuts.py +14 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +34 -0
- django_bolt/testing/client.py +398 -0
- django_bolt/testing/helpers.py +50 -0
- django_bolt/testing/websocket.py +508 -0
- django_bolt/types.py +591 -0
- django_bolt/typing.py +429 -0
- django_bolt/views.py +1116 -0
- django_bolt/websocket/__init__.py +78 -0
- django_bolt/websocket/close_codes.py +35 -0
- django_bolt/websocket/exceptions.py +28 -0
- django_bolt/websocket/handlers.py +140 -0
- django_bolt/websocket/state.py +13 -0
- django_bolt/websocket/types.py +231 -0
- django_bolt-0.4.2.dist-info/METADATA +618 -0
- django_bolt-0.4.2.dist-info/RECORD +114 -0
- django_bolt-0.4.2.dist-info/WHEEL +6 -0
- django_bolt-0.4.2.dist-info/entry_points.txt +2 -0
django_bolt/__init__.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-Bolt: High-performance API framework for Django.
|
|
3
|
+
|
|
4
|
+
Provides Rust-powered API endpoints with 60k+ RPS performance, integrating
|
|
5
|
+
with existing Django projects via Actix Web, PyO3, and msgspec.
|
|
6
|
+
|
|
7
|
+
Quick Start:
|
|
8
|
+
from django_bolt import BoltAPI, Request
|
|
9
|
+
|
|
10
|
+
api = BoltAPI()
|
|
11
|
+
|
|
12
|
+
@api.get("/hello")
|
|
13
|
+
async def hello(request: Request) -> dict:
|
|
14
|
+
return {"message": "Hello, World!"}
|
|
15
|
+
|
|
16
|
+
Type-Safe Requests:
|
|
17
|
+
from django_bolt import BoltAPI, Request
|
|
18
|
+
from django_bolt.types import JWTClaims
|
|
19
|
+
from myapp.models import User
|
|
20
|
+
|
|
21
|
+
api = BoltAPI()
|
|
22
|
+
|
|
23
|
+
@api.get("/profile", guards=[IsAuthenticated()])
|
|
24
|
+
async def profile(request: Request[User, JWTClaims, dict]) -> dict:
|
|
25
|
+
return {"email": request.user.email} # IDE knows User has email
|
|
26
|
+
|
|
27
|
+
Middleware:
|
|
28
|
+
from django_bolt import BoltAPI
|
|
29
|
+
from django_bolt.middleware import (
|
|
30
|
+
DjangoMiddleware,
|
|
31
|
+
TimingMiddleware,
|
|
32
|
+
LoggingMiddleware,
|
|
33
|
+
)
|
|
34
|
+
from django.contrib.sessions.middleware import SessionMiddleware
|
|
35
|
+
from django.contrib.auth.middleware import AuthenticationMiddleware
|
|
36
|
+
|
|
37
|
+
api = BoltAPI(
|
|
38
|
+
middleware=[
|
|
39
|
+
DjangoMiddleware(SessionMiddleware),
|
|
40
|
+
DjangoMiddleware(AuthenticationMiddleware),
|
|
41
|
+
TimingMiddleware(),
|
|
42
|
+
LoggingMiddleware(),
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from .api import BoltAPI
|
|
48
|
+
|
|
49
|
+
# Auth module
|
|
50
|
+
from .auth import (
|
|
51
|
+
# Guards/Permissions
|
|
52
|
+
AllowAny,
|
|
53
|
+
APIKeyAuthentication,
|
|
54
|
+
AuthContext,
|
|
55
|
+
HasAllPermissions,
|
|
56
|
+
HasAnyPermission,
|
|
57
|
+
HasPermission,
|
|
58
|
+
IsAdminUser,
|
|
59
|
+
IsAuthenticated,
|
|
60
|
+
IsStaff,
|
|
61
|
+
# Authentication backends
|
|
62
|
+
JWTAuthentication,
|
|
63
|
+
SessionAuthentication,
|
|
64
|
+
# JWT Token & Utilities
|
|
65
|
+
Token,
|
|
66
|
+
create_jwt_for_user,
|
|
67
|
+
extract_user_id_from_context,
|
|
68
|
+
get_auth_context,
|
|
69
|
+
get_current_user,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Decorators module
|
|
73
|
+
from .decorators import action
|
|
74
|
+
|
|
75
|
+
# Middleware module
|
|
76
|
+
from .middleware import (
|
|
77
|
+
BaseMiddleware,
|
|
78
|
+
CompressionConfig,
|
|
79
|
+
# Django compatibility
|
|
80
|
+
DjangoMiddleware,
|
|
81
|
+
ErrorHandlerMiddleware,
|
|
82
|
+
LoggingMiddleware,
|
|
83
|
+
Middleware,
|
|
84
|
+
# Protocols and base classes
|
|
85
|
+
MiddlewareProtocol,
|
|
86
|
+
# Built-in middleware (Python)
|
|
87
|
+
TimingMiddleware,
|
|
88
|
+
cors,
|
|
89
|
+
# Decorators
|
|
90
|
+
middleware,
|
|
91
|
+
no_compress,
|
|
92
|
+
rate_limit,
|
|
93
|
+
skip_middleware,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# OpenAPI module
|
|
97
|
+
from .openapi import (
|
|
98
|
+
JsonRenderPlugin,
|
|
99
|
+
OpenAPIConfig,
|
|
100
|
+
RapidocRenderPlugin,
|
|
101
|
+
RedocRenderPlugin,
|
|
102
|
+
ScalarRenderPlugin,
|
|
103
|
+
StoplightRenderPlugin,
|
|
104
|
+
SwaggerRenderPlugin,
|
|
105
|
+
YamlRenderPlugin,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Pagination module
|
|
109
|
+
from .pagination import (
|
|
110
|
+
CursorPagination,
|
|
111
|
+
LimitOffsetPagination,
|
|
112
|
+
PageNumberPagination,
|
|
113
|
+
PaginatedResponse,
|
|
114
|
+
PaginationBase,
|
|
115
|
+
paginate,
|
|
116
|
+
)
|
|
117
|
+
from .params import Depends
|
|
118
|
+
|
|
119
|
+
# Type-safe Request object
|
|
120
|
+
from .request import Request
|
|
121
|
+
from .responses import JSON, Response, StreamingResponse
|
|
122
|
+
from .router import Router
|
|
123
|
+
from .types import (
|
|
124
|
+
APIKeyAuth,
|
|
125
|
+
DjangoModel,
|
|
126
|
+
JWTClaims,
|
|
127
|
+
SessionAuth,
|
|
128
|
+
TimingState,
|
|
129
|
+
TracingState,
|
|
130
|
+
UserType,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Types and protocols
|
|
134
|
+
from .types import (
|
|
135
|
+
Request as RequestProtocol, # Protocol for type checking
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Views module
|
|
139
|
+
from .views import (
|
|
140
|
+
APIView,
|
|
141
|
+
CreateMixin,
|
|
142
|
+
DestroyMixin,
|
|
143
|
+
ListMixin,
|
|
144
|
+
ModelViewSet,
|
|
145
|
+
PartialUpdateMixin,
|
|
146
|
+
ReadOnlyModelViewSet,
|
|
147
|
+
RetrieveMixin,
|
|
148
|
+
UpdateMixin,
|
|
149
|
+
ViewSet,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# WebSocket module
|
|
153
|
+
from .websocket import (
|
|
154
|
+
CloseCode,
|
|
155
|
+
WebSocket,
|
|
156
|
+
WebSocketClose,
|
|
157
|
+
WebSocketDisconnect,
|
|
158
|
+
WebSocketException,
|
|
159
|
+
WebSocketState,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
__all__ = [
|
|
163
|
+
# Core
|
|
164
|
+
"BoltAPI",
|
|
165
|
+
"Request",
|
|
166
|
+
"Response",
|
|
167
|
+
"JSON",
|
|
168
|
+
"StreamingResponse",
|
|
169
|
+
"CompressionConfig",
|
|
170
|
+
"Depends",
|
|
171
|
+
# Router
|
|
172
|
+
"Router",
|
|
173
|
+
# Types
|
|
174
|
+
"RequestProtocol",
|
|
175
|
+
"UserType",
|
|
176
|
+
"AuthContext",
|
|
177
|
+
"DjangoModel",
|
|
178
|
+
"JWTClaims",
|
|
179
|
+
"APIKeyAuth",
|
|
180
|
+
"SessionAuth",
|
|
181
|
+
"TimingState",
|
|
182
|
+
"TracingState",
|
|
183
|
+
# Views
|
|
184
|
+
"APIView",
|
|
185
|
+
"ViewSet",
|
|
186
|
+
"ModelViewSet",
|
|
187
|
+
"ReadOnlyModelViewSet",
|
|
188
|
+
"ListMixin",
|
|
189
|
+
"RetrieveMixin",
|
|
190
|
+
"CreateMixin",
|
|
191
|
+
"UpdateMixin",
|
|
192
|
+
"PartialUpdateMixin",
|
|
193
|
+
"DestroyMixin",
|
|
194
|
+
# Pagination
|
|
195
|
+
"PaginationBase",
|
|
196
|
+
"PageNumberPagination",
|
|
197
|
+
"LimitOffsetPagination",
|
|
198
|
+
"CursorPagination",
|
|
199
|
+
"PaginatedResponse",
|
|
200
|
+
"paginate",
|
|
201
|
+
# Decorators
|
|
202
|
+
"action",
|
|
203
|
+
# Auth - Authentication
|
|
204
|
+
"JWTAuthentication",
|
|
205
|
+
"APIKeyAuthentication",
|
|
206
|
+
"SessionAuthentication",
|
|
207
|
+
"AuthContext",
|
|
208
|
+
# Auth - Guards/Permissions
|
|
209
|
+
"AllowAny",
|
|
210
|
+
"IsAuthenticated",
|
|
211
|
+
"IsAdminUser",
|
|
212
|
+
"IsStaff",
|
|
213
|
+
"HasPermission",
|
|
214
|
+
"HasAnyPermission",
|
|
215
|
+
"HasAllPermissions",
|
|
216
|
+
# Middleware - Protocols and base classes
|
|
217
|
+
"MiddlewareProtocol",
|
|
218
|
+
"BaseMiddleware",
|
|
219
|
+
"Middleware",
|
|
220
|
+
# Middleware - Decorators
|
|
221
|
+
"middleware",
|
|
222
|
+
"rate_limit",
|
|
223
|
+
"cors",
|
|
224
|
+
"skip_middleware",
|
|
225
|
+
"no_compress",
|
|
226
|
+
# Middleware - Built-in (Python)
|
|
227
|
+
"TimingMiddleware",
|
|
228
|
+
"LoggingMiddleware",
|
|
229
|
+
"ErrorHandlerMiddleware",
|
|
230
|
+
# Middleware - Django compatibility
|
|
231
|
+
"DjangoMiddleware",
|
|
232
|
+
# Auth - JWT Token & Utilities
|
|
233
|
+
"Token",
|
|
234
|
+
"create_jwt_for_user",
|
|
235
|
+
"get_current_user",
|
|
236
|
+
"extract_user_id_from_context",
|
|
237
|
+
"get_auth_context",
|
|
238
|
+
# OpenAPI
|
|
239
|
+
"OpenAPIConfig",
|
|
240
|
+
"SwaggerRenderPlugin",
|
|
241
|
+
"RedocRenderPlugin",
|
|
242
|
+
"ScalarRenderPlugin",
|
|
243
|
+
"RapidocRenderPlugin",
|
|
244
|
+
"StoplightRenderPlugin",
|
|
245
|
+
"JsonRenderPlugin",
|
|
246
|
+
"YamlRenderPlugin",
|
|
247
|
+
# WebSocket
|
|
248
|
+
"WebSocket",
|
|
249
|
+
"WebSocketState",
|
|
250
|
+
"WebSocketDisconnect",
|
|
251
|
+
"WebSocketClose",
|
|
252
|
+
"WebSocketException",
|
|
253
|
+
"CloseCode",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
default_app_config = 'django_bolt.apps.DjangoBoltConfig'
|
|
Binary file
|
django_bolt/_json.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Fast JSON helpers backed by cached msgspec Encoder/Decoder.
|
|
2
|
+
|
|
3
|
+
This module provides optimized JSON encoding/decoding using msgspec with:
|
|
4
|
+
- Thread-local cached encoder/decoder instances (thread-safe buffer reuse)
|
|
5
|
+
- Support for common non-JSON-native types (datetime, Path, Decimal, UUID, IP addresses)
|
|
6
|
+
- Automatic Serializer.dump() for write_only, computed_field support
|
|
7
|
+
- Type-safe decoding with validation
|
|
8
|
+
- Custom encoder/decoder hooks
|
|
9
|
+
|
|
10
|
+
Inspired by Litestar's serialization approach.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import threading
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from datetime import date, datetime, time
|
|
18
|
+
from decimal import Decimal
|
|
19
|
+
from ipaddress import (
|
|
20
|
+
IPv4Address,
|
|
21
|
+
IPv4Interface,
|
|
22
|
+
IPv4Network,
|
|
23
|
+
IPv6Address,
|
|
24
|
+
IPv6Interface,
|
|
25
|
+
IPv6Network,
|
|
26
|
+
)
|
|
27
|
+
from pathlib import Path, PurePath
|
|
28
|
+
from typing import Any, TypeVar
|
|
29
|
+
from uuid import UUID
|
|
30
|
+
|
|
31
|
+
import msgspec
|
|
32
|
+
|
|
33
|
+
T = TypeVar("T")
|
|
34
|
+
|
|
35
|
+
# Thread-local storage for encoder/decoder instances
|
|
36
|
+
_thread_local = threading.local()
|
|
37
|
+
|
|
38
|
+
# Default type encoders for non-JSON-native types
|
|
39
|
+
# Maps type -> encoder function
|
|
40
|
+
DEFAULT_TYPE_ENCODERS: dict[type, Callable[[Any], Any]] = {
|
|
41
|
+
# Paths
|
|
42
|
+
Path: str,
|
|
43
|
+
PurePath: str,
|
|
44
|
+
# Dates/Times -> ISO format
|
|
45
|
+
datetime: lambda v: v.isoformat(),
|
|
46
|
+
date: lambda v: v.isoformat(),
|
|
47
|
+
time: lambda v: v.isoformat(),
|
|
48
|
+
# Decimals -> int or float
|
|
49
|
+
Decimal: lambda v: int(v) if v.as_tuple().exponent >= 0 else float(v),
|
|
50
|
+
# IP addresses
|
|
51
|
+
IPv4Address: str,
|
|
52
|
+
IPv4Interface: str,
|
|
53
|
+
IPv4Network: str,
|
|
54
|
+
IPv6Address: str,
|
|
55
|
+
IPv6Interface: str,
|
|
56
|
+
IPv6Network: str,
|
|
57
|
+
# UUID
|
|
58
|
+
UUID: str,
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def default_serializer(value: Any) -> Any:
|
|
64
|
+
"""Transform values non-natively supported by msgspec.
|
|
65
|
+
|
|
66
|
+
Walks the MRO (Method Resolution Order) to support subclasses.
|
|
67
|
+
Raises TypeError if type is unsupported.
|
|
68
|
+
|
|
69
|
+
Note: Serializer instances are handled in serialization.py before reaching
|
|
70
|
+
this hook, since msgspec.Struct is natively supported by msgspec.
|
|
71
|
+
"""
|
|
72
|
+
# Walk MRO to support polymorphic types
|
|
73
|
+
for base in value.__class__.__mro__[:-1]: # Skip 'object'
|
|
74
|
+
encoder = DEFAULT_TYPE_ENCODERS.get(base)
|
|
75
|
+
if encoder is not None:
|
|
76
|
+
return encoder(value)
|
|
77
|
+
|
|
78
|
+
raise TypeError(f"Unsupported type: {type(value)!r}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_encoder() -> msgspec.json.Encoder:
|
|
82
|
+
"""Return a thread-local msgspec JSON Encoder instance.
|
|
83
|
+
|
|
84
|
+
Using a per-thread encoder is thread-safe and avoids cross-thread contention
|
|
85
|
+
while still reusing the internal buffer for repeated encodes on the same thread.
|
|
86
|
+
"""
|
|
87
|
+
encoder = getattr(_thread_local, "encoder", None)
|
|
88
|
+
if encoder is None:
|
|
89
|
+
encoder = msgspec.json.Encoder(enc_hook=default_serializer)
|
|
90
|
+
_thread_local.encoder = encoder
|
|
91
|
+
return encoder
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_decoder() -> msgspec.json.Decoder:
|
|
95
|
+
"""Return a thread-local msgspec JSON Decoder instance.
|
|
96
|
+
|
|
97
|
+
Using a per-thread decoder is thread-safe and reuses the internal buffer.
|
|
98
|
+
"""
|
|
99
|
+
decoder = getattr(_thread_local, "decoder", None)
|
|
100
|
+
if decoder is None:
|
|
101
|
+
decoder = msgspec.json.Decoder()
|
|
102
|
+
_thread_local.decoder = decoder
|
|
103
|
+
return decoder
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def encode(value: Any, serializer: Callable[[Any], Any] | None = None) -> bytes:
|
|
107
|
+
"""Encode a Python object to JSON bytes.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
value: Object to encode
|
|
111
|
+
serializer: Optional custom encoder hook (overrides default)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
JSON bytes
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
TypeError: If value contains unsupported types
|
|
118
|
+
msgspec.EncodeError: If encoding fails
|
|
119
|
+
"""
|
|
120
|
+
if serializer is not None:
|
|
121
|
+
# Custom serializer provided - use one-off encoder
|
|
122
|
+
return msgspec.json.encode(value, enc_hook=serializer)
|
|
123
|
+
|
|
124
|
+
# Use thread-local cached encoder with default serializer
|
|
125
|
+
return _get_encoder().encode(value)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def decode(value: bytes | str) -> Any:
|
|
129
|
+
"""Decode JSON bytes/string to Python object.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
value: JSON bytes or string
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Decoded Python object
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
msgspec.DecodeError: If decoding fails
|
|
139
|
+
"""
|
|
140
|
+
return _get_decoder().decode(value)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def decode_typed[T](
|
|
144
|
+
value: bytes | str,
|
|
145
|
+
target_type: type[T],
|
|
146
|
+
strict: bool = True,
|
|
147
|
+
) -> T:
|
|
148
|
+
"""Decode JSON with type validation and coercion.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
value: JSON bytes or string
|
|
152
|
+
target_type: Expected type (e.g., msgspec.Struct subclass)
|
|
153
|
+
strict: If False, enables lenient type coercion (e.g., "123" -> 123)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Decoded and validated object of target_type
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
msgspec.DecodeError: If decoding or validation fails
|
|
160
|
+
msgspec.ValidationError: If type validation fails
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
>>> class User(msgspec.Struct):
|
|
164
|
+
... id: int
|
|
165
|
+
... name: str
|
|
166
|
+
>>> decode_typed(b'{"id": 1, "name": "Alice"}', User)
|
|
167
|
+
User(id=1, name='Alice')
|
|
168
|
+
"""
|
|
169
|
+
return msgspec.json.decode(value, type=target_type, strict=strict)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = ["encode", "decode", "decode_typed", "DEFAULT_TYPE_ENCODERS", "default_serializer"]
|
|
173
|
+
|
|
174
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
detect_admin_url_prefix,
|
|
10
|
+
get_admin_info,
|
|
11
|
+
get_admin_route_patterns,
|
|
12
|
+
is_admin_installed,
|
|
13
|
+
should_enable_admin,
|
|
14
|
+
)
|
|
15
|
+
from .asgi_bridge import ASGIFallbackHandler
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"is_admin_installed",
|
|
19
|
+
"detect_admin_url_prefix",
|
|
20
|
+
"get_admin_route_patterns",
|
|
21
|
+
"should_enable_admin",
|
|
22
|
+
"get_admin_info",
|
|
23
|
+
"ASGIFallbackHandler",
|
|
24
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for detecting and configuring Django admin integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.urls import get_resolver
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_admin_installed() -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Check if Django admin is installed and configured.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
True if admin is in INSTALLED_APPS, False otherwise
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
return 'django.contrib.admin' in settings.INSTALLED_APPS
|
|
23
|
+
except Exception:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect_admin_url_prefix() -> str | None:
|
|
28
|
+
"""
|
|
29
|
+
Detect the URL prefix for Django admin by parsing urlpatterns.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Admin URL prefix (e.g., 'admin' or 'dashboard') or None if not found
|
|
33
|
+
"""
|
|
34
|
+
if not is_admin_installed():
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
# Get root URL resolver
|
|
39
|
+
resolver = get_resolver(getattr(settings, 'ROOT_URLCONF', None))
|
|
40
|
+
|
|
41
|
+
# Search for admin patterns
|
|
42
|
+
for url_pattern in resolver.url_patterns:
|
|
43
|
+
# Check if this is admin.site.urls
|
|
44
|
+
if hasattr(url_pattern, 'app_name') and url_pattern.app_name == 'admin':
|
|
45
|
+
# Extract the pattern prefix
|
|
46
|
+
pattern_str = str(url_pattern.pattern)
|
|
47
|
+
# Remove trailing slash and special regex chars
|
|
48
|
+
prefix = pattern_str.rstrip('/^$')
|
|
49
|
+
return prefix if prefix else 'admin'
|
|
50
|
+
|
|
51
|
+
# Also check URLResolver with admin urlconf
|
|
52
|
+
if hasattr(url_pattern, 'urlconf_name'):
|
|
53
|
+
urlconf = url_pattern.urlconf_name
|
|
54
|
+
# Check if urlconf module contains admin.site
|
|
55
|
+
if hasattr(urlconf, '__name__') and 'admin' in str(urlconf.__name__):
|
|
56
|
+
pattern_str = str(url_pattern.pattern)
|
|
57
|
+
prefix = pattern_str.rstrip('/^$')
|
|
58
|
+
return prefix if prefix else 'admin'
|
|
59
|
+
|
|
60
|
+
# Check if urlconf is a list containing admin patterns
|
|
61
|
+
if isinstance(urlconf, (list, tuple)):
|
|
62
|
+
for sub_pattern in urlconf:
|
|
63
|
+
if hasattr(sub_pattern, 'callback') and hasattr(sub_pattern.callback, '__module__') and '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
|
+
print(f"[django-bolt] Warning: Could not auto-detect admin URL prefix: {e}", file=sys.stderr)
|
|
71
|
+
|
|
72
|
+
# Default fallback
|
|
73
|
+
return 'admin'
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_admin_route_patterns() -> list[tuple[str, list[str]]]:
|
|
77
|
+
"""
|
|
78
|
+
Get route patterns to register for Django admin.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of (path_pattern, methods) tuples for admin routes
|
|
82
|
+
Example: [('/admin/{path:path}', ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])]
|
|
83
|
+
"""
|
|
84
|
+
if not is_admin_installed():
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
# Detect admin URL prefix
|
|
88
|
+
admin_prefix = detect_admin_url_prefix()
|
|
89
|
+
if not admin_prefix:
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
# Build catch-all pattern for admin routes
|
|
93
|
+
# Use {path:path} syntax for catch-all parameter
|
|
94
|
+
admin_pattern = f'/{admin_prefix}/{{path:path}}'
|
|
95
|
+
|
|
96
|
+
# Admin needs to handle common HTTP methods
|
|
97
|
+
# Only use methods supported by django-bolt's router (GET, POST, PUT, PATCH, DELETE)
|
|
98
|
+
methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
|
|
99
|
+
|
|
100
|
+
# Also add exact /admin route (without trailing slash)
|
|
101
|
+
# NOTE: NormalizePath::trim() in Rust strips trailing slashes from incoming requests,
|
|
102
|
+
# so we register routes WITHOUT trailing slashes to match the normalized paths.
|
|
103
|
+
# However, for TestClient with use_http_layer=True, we also need the trailing slash version.
|
|
104
|
+
admin_root = f'/{admin_prefix}'
|
|
105
|
+
admin_root_slash = f'/{admin_prefix}/'
|
|
106
|
+
|
|
107
|
+
return [
|
|
108
|
+
(admin_root, methods),
|
|
109
|
+
(admin_root_slash, methods),
|
|
110
|
+
(admin_pattern, methods),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_static_url_prefix() -> str | None:
|
|
115
|
+
"""
|
|
116
|
+
Get the STATIC_URL prefix from Django settings.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Static URL prefix (e.g., 'static') or None if not configured
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
if hasattr(settings, 'STATIC_URL') and settings.STATIC_URL:
|
|
123
|
+
static_url = settings.STATIC_URL
|
|
124
|
+
# Remove leading/trailing slashes
|
|
125
|
+
return static_url.strip('/')
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"Failed to get STATIC_URL from Django settings. "
|
|
129
|
+
"Static file serving may not work correctly. Error: %s",
|
|
130
|
+
e
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def should_enable_admin() -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Determine if admin should be auto-enabled.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if admin is installed and can be enabled, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
if not is_admin_installed():
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Check if required dependencies are installed
|
|
147
|
+
try:
|
|
148
|
+
required_apps = [
|
|
149
|
+
'django.contrib.auth',
|
|
150
|
+
'django.contrib.contenttypes',
|
|
151
|
+
'django.contrib.sessions',
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
for app in required_apps:
|
|
155
|
+
if app not in settings.INSTALLED_APPS:
|
|
156
|
+
print(
|
|
157
|
+
f"[django-bolt] Warning: Django admin is installed but {app} is missing. "
|
|
158
|
+
f"Admin integration disabled.",
|
|
159
|
+
file=sys.stderr
|
|
160
|
+
)
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print(f"[django-bolt] Warning: Could not check admin dependencies: {e}", file=sys.stderr)
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_admin_info() -> dict:
|
|
171
|
+
"""
|
|
172
|
+
Get information about Django admin configuration.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dict with admin configuration details
|
|
176
|
+
"""
|
|
177
|
+
return {
|
|
178
|
+
'installed': is_admin_installed(),
|
|
179
|
+
'enabled': should_enable_admin(),
|
|
180
|
+
'url_prefix': detect_admin_url_prefix(),
|
|
181
|
+
'static_url': get_static_url_prefix(),
|
|
182
|
+
}
|