django-bolt 0.1.0__cp310-abi3-win_amd64.whl → 0.1.2__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 +2 -2
- django_bolt/_core.pyd +0 -0
- django_bolt/_json.py +169 -0
- django_bolt/admin/static_routes.py +15 -21
- django_bolt/api.py +181 -61
- django_bolt/auth/__init__.py +2 -2
- django_bolt/decorators.py +15 -3
- django_bolt/dependencies.py +30 -24
- django_bolt/error_handlers.py +2 -1
- django_bolt/openapi/plugins.py +3 -2
- django_bolt/openapi/schema_generator.py +65 -20
- django_bolt/pagination.py +2 -1
- django_bolt/responses.py +3 -2
- django_bolt/serialization.py +5 -4
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/METADATA +181 -201
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/RECORD +18 -55
- django_bolt/auth/README.md +0 -464
- django_bolt/auth/REVOCATION_EXAMPLE.md +0 -391
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +0 -1
- django_bolt/tests/admin_tests/conftest.py +0 -6
- django_bolt/tests/admin_tests/test_admin_with_django.py +0 -278
- django_bolt/tests/admin_tests/urls.py +0 -9
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +0 -570
- django_bolt/tests/cbv/test_class_views_django_orm.py +0 -703
- django_bolt/tests/cbv/test_class_views_features.py +0 -1173
- django_bolt/tests/cbv/test_class_views_with_client.py +0 -622
- django_bolt/tests/conftest.py +0 -165
- django_bolt/tests/test_action_decorator.py +0 -399
- django_bolt/tests/test_auth_secret_key.py +0 -83
- django_bolt/tests/test_decorator_syntax.py +0 -159
- django_bolt/tests/test_error_handling.py +0 -481
- django_bolt/tests/test_file_response.py +0 -192
- django_bolt/tests/test_global_cors.py +0 -172
- django_bolt/tests/test_guards_auth.py +0 -441
- django_bolt/tests/test_guards_integration.py +0 -303
- django_bolt/tests/test_health.py +0 -283
- django_bolt/tests/test_integration_validation.py +0 -400
- django_bolt/tests/test_json_validation.py +0 -536
- django_bolt/tests/test_jwt_auth.py +0 -327
- django_bolt/tests/test_jwt_token.py +0 -458
- django_bolt/tests/test_logging.py +0 -837
- django_bolt/tests/test_logging_merge.py +0 -419
- django_bolt/tests/test_middleware.py +0 -492
- django_bolt/tests/test_middleware_server.py +0 -230
- django_bolt/tests/test_model_viewset.py +0 -323
- django_bolt/tests/test_models.py +0 -24
- django_bolt/tests/test_pagination.py +0 -1258
- django_bolt/tests/test_parameter_validation.py +0 -178
- django_bolt/tests/test_syntax.py +0 -626
- django_bolt/tests/test_testing_utilities.py +0 -163
- django_bolt/tests/test_testing_utilities_simple.py +0 -123
- django_bolt/tests/test_viewset_unified.py +0 -346
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/WHEEL +0 -0
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/entry_points.txt +0 -0
django_bolt/__init__.py
CHANGED
|
@@ -34,7 +34,7 @@ from .auth import (
|
|
|
34
34
|
# Authentication backends
|
|
35
35
|
JWTAuthentication,
|
|
36
36
|
APIKeyAuthentication,
|
|
37
|
-
SessionAuthentication,
|
|
37
|
+
SessionAuthentication, # Session authentication is not implemented
|
|
38
38
|
AuthContext,
|
|
39
39
|
# Guards/Permissions
|
|
40
40
|
AllowAny,
|
|
@@ -107,7 +107,7 @@ __all__ = [
|
|
|
107
107
|
# Auth - Authentication
|
|
108
108
|
"JWTAuthentication",
|
|
109
109
|
"APIKeyAuthentication",
|
|
110
|
-
"SessionAuthentication",
|
|
110
|
+
"SessionAuthentication", # Session authentication is not implemented
|
|
111
111
|
"AuthContext",
|
|
112
112
|
# Auth - Guards/Permissions
|
|
113
113
|
"AllowAny",
|
django_bolt/_core.pyd
CHANGED
|
Binary file
|
django_bolt/_json.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
- Type-safe decoding with validation
|
|
7
|
+
- Custom encoder/decoder hooks
|
|
8
|
+
|
|
9
|
+
Inspired by Litestar's serialization approach.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
from datetime import date, datetime, time
|
|
16
|
+
from decimal import Decimal
|
|
17
|
+
from ipaddress import (
|
|
18
|
+
IPv4Address,
|
|
19
|
+
IPv4Interface,
|
|
20
|
+
IPv4Network,
|
|
21
|
+
IPv6Address,
|
|
22
|
+
IPv6Interface,
|
|
23
|
+
IPv6Network,
|
|
24
|
+
)
|
|
25
|
+
from pathlib import Path, PurePath
|
|
26
|
+
from typing import Any, Callable, TypeVar
|
|
27
|
+
from uuid import UUID
|
|
28
|
+
|
|
29
|
+
import msgspec
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
|
|
33
|
+
# Thread-local storage for encoder/decoder instances
|
|
34
|
+
_thread_local = threading.local()
|
|
35
|
+
|
|
36
|
+
# Default type encoders for non-JSON-native types
|
|
37
|
+
# Maps type -> encoder function
|
|
38
|
+
DEFAULT_TYPE_ENCODERS: dict[type, Callable[[Any], Any]] = {
|
|
39
|
+
# Paths
|
|
40
|
+
Path: str,
|
|
41
|
+
PurePath: str,
|
|
42
|
+
# Dates/Times -> ISO format
|
|
43
|
+
datetime: lambda v: v.isoformat(),
|
|
44
|
+
date: lambda v: v.isoformat(),
|
|
45
|
+
time: lambda v: v.isoformat(),
|
|
46
|
+
# Decimals -> int or float
|
|
47
|
+
Decimal: lambda v: int(v) if v.as_tuple().exponent >= 0 else float(v),
|
|
48
|
+
# IP addresses
|
|
49
|
+
IPv4Address: str,
|
|
50
|
+
IPv4Interface: str,
|
|
51
|
+
IPv4Network: str,
|
|
52
|
+
IPv6Address: str,
|
|
53
|
+
IPv6Interface: str,
|
|
54
|
+
IPv6Network: str,
|
|
55
|
+
# UUID
|
|
56
|
+
UUID: str,
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def default_serializer(value: Any) -> Any:
|
|
62
|
+
"""Transform values non-natively supported by msgspec.
|
|
63
|
+
|
|
64
|
+
Walks the MRO (Method Resolution Order) to support subclasses.
|
|
65
|
+
Raises TypeError if type is unsupported.
|
|
66
|
+
"""
|
|
67
|
+
# Walk MRO to support polymorphic types
|
|
68
|
+
for base in value.__class__.__mro__[:-1]: # Skip 'object'
|
|
69
|
+
encoder = DEFAULT_TYPE_ENCODERS.get(base)
|
|
70
|
+
if encoder is not None:
|
|
71
|
+
return encoder(value)
|
|
72
|
+
|
|
73
|
+
raise TypeError(f"Unsupported type: {type(value)!r}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_encoder() -> msgspec.json.Encoder:
|
|
77
|
+
"""Return a thread-local msgspec JSON Encoder instance.
|
|
78
|
+
|
|
79
|
+
Using a per-thread encoder is thread-safe and avoids cross-thread contention
|
|
80
|
+
while still reusing the internal buffer for repeated encodes on the same thread.
|
|
81
|
+
"""
|
|
82
|
+
encoder = getattr(_thread_local, "encoder", None)
|
|
83
|
+
if encoder is None:
|
|
84
|
+
encoder = msgspec.json.Encoder(enc_hook=default_serializer)
|
|
85
|
+
_thread_local.encoder = encoder
|
|
86
|
+
return encoder
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_decoder() -> msgspec.json.Decoder:
|
|
90
|
+
"""Return a thread-local msgspec JSON Decoder instance.
|
|
91
|
+
|
|
92
|
+
Using a per-thread decoder is thread-safe and reuses the internal buffer.
|
|
93
|
+
"""
|
|
94
|
+
decoder = getattr(_thread_local, "decoder", None)
|
|
95
|
+
if decoder is None:
|
|
96
|
+
decoder = msgspec.json.Decoder()
|
|
97
|
+
_thread_local.decoder = decoder
|
|
98
|
+
return decoder
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def encode(value: Any, serializer: Callable[[Any], Any] | None = None) -> bytes:
|
|
102
|
+
"""Encode a Python object to JSON bytes.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
value: Object to encode
|
|
106
|
+
serializer: Optional custom encoder hook (overrides default)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
JSON bytes
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
TypeError: If value contains unsupported types
|
|
113
|
+
msgspec.EncodeError: If encoding fails
|
|
114
|
+
"""
|
|
115
|
+
if serializer is not None:
|
|
116
|
+
# Custom serializer provided - use one-off encoder
|
|
117
|
+
return msgspec.json.encode(value, enc_hook=serializer)
|
|
118
|
+
|
|
119
|
+
# Use thread-local cached encoder with default serializer
|
|
120
|
+
return _get_encoder().encode(value)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def decode(value: bytes | str) -> Any:
|
|
124
|
+
"""Decode JSON bytes/string to Python object.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
value: JSON bytes or string
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Decoded Python object
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
msgspec.DecodeError: If decoding fails
|
|
134
|
+
"""
|
|
135
|
+
return _get_decoder().decode(value)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def decode_typed(
|
|
139
|
+
value: bytes | str,
|
|
140
|
+
target_type: type[T],
|
|
141
|
+
strict: bool = True,
|
|
142
|
+
) -> T:
|
|
143
|
+
"""Decode JSON with type validation and coercion.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
value: JSON bytes or string
|
|
147
|
+
target_type: Expected type (e.g., msgspec.Struct subclass)
|
|
148
|
+
strict: If False, enables lenient type coercion (e.g., "123" -> 123)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Decoded and validated object of target_type
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
msgspec.DecodeError: If decoding or validation fails
|
|
155
|
+
msgspec.ValidationError: If type validation fails
|
|
156
|
+
|
|
157
|
+
Examples:
|
|
158
|
+
>>> class User(msgspec.Struct):
|
|
159
|
+
... id: int
|
|
160
|
+
... name: str
|
|
161
|
+
>>> decode_typed(b'{"id": 1, "name": "Alice"}', User)
|
|
162
|
+
User(id=1, name='Alice')
|
|
163
|
+
"""
|
|
164
|
+
return msgspec.json.decode(value, type=target_type, strict=strict)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["encode", "decode", "decode_typed", "DEFAULT_TYPE_ENCODERS", "default_serializer"]
|
|
168
|
+
|
|
169
|
+
|
|
@@ -81,31 +81,25 @@ class StaticRouteRegistrar:
|
|
|
81
81
|
self.api._handlers[handler_id] = handler
|
|
82
82
|
|
|
83
83
|
# Create metadata for static handler
|
|
84
|
-
# Extract path parameter metadata
|
|
84
|
+
# Extract path parameter metadata using FieldDefinition
|
|
85
|
+
from django_bolt.typing import FieldDefinition
|
|
86
|
+
|
|
85
87
|
sig = inspect.signature(handler)
|
|
88
|
+
path_field = FieldDefinition(
|
|
89
|
+
name="path",
|
|
90
|
+
annotation=str,
|
|
91
|
+
default=inspect.Parameter.empty,
|
|
92
|
+
source="path",
|
|
93
|
+
alias=None,
|
|
94
|
+
embed=False,
|
|
95
|
+
dependency=None,
|
|
96
|
+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
97
|
+
)
|
|
98
|
+
|
|
86
99
|
meta = {
|
|
87
100
|
"mode": "mixed",
|
|
88
101
|
"sig": sig,
|
|
89
|
-
"fields": [
|
|
90
|
-
"name": "path",
|
|
91
|
-
"annotation": str,
|
|
92
|
-
"default": inspect.Parameter.empty,
|
|
93
|
-
"kind": inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
94
|
-
"source": "path",
|
|
95
|
-
"alias": None,
|
|
96
|
-
"embed": False,
|
|
97
|
-
"dependency": None,
|
|
98
|
-
}],
|
|
102
|
+
"fields": [path_field],
|
|
99
103
|
"path_params": {"path"},
|
|
100
|
-
"params": [{
|
|
101
|
-
"name": "path",
|
|
102
|
-
"annotation": str,
|
|
103
|
-
"default": inspect.Parameter.empty,
|
|
104
|
-
"kind": inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
105
|
-
"source": "path",
|
|
106
|
-
"alias": None,
|
|
107
|
-
"embed": False,
|
|
108
|
-
"dependency": None,
|
|
109
|
-
}],
|
|
110
104
|
}
|
|
111
105
|
self.api._handler_meta[handler] = meta
|
django_bolt/api.py
CHANGED
|
@@ -18,7 +18,7 @@ from .binding import (
|
|
|
18
18
|
convert_primitive,
|
|
19
19
|
create_extractor,
|
|
20
20
|
)
|
|
21
|
-
from .typing import is_msgspec_struct, is_optional
|
|
21
|
+
from .typing import is_msgspec_struct, is_optional, unwrap_optional
|
|
22
22
|
from .request_parsing import parse_form_data
|
|
23
23
|
from .dependencies import resolve_dependency
|
|
24
24
|
from .serialization import serialize_response
|
|
@@ -43,7 +43,7 @@ def _extract_path_params(path: str) -> set[str]:
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def extract_parameter_value(
|
|
46
|
-
|
|
46
|
+
field: "FieldDefinition",
|
|
47
47
|
request: Dict[str, Any],
|
|
48
48
|
params_map: Dict[str, Any],
|
|
49
49
|
query_map: Dict[str, Any],
|
|
@@ -56,16 +56,29 @@ def extract_parameter_value(
|
|
|
56
56
|
body_loaded: bool
|
|
57
57
|
) -> Tuple[Any, Any, bool]:
|
|
58
58
|
"""
|
|
59
|
-
Extract value for a handler parameter
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
Extract value for a handler parameter using FieldDefinition.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
field: FieldDefinition object describing the parameter
|
|
63
|
+
request: Request dictionary
|
|
64
|
+
params_map: Path parameters
|
|
65
|
+
query_map: Query parameters
|
|
66
|
+
headers_map: Request headers
|
|
67
|
+
cookies_map: Request cookies
|
|
68
|
+
form_map: Form data
|
|
69
|
+
files_map: Uploaded files
|
|
70
|
+
meta: Handler metadata
|
|
71
|
+
body_obj: Cached body object
|
|
72
|
+
body_loaded: Whether body has been loaded
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Tuple of (value, body_obj, body_loaded)
|
|
63
76
|
"""
|
|
64
|
-
name =
|
|
65
|
-
annotation =
|
|
66
|
-
default =
|
|
67
|
-
source =
|
|
68
|
-
alias =
|
|
77
|
+
name = field.name
|
|
78
|
+
annotation = field.annotation
|
|
79
|
+
default = field.default
|
|
80
|
+
source = field.source
|
|
81
|
+
alias = field.alias
|
|
69
82
|
key = alias or name
|
|
70
83
|
|
|
71
84
|
# Handle different sources
|
|
@@ -105,7 +118,32 @@ def extract_parameter_value(
|
|
|
105
118
|
|
|
106
119
|
elif source == "file":
|
|
107
120
|
if key in files_map:
|
|
108
|
-
|
|
121
|
+
file_info = files_map[key]
|
|
122
|
+
# Extract appropriate value based on annotation type
|
|
123
|
+
unwrapped_type = unwrap_optional(annotation) if is_optional(annotation) else annotation
|
|
124
|
+
|
|
125
|
+
# Get the origin type (list, dict, etc.)
|
|
126
|
+
origin = get_origin(unwrapped_type)
|
|
127
|
+
|
|
128
|
+
if unwrapped_type is bytes:
|
|
129
|
+
# For bytes annotation, extract content from single file
|
|
130
|
+
if isinstance(file_info, list):
|
|
131
|
+
# Multiple files, but bytes expects single - take first
|
|
132
|
+
return file_info[0].get("content", b""), body_obj, body_loaded
|
|
133
|
+
return file_info.get("content", b""), body_obj, body_loaded
|
|
134
|
+
elif origin is list:
|
|
135
|
+
# For list annotation, ensure value is a list
|
|
136
|
+
if isinstance(file_info, list):
|
|
137
|
+
return file_info, body_obj, body_loaded
|
|
138
|
+
else:
|
|
139
|
+
# Wrap single file in list
|
|
140
|
+
return [file_info], body_obj, body_loaded
|
|
141
|
+
else:
|
|
142
|
+
# Return full file info for dict/Any annotations
|
|
143
|
+
if isinstance(file_info, list):
|
|
144
|
+
# List but annotation doesn't expect list - take first
|
|
145
|
+
return file_info[0], body_obj, body_loaded
|
|
146
|
+
return file_info, body_obj, body_loaded
|
|
109
147
|
elif default is not inspect.Parameter.empty or is_optional(annotation):
|
|
110
148
|
return (None if default is inspect.Parameter.empty else default), body_obj, body_loaded
|
|
111
149
|
raise ValueError(f"Missing required file: {key}")
|
|
@@ -247,26 +285,103 @@ class BoltAPI:
|
|
|
247
285
|
# Register this instance globally for autodiscovery
|
|
248
286
|
_BOLT_API_REGISTRY.append(self)
|
|
249
287
|
|
|
250
|
-
def get(
|
|
251
|
-
|
|
288
|
+
def get(
|
|
289
|
+
self,
|
|
290
|
+
path: str,
|
|
291
|
+
*,
|
|
292
|
+
response_model: Optional[Any] = None,
|
|
293
|
+
status_code: Optional[int] = None,
|
|
294
|
+
guards: Optional[List[Any]] = None,
|
|
295
|
+
auth: Optional[List[Any]] = None,
|
|
296
|
+
tags: Optional[List[str]] = None,
|
|
297
|
+
summary: Optional[str] = None,
|
|
298
|
+
description: Optional[str] = None,
|
|
299
|
+
):
|
|
300
|
+
return self._route_decorator("GET", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth, tags=tags, summary=summary, description=description)
|
|
252
301
|
|
|
253
|
-
def post(
|
|
254
|
-
|
|
302
|
+
def post(
|
|
303
|
+
self,
|
|
304
|
+
path: str,
|
|
305
|
+
*,
|
|
306
|
+
response_model: Optional[Any] = None,
|
|
307
|
+
status_code: Optional[int] = None,
|
|
308
|
+
guards: Optional[List[Any]] = None,
|
|
309
|
+
auth: Optional[List[Any]] = None,
|
|
310
|
+
tags: Optional[List[str]] = None,
|
|
311
|
+
summary: Optional[str] = None,
|
|
312
|
+
description: Optional[str] = None,
|
|
313
|
+
):
|
|
314
|
+
return self._route_decorator("POST", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth, tags=tags, summary=summary, description=description)
|
|
255
315
|
|
|
256
|
-
def put(
|
|
257
|
-
|
|
316
|
+
def put(
|
|
317
|
+
self,
|
|
318
|
+
path: str,
|
|
319
|
+
*,
|
|
320
|
+
response_model: Optional[Any] = None,
|
|
321
|
+
status_code: Optional[int] = None,
|
|
322
|
+
guards: Optional[List[Any]] = None,
|
|
323
|
+
auth: Optional[List[Any]] = None,
|
|
324
|
+
tags: Optional[List[str]] = None,
|
|
325
|
+
summary: Optional[str] = None,
|
|
326
|
+
description: Optional[str] = None,
|
|
327
|
+
):
|
|
328
|
+
return self._route_decorator("PUT", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth, tags=tags, summary=summary, description=description)
|
|
258
329
|
|
|
259
|
-
def patch(
|
|
260
|
-
|
|
330
|
+
def patch(
|
|
331
|
+
self,
|
|
332
|
+
path: str,
|
|
333
|
+
*,
|
|
334
|
+
response_model: Optional[Any] = None,
|
|
335
|
+
status_code: Optional[int] = None,
|
|
336
|
+
guards: Optional[List[Any]] = None,
|
|
337
|
+
auth: Optional[List[Any]] = None,
|
|
338
|
+
tags: Optional[List[str]] = None,
|
|
339
|
+
summary: Optional[str] = None,
|
|
340
|
+
description: Optional[str] = None,
|
|
341
|
+
):
|
|
342
|
+
return self._route_decorator("PATCH", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth, tags=tags, summary=summary, description=description)
|
|
261
343
|
|
|
262
|
-
def delete(
|
|
263
|
-
|
|
344
|
+
def delete(
|
|
345
|
+
self,
|
|
346
|
+
path: str,
|
|
347
|
+
*,
|
|
348
|
+
response_model: Optional[Any] = None,
|
|
349
|
+
status_code: Optional[int] = None,
|
|
350
|
+
guards: Optional[List[Any]] = None,
|
|
351
|
+
auth: Optional[List[Any]] = None,
|
|
352
|
+
tags: Optional[List[str]] = None,
|
|
353
|
+
summary: Optional[str] = None,
|
|
354
|
+
description: Optional[str] = None,
|
|
355
|
+
):
|
|
356
|
+
return self._route_decorator("DELETE", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth, tags=tags, summary=summary, description=description)
|
|
264
357
|
|
|
265
|
-
def head(
|
|
266
|
-
|
|
358
|
+
def head(
|
|
359
|
+
self,
|
|
360
|
+
path: str,
|
|
361
|
+
*,
|
|
362
|
+
response_model: Optional[Any] = None,
|
|
363
|
+
status_code: Optional[int] = None,
|
|
364
|
+
guards: Optional[List[Any]] = None,
|
|
365
|
+
auth: Optional[List[Any]] = None,
|
|
366
|
+
tags: Optional[List[str]] = None,
|
|
367
|
+
summary: Optional[str] = None,
|
|
368
|
+
description: Optional[str] = None,
|
|
369
|
+
):
|
|
370
|
+
return self._route_decorator("HEAD", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth, tags=tags, summary=summary, description=description)
|
|
267
371
|
|
|
268
|
-
def options(
|
|
269
|
-
|
|
372
|
+
def options(
|
|
373
|
+
self,
|
|
374
|
+
path: str,
|
|
375
|
+
*,
|
|
376
|
+
response_model: Optional[Any] = None,
|
|
377
|
+
status_code: Optional[int] = None,
|
|
378
|
+
guards: Optional[List[Any]] = None,
|
|
379
|
+
auth: Optional[List[Any]] = None,
|
|
380
|
+
tags: Optional[List[str]] = None,
|
|
381
|
+
summary: Optional[str] = None,
|
|
382
|
+
description: Optional[str] = None,
|
|
383
|
+
):
|
|
384
|
+
return self._route_decorator("OPTIONS", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth, tags=tags, summary=summary, description=description)
|
|
270
385
|
|
|
271
386
|
def view(
|
|
272
387
|
self,
|
|
@@ -588,11 +703,26 @@ class BoltAPI:
|
|
|
588
703
|
response_model=attr.response_model,
|
|
589
704
|
status_code=attr.status_code,
|
|
590
705
|
guards=final_guards,
|
|
591
|
-
auth=final_auth
|
|
706
|
+
auth=final_auth,
|
|
707
|
+
tags=attr.tags,
|
|
708
|
+
summary=attr.summary,
|
|
709
|
+
description=attr.description,
|
|
592
710
|
)
|
|
593
711
|
decorator(custom_action_handler)
|
|
594
712
|
|
|
595
|
-
def _route_decorator(
|
|
713
|
+
def _route_decorator(
|
|
714
|
+
self,
|
|
715
|
+
method: str,
|
|
716
|
+
path: str,
|
|
717
|
+
*,
|
|
718
|
+
response_model: Optional[Any] = None,
|
|
719
|
+
status_code: Optional[int] = None,
|
|
720
|
+
guards: Optional[List[Any]] = None,
|
|
721
|
+
auth: Optional[List[Any]] = None,
|
|
722
|
+
tags: Optional[List[str]] = None,
|
|
723
|
+
summary: Optional[str] = None,
|
|
724
|
+
description: Optional[str] = None,
|
|
725
|
+
):
|
|
596
726
|
def decorator(fn: Callable):
|
|
597
727
|
# Enforce async handlers
|
|
598
728
|
if not inspect.iscoroutinefunction(fn):
|
|
@@ -614,6 +744,13 @@ class BoltAPI:
|
|
|
614
744
|
meta["response_type"] = response_model
|
|
615
745
|
if status_code is not None:
|
|
616
746
|
meta["default_status_code"] = int(status_code)
|
|
747
|
+
# Store OpenAPI metadata
|
|
748
|
+
if tags is not None:
|
|
749
|
+
meta["openapi_tags"] = tags
|
|
750
|
+
if summary is not None:
|
|
751
|
+
meta["openapi_summary"] = summary
|
|
752
|
+
if description is not None:
|
|
753
|
+
meta["openapi_description"] = description
|
|
617
754
|
self._handler_meta[fn] = meta
|
|
618
755
|
|
|
619
756
|
# Compile middleware metadata for this handler (including guards and auth)
|
|
@@ -717,20 +854,8 @@ class BoltAPI:
|
|
|
717
854
|
f" 3. Use simple types (str, int) which auto-infer as query params"
|
|
718
855
|
)
|
|
719
856
|
|
|
720
|
-
#
|
|
721
|
-
|
|
722
|
-
for field in field_definitions:
|
|
723
|
-
meta["fields"].append({
|
|
724
|
-
"name": field.name,
|
|
725
|
-
"annotation": field.annotation,
|
|
726
|
-
"default": field.default,
|
|
727
|
-
"kind": field.kind,
|
|
728
|
-
"source": field.source,
|
|
729
|
-
"alias": field.alias,
|
|
730
|
-
"embed": field.embed,
|
|
731
|
-
"dependency": field.dependency,
|
|
732
|
-
"field_def": field, # Store FieldDefinition for future use
|
|
733
|
-
})
|
|
857
|
+
# Store FieldDefinition objects directly (Phase 4: completed migration)
|
|
858
|
+
meta["fields"] = field_definitions
|
|
734
859
|
|
|
735
860
|
# Detect single body parameter for fast path
|
|
736
861
|
if len(body_fields) == 1:
|
|
@@ -745,9 +870,6 @@ class BoltAPI:
|
|
|
745
870
|
|
|
746
871
|
meta["mode"] = "mixed"
|
|
747
872
|
|
|
748
|
-
# Maintain backward compatibility with old "params" key
|
|
749
|
-
meta["params"] = meta["fields"]
|
|
750
|
-
|
|
751
873
|
# Performance: Check if handler needs form/file parsing
|
|
752
874
|
# This allows us to skip expensive form parsing for 95% of endpoints
|
|
753
875
|
needs_form_parsing = any(f.source in ("form", "file") for f in field_definitions)
|
|
@@ -778,34 +900,31 @@ class BoltAPI:
|
|
|
778
900
|
body_loaded: bool = False
|
|
779
901
|
dep_cache: Dict[Any, Any] = {}
|
|
780
902
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if source == "request":
|
|
903
|
+
# Use FieldDefinition objects directly
|
|
904
|
+
fields = meta["fields"]
|
|
905
|
+
for field in fields:
|
|
906
|
+
if field.source == "request":
|
|
787
907
|
value = request
|
|
788
|
-
elif source == "dependency":
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
raise ValueError(f"Depends for parameter {name} requires a callable")
|
|
908
|
+
elif field.source == "dependency":
|
|
909
|
+
if field.dependency is None:
|
|
910
|
+
raise ValueError(f"Depends for parameter {field.name} requires a callable")
|
|
792
911
|
value = await resolve_dependency(
|
|
793
|
-
|
|
912
|
+
field.dependency.dependency, field.dependency, request, dep_cache,
|
|
794
913
|
params_map, query_map, headers_map, cookies_map,
|
|
795
914
|
self._handler_meta, self._compile_binder,
|
|
796
915
|
meta.get("http_method", ""), meta.get("path", "")
|
|
797
916
|
)
|
|
798
917
|
else:
|
|
799
918
|
value, body_obj, body_loaded = extract_parameter_value(
|
|
800
|
-
|
|
919
|
+
field, request, params_map, query_map, headers_map, cookies_map,
|
|
801
920
|
form_map, files_map, meta, body_obj, body_loaded
|
|
802
921
|
)
|
|
803
922
|
|
|
804
923
|
# Respect positional-only/keyword-only kinds
|
|
805
|
-
if
|
|
924
|
+
if field.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
|
|
806
925
|
args.append(value)
|
|
807
926
|
else:
|
|
808
|
-
kwargs[name] = value
|
|
927
|
+
kwargs[field.name] = value
|
|
809
928
|
|
|
810
929
|
return args, kwargs
|
|
811
930
|
|
|
@@ -813,7 +932,8 @@ class BoltAPI:
|
|
|
813
932
|
def _handle_http_exception(self, he: HTTPException) -> Response:
|
|
814
933
|
"""Handle HTTPException and return response."""
|
|
815
934
|
try:
|
|
816
|
-
|
|
935
|
+
from . import _json
|
|
936
|
+
body = _json.encode({"detail": he.detail})
|
|
817
937
|
headers = [("content-type", "application/json")]
|
|
818
938
|
except Exception:
|
|
819
939
|
body = str(he.detail).encode()
|
|
@@ -828,7 +948,7 @@ class BoltAPI:
|
|
|
828
948
|
"""Handle generic exception using error_handlers module."""
|
|
829
949
|
from . import error_handlers
|
|
830
950
|
# Use the error handler which respects Django DEBUG setting
|
|
831
|
-
return error_handlers.handle_exception(e, debug=
|
|
951
|
+
return error_handlers.handle_exception(e, debug=None, request=request) # debug will be checked dynamically
|
|
832
952
|
|
|
833
953
|
async def _dispatch(self, handler: Callable, request: Dict[str, Any], handler_id: int = None) -> Response:
|
|
834
954
|
"""Async dispatch that calls the handler and returns response tuple.
|
django_bolt/auth/__init__.py
CHANGED
|
@@ -10,7 +10,7 @@ from .backends import (
|
|
|
10
10
|
BaseAuthentication,
|
|
11
11
|
JWTAuthentication,
|
|
12
12
|
APIKeyAuthentication,
|
|
13
|
-
SessionAuthentication,
|
|
13
|
+
SessionAuthentication, # Session authentication is not implemented
|
|
14
14
|
AuthContext,
|
|
15
15
|
get_default_authentication_classes,
|
|
16
16
|
)
|
|
@@ -53,7 +53,7 @@ __all__ = [
|
|
|
53
53
|
"BaseAuthentication",
|
|
54
54
|
"JWTAuthentication",
|
|
55
55
|
"APIKeyAuthentication",
|
|
56
|
-
"SessionAuthentication",
|
|
56
|
+
"SessionAuthentication", # Session authentication is not implemented
|
|
57
57
|
"AuthContext",
|
|
58
58
|
"get_default_authentication_classes",
|
|
59
59
|
|
django_bolt/decorators.py
CHANGED
|
@@ -27,7 +27,7 @@ class ActionHandler:
|
|
|
27
27
|
status_code: Optional HTTP status code
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
__slots__ = ('fn', 'methods', 'detail', 'path', 'auth', 'guards', 'response_model', 'status_code')
|
|
30
|
+
__slots__ = ('fn', 'methods', 'detail', 'path', 'auth', 'guards', 'response_model', 'status_code', 'tags', 'summary', 'description')
|
|
31
31
|
|
|
32
32
|
def __init__(
|
|
33
33
|
self,
|
|
@@ -39,6 +39,9 @@ class ActionHandler:
|
|
|
39
39
|
guards: Optional[List[Any]] = None,
|
|
40
40
|
response_model: Optional[Any] = None,
|
|
41
41
|
status_code: Optional[int] = None,
|
|
42
|
+
tags: Optional[List[str]] = None,
|
|
43
|
+
summary: Optional[str] = None,
|
|
44
|
+
description: Optional[str] = None,
|
|
42
45
|
):
|
|
43
46
|
self.fn = fn
|
|
44
47
|
self.methods = [m.upper() for m in methods] # Normalize to uppercase
|
|
@@ -48,6 +51,9 @@ class ActionHandler:
|
|
|
48
51
|
self.guards = guards
|
|
49
52
|
self.response_model = response_model
|
|
50
53
|
self.status_code = status_code
|
|
54
|
+
self.tags = tags
|
|
55
|
+
self.summary = summary
|
|
56
|
+
self.description = description
|
|
51
57
|
|
|
52
58
|
|
|
53
59
|
def __call__(self, *args, **kwargs):
|
|
@@ -68,7 +74,10 @@ def action(
|
|
|
68
74
|
auth: Optional[List[Any]] = None,
|
|
69
75
|
guards: Optional[List[Any]] = None,
|
|
70
76
|
response_model: Optional[Any] = None,
|
|
71
|
-
status_code: Optional[int] = None
|
|
77
|
+
status_code: Optional[int] = None,
|
|
78
|
+
tags: Optional[List[str]] = None,
|
|
79
|
+
summary: Optional[str] = None,
|
|
80
|
+
description: Optional[str] = None,
|
|
72
81
|
) -> Callable:
|
|
73
82
|
"""
|
|
74
83
|
Decorator for ViewSet custom actions (DRF-style).
|
|
@@ -153,7 +162,10 @@ def action(
|
|
|
153
162
|
auth=auth,
|
|
154
163
|
guards=guards,
|
|
155
164
|
response_model=response_model,
|
|
156
|
-
status_code=status_code
|
|
165
|
+
status_code=status_code,
|
|
166
|
+
tags=tags,
|
|
167
|
+
summary=summary,
|
|
168
|
+
description=description,
|
|
157
169
|
)
|
|
158
170
|
|
|
159
171
|
return decorator
|