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.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.abi3.so +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/apps.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-performance async to sync collector for streaming.
|
|
3
|
+
Implements proven patterns for efficient async iterator batching and PyO3 boundary optimization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from typing import AsyncIterable, AsyncIterator, Iterator, List, Optional, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
X = TypeVar("X")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def batch_async(
|
|
14
|
+
aib: AsyncIterable[X],
|
|
15
|
+
timeout: timedelta,
|
|
16
|
+
batch_size: int,
|
|
17
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
18
|
+
) -> Iterator[List[X]]:
|
|
19
|
+
"""
|
|
20
|
+
Batch an async iterable synchronously without timeouts for minimal latency.
|
|
21
|
+
|
|
22
|
+
Optimized for low-latency streaming:
|
|
23
|
+
- No timeout overhead for immediate async generators
|
|
24
|
+
- Direct async iteration for maximum speed
|
|
25
|
+
- Simplified control flow
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
aib: The underlying source async iterable of items
|
|
29
|
+
timeout: Ignored in this optimized version
|
|
30
|
+
batch_size: Maximum number of items to yield in a batch
|
|
31
|
+
loop: Custom asyncio run loop to use, if any
|
|
32
|
+
|
|
33
|
+
Yields:
|
|
34
|
+
The next gathered batch of items
|
|
35
|
+
"""
|
|
36
|
+
# Ensure that we have the stateful iterator of the source
|
|
37
|
+
ait = aib.__aiter__()
|
|
38
|
+
|
|
39
|
+
loop = loop if loop is not None else asyncio.new_event_loop()
|
|
40
|
+
|
|
41
|
+
async def get_next_batch():
|
|
42
|
+
batch = []
|
|
43
|
+
# Gather items greedily up to batch_size
|
|
44
|
+
# This is optimal for small iterators (SSE with 3-5 chunks)
|
|
45
|
+
for _ in range(batch_size):
|
|
46
|
+
try:
|
|
47
|
+
next_item = await ait.__anext__()
|
|
48
|
+
batch.append(next_item)
|
|
49
|
+
except StopAsyncIteration:
|
|
50
|
+
# End of iterator
|
|
51
|
+
break
|
|
52
|
+
return batch
|
|
53
|
+
|
|
54
|
+
while True:
|
|
55
|
+
# Direct execution without timeout wrapper for minimal overhead
|
|
56
|
+
batch = loop.run_until_complete(get_next_batch())
|
|
57
|
+
if not batch:
|
|
58
|
+
return
|
|
59
|
+
yield batch
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AsyncToSyncCollector:
|
|
63
|
+
"""
|
|
64
|
+
High-performance async stream collector for Rust integration.
|
|
65
|
+
|
|
66
|
+
Optimized for streaming with efficient batching to minimize PyO3 boundary
|
|
67
|
+
crossings while maintaining good throughput and latency characteristics.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
async_iterable: AsyncIterable,
|
|
73
|
+
batch_size: int = 50, # Optimized for OpenAI streaming
|
|
74
|
+
timeout_ms: int = 10, # Low latency: 10ms timeout
|
|
75
|
+
convert_to_bytes: bool = True
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Initialize the collector.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
async_iterable: The async iterable to collect from
|
|
82
|
+
batch_size: Number of chunks to batch together
|
|
83
|
+
timeout_ms: Maximum time to wait for a full batch (milliseconds)
|
|
84
|
+
convert_to_bytes: Whether to convert items to bytes
|
|
85
|
+
"""
|
|
86
|
+
# Assume it's an AsyncIterable and let batch_async call __aiter__() on it
|
|
87
|
+
self.async_gen = async_iterable
|
|
88
|
+
|
|
89
|
+
self.batch_size = batch_size
|
|
90
|
+
self.timeout = timedelta(milliseconds=timeout_ms)
|
|
91
|
+
self.convert_to_bytes = convert_to_bytes
|
|
92
|
+
self._iterator = None
|
|
93
|
+
self._loop = None
|
|
94
|
+
|
|
95
|
+
def _convert_to_bytes(self, item) -> bytes:
|
|
96
|
+
"""Convert various types to bytes for streaming."""
|
|
97
|
+
if isinstance(item, bytes):
|
|
98
|
+
return item
|
|
99
|
+
elif isinstance(item, bytearray):
|
|
100
|
+
return bytes(item)
|
|
101
|
+
elif isinstance(item, memoryview):
|
|
102
|
+
return bytes(item)
|
|
103
|
+
elif isinstance(item, str):
|
|
104
|
+
return item.encode('utf-8')
|
|
105
|
+
else:
|
|
106
|
+
return str(item).encode('utf-8')
|
|
107
|
+
|
|
108
|
+
def __iter__(self):
|
|
109
|
+
"""Initialize the iterator."""
|
|
110
|
+
# Try to use existing event loop, create new one only if needed
|
|
111
|
+
try:
|
|
112
|
+
# Try to get the running event loop first
|
|
113
|
+
self._loop = asyncio.get_running_loop()
|
|
114
|
+
except RuntimeError:
|
|
115
|
+
# No event loop running, create a new one
|
|
116
|
+
self._loop = asyncio.new_event_loop()
|
|
117
|
+
|
|
118
|
+
# Get the batch iterator
|
|
119
|
+
self._iterator = batch_async(
|
|
120
|
+
self.async_gen,
|
|
121
|
+
self.timeout,
|
|
122
|
+
self.batch_size,
|
|
123
|
+
self._loop
|
|
124
|
+
)
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __next__(self) -> bytes:
|
|
128
|
+
"""Get the next batch of chunks as a single bytes object."""
|
|
129
|
+
# Initialize iterator if not already done (Rust calls __next__ without __iter__)
|
|
130
|
+
if self._iterator is None:
|
|
131
|
+
self.__iter__()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
batch = next(self._iterator)
|
|
135
|
+
|
|
136
|
+
# Empty batch means we're done
|
|
137
|
+
if not batch:
|
|
138
|
+
if self._loop:
|
|
139
|
+
self._loop.close()
|
|
140
|
+
self._loop = None
|
|
141
|
+
raise StopIteration
|
|
142
|
+
|
|
143
|
+
if self.convert_to_bytes:
|
|
144
|
+
# Convert and join all chunks into a single bytes object
|
|
145
|
+
# This is critical for performance - one PyO3 crossing instead of many
|
|
146
|
+
converted = [self._convert_to_bytes(item) for item in batch]
|
|
147
|
+
return b''.join(converted)
|
|
148
|
+
else:
|
|
149
|
+
# Return raw batch if conversion not needed
|
|
150
|
+
return batch
|
|
151
|
+
|
|
152
|
+
except StopIteration:
|
|
153
|
+
# Clean up the event loop
|
|
154
|
+
if self._loop:
|
|
155
|
+
self._loop.close()
|
|
156
|
+
self._loop = None
|
|
157
|
+
raise
|
|
158
|
+
except Exception as e:
|
|
159
|
+
# Clean up on any error
|
|
160
|
+
if self._loop:
|
|
161
|
+
self._loop.close()
|
|
162
|
+
self._loop = None
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
def __del__(self):
|
|
166
|
+
"""Cleanup event loop on deletion."""
|
|
167
|
+
if hasattr(self, '_loop') and self._loop:
|
|
168
|
+
try:
|
|
169
|
+
if not self._loop.is_closed():
|
|
170
|
+
self._loop.close()
|
|
171
|
+
except:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def wrap_async_stream(
|
|
176
|
+
async_gen: AsyncIterable,
|
|
177
|
+
batch_size: int = 50,
|
|
178
|
+
timeout_ms: int = 10
|
|
179
|
+
) -> Iterator[bytes]:
|
|
180
|
+
"""
|
|
181
|
+
Convenience function to wrap an async generator for sync iteration.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
async_gen: The async generator to wrap
|
|
185
|
+
batch_size: Number of chunks to batch together
|
|
186
|
+
timeout_ms: Maximum time to wait for a full batch (milliseconds)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
A sync iterator that yields batched bytes
|
|
190
|
+
"""
|
|
191
|
+
return AsyncToSyncCollector(async_gen, batch_size, timeout_ms)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Performance-tuned configurations for different streaming scenarios
|
|
195
|
+
class StreamProfiles:
|
|
196
|
+
"""Pre-configured profiles for different streaming scenarios."""
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def openai_streaming():
|
|
200
|
+
"""Optimized for OpenAI-style token streaming (many small chunks)."""
|
|
201
|
+
return {
|
|
202
|
+
'batch_size': 100, # Aggressive batching for tiny chunks
|
|
203
|
+
'timeout_ms': 20 # 20ms max latency
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def large_chunks():
|
|
208
|
+
"""Optimized for larger chunk streaming (e.g., file downloads)."""
|
|
209
|
+
return {
|
|
210
|
+
'batch_size': 10, # Less batching needed
|
|
211
|
+
'timeout_ms': 50 # Can tolerate more latency
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def realtime():
|
|
216
|
+
"""Optimized for real-time streaming with minimal latency."""
|
|
217
|
+
return {
|
|
218
|
+
'batch_size': 5, # Small batches
|
|
219
|
+
'timeout_ms': 5 # Ultra-low latency (5ms)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def high_throughput():
|
|
224
|
+
"""Optimized for maximum throughput (batch processing)."""
|
|
225
|
+
return {
|
|
226
|
+
'batch_size': 200, # Very aggressive batching
|
|
227
|
+
'timeout_ms': 100 # Higher latency acceptable
|
|
228
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# Django-Bolt Authentication System
|
|
2
|
+
|
|
3
|
+
High-performance authentication and authorization system where **validation happens in Rust without the GIL**, achieving 60k+ RPS with JWT authentication.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ Python Layer (Configuration) │
|
|
10
|
+
│ │
|
|
11
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
12
|
+
│ │ JWTAuth │ │ APIKeyAuth │ │ Guards │ │
|
|
13
|
+
│ │ │ │ │ │ │ │
|
|
14
|
+
│ │ .to_metadata()│ │ .to_metadata()│ │ .to_metadata()│ │
|
|
15
|
+
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
|
16
|
+
│ │ │ │ │
|
|
17
|
+
│ └──────────────────┴──────────────────┘ │
|
|
18
|
+
│ │ │
|
|
19
|
+
│ Compile to metadata │
|
|
20
|
+
│ │ │
|
|
21
|
+
└────────────────────────────┼─────────────────────────────────────┘
|
|
22
|
+
│
|
|
23
|
+
▼
|
|
24
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
25
|
+
│ Rust Layer (Validation - NO GIL) │
|
|
26
|
+
│ │
|
|
27
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
28
|
+
│ │ Route Registration │ │
|
|
29
|
+
│ │ • Parse metadata → typed Rust enums │ │
|
|
30
|
+
│ │ • Store auth backends & guards per route │ │
|
|
31
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
32
|
+
│ │
|
|
33
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
34
|
+
│ │ Request Processing (HOT PATH - NO GIL) │ │
|
|
35
|
+
│ │ │ │
|
|
36
|
+
│ │ 1. Extract token from header │ │
|
|
37
|
+
│ │ 2. Validate JWT (jsonwebtoken crate) │ │
|
|
38
|
+
│ │ 3. Check guards/permissions │ │
|
|
39
|
+
│ │ 4. Populate request.context with auth data │ │
|
|
40
|
+
│ │ │ │
|
|
41
|
+
│ │ ⚡ All happens without touching Python/GIL │ │
|
|
42
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
43
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
47
|
+
│ Python Handler (WITH AuthContext) │
|
|
48
|
+
│ │
|
|
49
|
+
│ async def my_handler(request): │
|
|
50
|
+
│ user_id = request["context"]["user_id"] │
|
|
51
|
+
│ is_admin = request["context"]["is_admin"] │
|
|
52
|
+
│ permissions = request["context"]["permissions"] │
|
|
53
|
+
│ claims = request["context"]["auth_claims"] │
|
|
54
|
+
│ ... │
|
|
55
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Module Structure
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
django_bolt/auth/
|
|
62
|
+
├── __init__.py # Public API exports
|
|
63
|
+
├── README.md # This file
|
|
64
|
+
├── backends.py # Authentication backends (JWT, API key, Session)
|
|
65
|
+
├── guards.py # Permission guards (IsAuthenticated, IsAdmin, etc.)
|
|
66
|
+
├── middleware.py # Middleware decorators (cors, rate_limit, etc.)
|
|
67
|
+
└── token.py # JWT Token dataclass with encode/decode
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
### 1. Define Authentication Backend
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from django_bolt import BoltAPI, JWTAuthentication, IsAuthenticated, Token
|
|
76
|
+
from datetime import timedelta
|
|
77
|
+
|
|
78
|
+
api = BoltAPI()
|
|
79
|
+
|
|
80
|
+
# Create JWT token for a user
|
|
81
|
+
def create_token_for_user(user):
|
|
82
|
+
return Token.create(
|
|
83
|
+
sub=str(user.id),
|
|
84
|
+
expires_delta=timedelta(hours=1),
|
|
85
|
+
is_staff=user.is_staff,
|
|
86
|
+
is_admin=user.is_superuser,
|
|
87
|
+
permissions=list(user.get_all_permissions()),
|
|
88
|
+
email=user.email,
|
|
89
|
+
).encode(secret=settings.SECRET_KEY)
|
|
90
|
+
|
|
91
|
+
# Protected endpoint
|
|
92
|
+
@api.get(
|
|
93
|
+
"/profile",
|
|
94
|
+
auth=[JWTAuthentication()],
|
|
95
|
+
guards=[IsAuthenticated()]
|
|
96
|
+
)
|
|
97
|
+
async def get_profile(request):
|
|
98
|
+
user_id = request["context"]["user_id"]
|
|
99
|
+
is_admin = request["context"]["is_admin"]
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"user_id": user_id,
|
|
103
|
+
"is_admin": is_admin,
|
|
104
|
+
"permissions": request["context"].get("permissions", [])
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 2. Global Authentication (Settings)
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# settings.py
|
|
112
|
+
|
|
113
|
+
BOLT_AUTHENTICATION_CLASSES = [
|
|
114
|
+
JWTAuthentication(
|
|
115
|
+
secret=SECRET_KEY,
|
|
116
|
+
algorithms=["HS256"],
|
|
117
|
+
header="authorization",
|
|
118
|
+
audience="my-api",
|
|
119
|
+
)
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
BOLT_DEFAULT_PERMISSION_CLASSES = [
|
|
123
|
+
IsAuthenticated()
|
|
124
|
+
]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Now all routes are protected by default unless you override with `guards=[AllowAny()]`.
|
|
128
|
+
|
|
129
|
+
### 3. Per-Route Authentication Override
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from django_bolt import APIKeyAuthentication, HasPermission
|
|
133
|
+
|
|
134
|
+
# Admin-only endpoint with API key auth
|
|
135
|
+
@api.delete(
|
|
136
|
+
"/users/{user_id}",
|
|
137
|
+
auth=[APIKeyAuthentication(api_keys={"admin-key-123"})],
|
|
138
|
+
guards=[HasPermission("users.delete")]
|
|
139
|
+
)
|
|
140
|
+
async def delete_user(user_id: int):
|
|
141
|
+
# Only callable with valid API key and users.delete permission
|
|
142
|
+
return {"deleted": user_id}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Authentication Backends
|
|
146
|
+
|
|
147
|
+
### JWTAuthentication
|
|
148
|
+
|
|
149
|
+
High-performance JWT validation in Rust using the `jsonwebtoken` crate.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from django_bolt import JWTAuthentication
|
|
153
|
+
|
|
154
|
+
auth = JWTAuthentication(
|
|
155
|
+
secret="your-secret-key", # Default: Django SECRET_KEY
|
|
156
|
+
algorithms=["HS256"], # Supported: HS256/384/512, RS256/384/512, ES256/384
|
|
157
|
+
header="authorization", # Header to extract token from
|
|
158
|
+
audience="my-api", # Optional: validate aud claim
|
|
159
|
+
issuer="auth-service", # Optional: validate iss claim
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Token Format**: `Authorization: Bearer <jwt-token>`
|
|
164
|
+
|
|
165
|
+
**Performance**: ~60k RPS with JWT validation
|
|
166
|
+
|
|
167
|
+
### APIKeyAuthentication
|
|
168
|
+
|
|
169
|
+
Simple API key validation with optional per-key permissions.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from django_bolt import APIKeyAuthentication
|
|
173
|
+
|
|
174
|
+
auth = APIKeyAuthentication(
|
|
175
|
+
api_keys={"key1", "key2", "admin-key"},
|
|
176
|
+
header="x-api-key",
|
|
177
|
+
key_permissions={
|
|
178
|
+
"admin-key": ["users.create", "users.delete", "posts.create"],
|
|
179
|
+
"key1": ["users.view"],
|
|
180
|
+
"key2": ["posts.view", "posts.create"],
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Header Format**: `X-API-Key: your-api-key`
|
|
186
|
+
|
|
187
|
+
### SessionAuthentication
|
|
188
|
+
|
|
189
|
+
Django session-based authentication (falls back to Python execution).
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from django_bolt import SessionAuthentication
|
|
193
|
+
|
|
194
|
+
auth = SessionAuthentication()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Note**: This has higher overhead than JWT/API key as it requires Python execution per request.
|
|
198
|
+
|
|
199
|
+
## Permission Guards
|
|
200
|
+
|
|
201
|
+
Guards are checked **after authentication** in Rust, providing early 403 responses without GIL overhead.
|
|
202
|
+
|
|
203
|
+
### AllowAny
|
|
204
|
+
|
|
205
|
+
Allow unauthenticated requests (bypasses global defaults).
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from django_bolt import AllowAny
|
|
209
|
+
|
|
210
|
+
@api.get("/public", guards=[AllowAny()])
|
|
211
|
+
async def public_endpoint():
|
|
212
|
+
return {"message": "Anyone can access this"}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### IsAuthenticated
|
|
216
|
+
|
|
217
|
+
Require valid authentication (any backend).
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from django_bolt import IsAuthenticated
|
|
221
|
+
|
|
222
|
+
@api.get("/protected", guards=[IsAuthenticated()])
|
|
223
|
+
async def protected():
|
|
224
|
+
return {"message": "Must be authenticated"}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### IsAdminUser / IsStaff
|
|
228
|
+
|
|
229
|
+
Require admin or staff status (from JWT claims `is_superuser`, `is_admin`, or `is_staff`).
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from django_bolt import IsAdminUser, IsStaff
|
|
233
|
+
|
|
234
|
+
@api.get("/admin", guards=[IsAdminUser()])
|
|
235
|
+
async def admin_only():
|
|
236
|
+
return {"message": "Admin access"}
|
|
237
|
+
|
|
238
|
+
@api.get("/staff", guards=[IsStaff()])
|
|
239
|
+
async def staff_only():
|
|
240
|
+
return {"message": "Staff access"}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### HasPermission / HasAnyPermission / HasAllPermissions
|
|
244
|
+
|
|
245
|
+
Fine-grained permission checking.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from django_bolt import HasPermission, HasAnyPermission, HasAllPermissions
|
|
249
|
+
|
|
250
|
+
# Require specific permission
|
|
251
|
+
@api.delete("/users/{id}", guards=[HasPermission("users.delete")])
|
|
252
|
+
async def delete_user(id: int):
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
# Require at least one permission
|
|
256
|
+
@api.put("/users/{id}", guards=[HasAnyPermission("users.edit", "users.admin")])
|
|
257
|
+
async def edit_user(id: int):
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
# Require all permissions
|
|
261
|
+
@api.post("/admin/reset", guards=[HasAllPermissions("admin.full", "admin.reset")])
|
|
262
|
+
async def reset_system():
|
|
263
|
+
pass
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## JWT Token Class
|
|
267
|
+
|
|
268
|
+
The `Token` dataclass provides a Pythonic interface for JWT tokens with validation.
|
|
269
|
+
|
|
270
|
+
### Creating Tokens
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from django_bolt import Token
|
|
274
|
+
from datetime import datetime, timedelta, timezone
|
|
275
|
+
|
|
276
|
+
# Option 1: Direct instantiation
|
|
277
|
+
token = Token(
|
|
278
|
+
sub="user123",
|
|
279
|
+
exp=datetime.now(timezone.utc) + timedelta(hours=1),
|
|
280
|
+
is_staff=True,
|
|
281
|
+
permissions=["read", "write"],
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Option 2: Factory method (recommended)
|
|
285
|
+
token = Token.create(
|
|
286
|
+
sub="user123",
|
|
287
|
+
expires_delta=timedelta(hours=1),
|
|
288
|
+
is_staff=True,
|
|
289
|
+
is_admin=False,
|
|
290
|
+
permissions=["users.view", "posts.create"],
|
|
291
|
+
# Extra custom claims
|
|
292
|
+
tenant_id="acme-corp",
|
|
293
|
+
role="manager",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Encode to JWT string
|
|
297
|
+
jwt_string = token.encode(secret="my-secret", algorithm="HS256")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Decoding Tokens
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
# Decode and validate
|
|
304
|
+
token = Token.decode(
|
|
305
|
+
jwt_string,
|
|
306
|
+
secret="my-secret",
|
|
307
|
+
algorithm="HS256",
|
|
308
|
+
audience="my-api", # Optional
|
|
309
|
+
issuer="auth-service", # Optional
|
|
310
|
+
verify_exp=True, # Verify expiration
|
|
311
|
+
verify_nbf=True, # Verify not-before
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
print(token.sub) # "user123"
|
|
315
|
+
print(token.is_staff) # True
|
|
316
|
+
print(token.permissions) # ["users.view", "posts.create"]
|
|
317
|
+
print(token.extras) # {"tenant_id": "acme-corp", "role": "manager"}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Integration with Django Users
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from django_bolt.jwt_utils import create_jwt_for_user
|
|
324
|
+
|
|
325
|
+
# Create token from Django User instance
|
|
326
|
+
user = await User.objects.aget(username="john")
|
|
327
|
+
token = create_jwt_for_user(
|
|
328
|
+
user,
|
|
329
|
+
expires_in=3600, # 1 hour
|
|
330
|
+
extra_claims={
|
|
331
|
+
"permissions": ["users.view", "posts.create"],
|
|
332
|
+
"tenant": "acme",
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Use in login endpoint
|
|
337
|
+
@api.post("/login")
|
|
338
|
+
async def login(username: str, password: str):
|
|
339
|
+
# Authenticate user...
|
|
340
|
+
token = create_jwt_for_user(user)
|
|
341
|
+
return {"access_token": token, "token_type": "bearer"}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Request Context
|
|
345
|
+
|
|
346
|
+
After authentication, the request context is populated with auth data:
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
@api.get("/me")
|
|
350
|
+
async def get_current_user_info(request):
|
|
351
|
+
ctx = request["context"]
|
|
352
|
+
|
|
353
|
+
# Always available after successful auth
|
|
354
|
+
user_id = ctx["user_id"] # str: User identifier
|
|
355
|
+
is_staff = ctx["is_staff"] # bool: Staff status
|
|
356
|
+
is_admin = ctx["is_admin"] # bool: Admin status
|
|
357
|
+
backend = ctx["auth_backend"] # str: "jwt", "api_key", etc.
|
|
358
|
+
|
|
359
|
+
# Available if permissions configured
|
|
360
|
+
permissions = ctx.get("permissions", []) # list[str]: Permission strings
|
|
361
|
+
|
|
362
|
+
# Available for JWT auth
|
|
363
|
+
if "auth_claims" in ctx:
|
|
364
|
+
claims = ctx["auth_claims"]
|
|
365
|
+
exp = claims["exp"] # Expiration timestamp
|
|
366
|
+
iat = claims["iat"] # Issued at timestamp
|
|
367
|
+
# Plus any custom claims
|
|
368
|
+
|
|
369
|
+
return {"user_id": user_id, "is_admin": is_admin}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Helper Functions
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
from django_bolt.jwt_utils import (
|
|
376
|
+
get_current_user, # Fetch Django User from DB
|
|
377
|
+
extract_user_id_from_context,
|
|
378
|
+
get_auth_context,
|
|
379
|
+
)
|
|
380
|
+
from django_bolt.params import Depends
|
|
381
|
+
|
|
382
|
+
# Dependency injection to get Django User
|
|
383
|
+
@api.get("/profile")
|
|
384
|
+
async def my_profile(user=Depends(get_current_user)):
|
|
385
|
+
if not user:
|
|
386
|
+
return {"error": "Not authenticated"}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"id": user.id,
|
|
390
|
+
"username": user.username,
|
|
391
|
+
"email": user.email,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# Extract just the user ID
|
|
395
|
+
@api.get("/data")
|
|
396
|
+
async def get_data(request):
|
|
397
|
+
user_id = extract_user_id_from_context(request)
|
|
398
|
+
# Use user_id...
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Performance Characteristics
|
|
402
|
+
|
|
403
|
+
| Operation | Performance | Notes |
|
|
404
|
+
|-----------|-------------|-------|
|
|
405
|
+
| JWT Validation | ~60k+ RPS | Entirely in Rust, no GIL |
|
|
406
|
+
| API Key Check | ~65k+ RPS | Simple HashSet lookup |
|
|
407
|
+
| Guard Check | ~65k+ RPS | In-memory permission check |
|
|
408
|
+
| Session Auth | ~10-15k RPS | Falls back to Python/Django |
|
|
409
|
+
|
|
410
|
+
**Key Insight**: Authentication/authorization happens in the hot path **before** calling Python handlers, so most invalid requests are rejected at ~60k RPS without ever touching the GIL.
|
|
411
|
+
|
|
412
|
+
## Rust Implementation Details
|
|
413
|
+
|
|
414
|
+
### Key Files
|
|
415
|
+
|
|
416
|
+
- **`src/middleware/auth.rs`**: JWT/API key validation, Claims struct, AuthContext
|
|
417
|
+
- **`src/metadata.rs`**: Parse Python metadata → Rust types at registration
|
|
418
|
+
- **`src/permissions.rs`**: Guard enum and validation logic
|
|
419
|
+
- **`src/lib.rs`**: Request processing pipeline
|
|
420
|
+
|
|
421
|
+
### Performance Optimizations
|
|
422
|
+
|
|
423
|
+
1. **Zero-copy validation**: JWT validation uses `jsonwebtoken` crate directly on request bytes
|
|
424
|
+
2. **No GIL during auth**: Entire auth pipeline runs without acquiring GIL
|
|
425
|
+
3. **Early rejection**: Invalid tokens/permissions rejected before Python handler call
|
|
426
|
+
4. **Metadata compilation**: Auth config parsed once at registration, not per-request
|
|
427
|
+
5. **Efficient data structures**: DashMap for rate limiting, HashSet for permissions
|
|
428
|
+
|
|
429
|
+
## Testing
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
# Run auth tests
|
|
433
|
+
uv run pytest python/django_bolt/tests/test_guards_auth.py -v
|
|
434
|
+
uv run pytest python/django_bolt/tests/test_jwt_token.py -v
|
|
435
|
+
|
|
436
|
+
# All tests
|
|
437
|
+
uv run pytest python/django_bolt/tests/ -v
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Migration from Old Code
|
|
441
|
+
|
|
442
|
+
If you had direct imports from `django_bolt.auth` or `django_bolt.permissions`, they still work:
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
# Old imports (still work via backward compat shim)
|
|
446
|
+
from django_bolt.auth import JWTAuthentication
|
|
447
|
+
from django_bolt.permissions import IsAuthenticated
|
|
448
|
+
|
|
449
|
+
# New organized imports (recommended)
|
|
450
|
+
from django_bolt.auth import JWTAuthentication, IsAuthenticated
|
|
451
|
+
# or
|
|
452
|
+
from django_bolt import JWTAuthentication, IsAuthenticated
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Examples
|
|
456
|
+
|
|
457
|
+
See `/python/examples/testproject/testproject/api.py` for real-world usage examples.
|
|
458
|
+
|
|
459
|
+
## Benchmarks
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
# Run auth-specific benchmarks
|
|
463
|
+
make bench-auth # TODO: Add auth benchmarks to Makefile
|
|
464
|
+
```
|