django-bolt 0.1.0__cp310-abi3-win_amd64.whl → 0.1.1__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.1.dist-info}/METADATA +179 -197
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.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.1.dist-info}/WHEEL +0 -0
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/entry_points.txt +0 -0
django_bolt/dependencies.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Dependency injection utilities."""
|
|
2
2
|
import inspect
|
|
3
|
-
from typing import Any, Callable, Dict, List
|
|
3
|
+
from typing import Any, Callable, Dict, List, TYPE_CHECKING
|
|
4
4
|
from .params import Depends as DependsMarker
|
|
5
5
|
from .binding import convert_primitive
|
|
6
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .typing import FieldDefinition
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
async def resolve_dependency(
|
|
9
12
|
dep_fn: Callable,
|
|
@@ -77,52 +80,55 @@ async def call_dependency(
|
|
|
77
80
|
dep_args: List[Any] = []
|
|
78
81
|
dep_kwargs: Dict[str, Any] = {}
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
dsrc = dp["source"]
|
|
84
|
-
dalias = dp.get("alias")
|
|
85
|
-
|
|
86
|
-
if dsrc == "request":
|
|
83
|
+
# Use FieldDefinition objects directly
|
|
84
|
+
for field in dep_meta["fields"]:
|
|
85
|
+
if field.source == "request":
|
|
87
86
|
dval = request
|
|
88
87
|
else:
|
|
89
|
-
dval = extract_dependency_value(
|
|
88
|
+
dval = extract_dependency_value(field, params_map, query_map, headers_map, cookies_map)
|
|
90
89
|
|
|
91
|
-
if
|
|
90
|
+
if field.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
|
|
92
91
|
dep_args.append(dval)
|
|
93
92
|
else:
|
|
94
|
-
dep_kwargs[
|
|
93
|
+
dep_kwargs[field.name] = dval
|
|
95
94
|
|
|
96
95
|
return await dep_fn(*dep_args, **dep_kwargs)
|
|
97
96
|
|
|
98
97
|
|
|
99
98
|
def extract_dependency_value(
|
|
100
|
-
|
|
99
|
+
field: "FieldDefinition",
|
|
101
100
|
params_map: Dict[str, Any],
|
|
102
101
|
query_map: Dict[str, Any],
|
|
103
102
|
headers_map: Dict[str, str],
|
|
104
103
|
cookies_map: Dict[str, str]
|
|
105
104
|
) -> Any:
|
|
106
|
-
"""Extract value for a dependency parameter.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
"""Extract value for a dependency parameter using FieldDefinition.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
field: FieldDefinition object describing the parameter
|
|
109
|
+
params_map: Path parameters
|
|
110
|
+
query_map: Query parameters
|
|
111
|
+
headers_map: Request headers
|
|
112
|
+
cookies_map: Request cookies
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Extracted and converted parameter value
|
|
116
|
+
"""
|
|
117
|
+
key = field.alias or field.name
|
|
112
118
|
|
|
113
119
|
if key in params_map:
|
|
114
|
-
return convert_primitive(str(params_map[key]),
|
|
120
|
+
return convert_primitive(str(params_map[key]), field.annotation)
|
|
115
121
|
elif key in query_map:
|
|
116
|
-
return convert_primitive(str(query_map[key]),
|
|
117
|
-
elif
|
|
122
|
+
return convert_primitive(str(query_map[key]), field.annotation)
|
|
123
|
+
elif field.source == "header":
|
|
118
124
|
raw = headers_map.get(key.lower())
|
|
119
125
|
if raw is None:
|
|
120
126
|
raise ValueError(f"Missing required header: {key}")
|
|
121
|
-
return convert_primitive(str(raw),
|
|
122
|
-
elif
|
|
127
|
+
return convert_primitive(str(raw), field.annotation)
|
|
128
|
+
elif field.source == "cookie":
|
|
123
129
|
raw = cookies_map.get(key)
|
|
124
130
|
if raw is None:
|
|
125
131
|
raise ValueError(f"Missing required cookie: {key}")
|
|
126
|
-
return convert_primitive(str(raw),
|
|
132
|
+
return convert_primitive(str(raw), field.annotation)
|
|
127
133
|
else:
|
|
128
134
|
return None
|
django_bolt/error_handlers.py
CHANGED
|
@@ -7,6 +7,7 @@ structured HTTP error responses.
|
|
|
7
7
|
import msgspec
|
|
8
8
|
import traceback
|
|
9
9
|
from typing import Any, Dict, List, Tuple, Optional
|
|
10
|
+
from . import _json
|
|
10
11
|
from .exceptions import (
|
|
11
12
|
HTTPException,
|
|
12
13
|
RequestValidationError,
|
|
@@ -38,7 +39,7 @@ def format_error_response(
|
|
|
38
39
|
if extra is not None:
|
|
39
40
|
error_body["extra"] = extra
|
|
40
41
|
|
|
41
|
-
body_bytes =
|
|
42
|
+
body_bytes = _json.encode(error_body)
|
|
42
43
|
|
|
43
44
|
response_headers = [("content-type", "application/json")]
|
|
44
45
|
if headers:
|
django_bolt/openapi/plugins.py
CHANGED
|
@@ -57,7 +57,8 @@ class OpenAPIRenderPlugin(ABC):
|
|
|
57
57
|
Returns:
|
|
58
58
|
The rendered JSON as string.
|
|
59
59
|
"""
|
|
60
|
-
|
|
60
|
+
from .. import _json
|
|
61
|
+
return _json.encode(openapi_schema).decode('utf-8')
|
|
61
62
|
|
|
62
63
|
@abstractmethod
|
|
63
64
|
def render(self, openapi_schema: Dict[str, Any], schema_url: str) -> str:
|
|
@@ -294,7 +295,7 @@ class ScalarRenderPlugin(OpenAPIRenderPlugin):
|
|
|
294
295
|
if self.options:
|
|
295
296
|
options_script = f"""
|
|
296
297
|
<script>
|
|
297
|
-
document.getElementById('api-reference').dataset.configuration = '{
|
|
298
|
+
document.getElementById('api-reference').dataset.configuration = '{__import__("django_bolt")._json.encode(self.options).decode()}'
|
|
298
299
|
</script>
|
|
299
300
|
"""
|
|
300
301
|
|
|
@@ -17,6 +17,7 @@ from .spec import (
|
|
|
17
17
|
Schema,
|
|
18
18
|
Reference,
|
|
19
19
|
SecurityRequirement,
|
|
20
|
+
Tag,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
if TYPE_CHECKING:
|
|
@@ -48,8 +49,10 @@ class SchemaGenerator:
|
|
|
48
49
|
"""
|
|
49
50
|
openapi = self.config.to_openapi_schema()
|
|
50
51
|
|
|
51
|
-
# Generate path items from routes
|
|
52
|
+
# Generate path items from routes and collect tags
|
|
52
53
|
paths: Dict[str, PathItem] = {}
|
|
54
|
+
collected_tags: set[str] = set()
|
|
55
|
+
|
|
53
56
|
for method, path, handler_id, handler in self.api._routes:
|
|
54
57
|
# Skip OpenAPI docs routes (always excluded)
|
|
55
58
|
if path.startswith(self.config.path):
|
|
@@ -80,6 +83,10 @@ class SchemaGenerator:
|
|
|
80
83
|
handler_id=handler_id,
|
|
81
84
|
)
|
|
82
85
|
|
|
86
|
+
# Collect tags from operation
|
|
87
|
+
if operation.tags:
|
|
88
|
+
collected_tags.update(operation.tags)
|
|
89
|
+
|
|
83
90
|
# Add operation to path item
|
|
84
91
|
method_lower = method.lower()
|
|
85
92
|
setattr(paths[path], method_lower, operation)
|
|
@@ -90,6 +97,9 @@ class SchemaGenerator:
|
|
|
90
97
|
if self.schemas:
|
|
91
98
|
openapi.components.schemas = self.schemas
|
|
92
99
|
|
|
100
|
+
# Collect and merge tags
|
|
101
|
+
openapi.tags = self._collect_tags(collected_tags)
|
|
102
|
+
|
|
93
103
|
return openapi
|
|
94
104
|
|
|
95
105
|
def _create_operation(
|
|
@@ -112,14 +122,17 @@ class SchemaGenerator:
|
|
|
112
122
|
Returns:
|
|
113
123
|
Operation object.
|
|
114
124
|
"""
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
# Prefer explicit metadata over docstring extraction
|
|
126
|
+
summary = meta.get("openapi_summary")
|
|
127
|
+
description = meta.get("openapi_description")
|
|
128
|
+
|
|
129
|
+
# Fallback to docstring if not explicitly set
|
|
130
|
+
if (summary is None or description is None) and self.config.use_handler_docstrings and handler.__doc__:
|
|
119
131
|
doc = inspect.cleandoc(handler.__doc__)
|
|
120
132
|
lines = doc.split("\n", 1)
|
|
121
|
-
summary
|
|
122
|
-
|
|
133
|
+
if summary is None:
|
|
134
|
+
summary = lines[0]
|
|
135
|
+
if description is None and len(lines) > 1:
|
|
123
136
|
description = lines[1].strip()
|
|
124
137
|
|
|
125
138
|
# Extract parameters
|
|
@@ -134,8 +147,11 @@ class SchemaGenerator:
|
|
|
134
147
|
# Extract security requirements
|
|
135
148
|
security = self._extract_security(handler_id)
|
|
136
149
|
|
|
137
|
-
#
|
|
138
|
-
tags =
|
|
150
|
+
# Prefer explicit tags over auto-extracted tags
|
|
151
|
+
tags = meta.get("openapi_tags")
|
|
152
|
+
if tags is None:
|
|
153
|
+
# Fallback to auto-extraction from handler module or class name
|
|
154
|
+
tags = self._extract_tags(handler)
|
|
139
155
|
|
|
140
156
|
operation = Operation(
|
|
141
157
|
summary=summary,
|
|
@@ -166,11 +182,12 @@ class SchemaGenerator:
|
|
|
166
182
|
fields = meta.get("fields", [])
|
|
167
183
|
|
|
168
184
|
for field in fields:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
# Access FieldDefinition attributes directly
|
|
186
|
+
source = field.source
|
|
187
|
+
name = field.name
|
|
188
|
+
alias = field.alias or name
|
|
189
|
+
annotation = field.annotation
|
|
190
|
+
default = field.default
|
|
174
191
|
|
|
175
192
|
# Skip request, body, form, file, and dependency parameters
|
|
176
193
|
if source in ("request", "body", "form", "file", "dependency"):
|
|
@@ -222,17 +239,18 @@ class SchemaGenerator:
|
|
|
222
239
|
if not body_param or not body_type:
|
|
223
240
|
# Check for form/file fields
|
|
224
241
|
fields = meta.get("fields", [])
|
|
225
|
-
form_fields = [f for f in fields if f.
|
|
242
|
+
form_fields = [f for f in fields if f.source in ("form", "file")]
|
|
226
243
|
|
|
227
244
|
if form_fields:
|
|
228
245
|
# Multipart form data
|
|
229
246
|
properties = {}
|
|
230
247
|
required = []
|
|
231
248
|
for field in form_fields:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
249
|
+
# Access FieldDefinition attributes directly
|
|
250
|
+
name = field.alias or field.name
|
|
251
|
+
annotation = field.annotation
|
|
252
|
+
default = field.default
|
|
253
|
+
source = field.source
|
|
236
254
|
|
|
237
255
|
if source == "file":
|
|
238
256
|
# File upload
|
|
@@ -316,7 +334,7 @@ class SchemaGenerator:
|
|
|
316
334
|
if self.config.include_error_responses:
|
|
317
335
|
# Check if request body is present (for 422 validation errors)
|
|
318
336
|
has_request_body = meta.get("body_struct_param") or any(
|
|
319
|
-
f.
|
|
337
|
+
f.source in ("body", "form", "file")
|
|
320
338
|
for f in meta.get("fields", [])
|
|
321
339
|
)
|
|
322
340
|
|
|
@@ -435,6 +453,33 @@ class SchemaGenerator:
|
|
|
435
453
|
|
|
436
454
|
return None
|
|
437
455
|
|
|
456
|
+
def _collect_tags(self, collected_tag_names: set[str]) -> Optional[List[Tag]]:
|
|
457
|
+
"""Collect and merge tags from operations with config tags.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
collected_tag_names: Set of tag names collected from operations.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
List of Tag objects or None if no tags.
|
|
464
|
+
"""
|
|
465
|
+
if not collected_tag_names and not self.config.tags:
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
# Start with existing tags from config
|
|
469
|
+
tag_objects: Dict[str, Tag] = {}
|
|
470
|
+
if self.config.tags:
|
|
471
|
+
for tag in self.config.tags:
|
|
472
|
+
tag_objects[tag.name] = tag
|
|
473
|
+
|
|
474
|
+
# Add tags from operations (if not already defined in config)
|
|
475
|
+
for tag_name in sorted(collected_tag_names):
|
|
476
|
+
if tag_name not in tag_objects:
|
|
477
|
+
# Create Tag object with just the name (no description)
|
|
478
|
+
tag_objects[tag_name] = Tag(name=tag_name)
|
|
479
|
+
|
|
480
|
+
# Return sorted list of Tag objects
|
|
481
|
+
return list(tag_objects.values()) if tag_objects else None
|
|
482
|
+
|
|
438
483
|
def _type_to_schema(
|
|
439
484
|
self, type_annotation: Any, register_component: bool = False
|
|
440
485
|
) -> Schema | Reference:
|
django_bolt/pagination.py
CHANGED
|
@@ -31,6 +31,7 @@ from asgiref.sync import sync_to_async
|
|
|
31
31
|
|
|
32
32
|
from .params import Query
|
|
33
33
|
from .typing import is_optional
|
|
34
|
+
from . import _json
|
|
34
35
|
|
|
35
36
|
__all__ = [
|
|
36
37
|
"PaginationBase",
|
|
@@ -455,7 +456,7 @@ class CursorPagination(PaginationBase):
|
|
|
455
456
|
Returns:
|
|
456
457
|
Base64-encoded cursor string
|
|
457
458
|
"""
|
|
458
|
-
cursor_data =
|
|
459
|
+
cursor_data = _json.encode({"v": value})
|
|
459
460
|
return base64.b64encode(cursor_data).decode('utf-8')
|
|
460
461
|
|
|
461
462
|
def _decode_cursor(self, cursor: str) -> Any:
|
django_bolt/responses.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import msgspec
|
|
2
2
|
from typing import Any, Dict, Optional, List
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from . import _json
|
|
4
5
|
|
|
5
6
|
# Cache for BOLT_ALLOWED_FILE_PATHS - loaded once at server startup
|
|
6
7
|
_ALLOWED_FILE_PATHS_CACHE: Optional[List[Path]] = None
|
|
@@ -70,7 +71,7 @@ class Response:
|
|
|
70
71
|
|
|
71
72
|
def to_bytes(self) -> bytes:
|
|
72
73
|
if self.media_type == "application/json":
|
|
73
|
-
return
|
|
74
|
+
return _json.encode(self.content)
|
|
74
75
|
elif isinstance(self.content, str):
|
|
75
76
|
return self.content.encode()
|
|
76
77
|
elif isinstance(self.content, bytes):
|
|
@@ -86,7 +87,7 @@ class JSON:
|
|
|
86
87
|
self.headers = headers or {}
|
|
87
88
|
|
|
88
89
|
def to_bytes(self) -> bytes:
|
|
89
|
-
return
|
|
90
|
+
return _json.encode(self.data)
|
|
90
91
|
|
|
91
92
|
|
|
92
93
|
|
django_bolt/serialization.py
CHANGED
|
@@ -4,6 +4,7 @@ import msgspec
|
|
|
4
4
|
from typing import Any, Dict, List, Optional, Tuple
|
|
5
5
|
from .responses import Response as ResponseClass, JSON, PlainText, HTML, Redirect, File, FileResponse, StreamingResponse
|
|
6
6
|
from .binding import coerce_to_response_type_async
|
|
7
|
+
from . import _json
|
|
7
8
|
|
|
8
9
|
ResponseTuple = Tuple[int, List[Tuple[str, str]], bytes]
|
|
9
10
|
|
|
@@ -71,7 +72,7 @@ async def serialize_generic_response(result: ResponseClass, response_tp: Optiona
|
|
|
71
72
|
if response_tp is not None:
|
|
72
73
|
try:
|
|
73
74
|
validated = await coerce_to_response_type_async(result.content, response_tp)
|
|
74
|
-
data_bytes =
|
|
75
|
+
data_bytes = _json.encode(validated) if result.media_type == "application/json" else result.to_bytes()
|
|
75
76
|
except Exception as e:
|
|
76
77
|
err = f"Response validation error: {e}"
|
|
77
78
|
return 500, [("content-type", "text/plain; charset=utf-8")], err.encode()
|
|
@@ -98,7 +99,7 @@ async def serialize_json_response(result: JSON, response_tp: Optional[Any]) -> R
|
|
|
98
99
|
if response_tp is not None:
|
|
99
100
|
try:
|
|
100
101
|
validated = await coerce_to_response_type_async(result.data, response_tp)
|
|
101
|
-
data_bytes =
|
|
102
|
+
data_bytes = _json.encode(validated)
|
|
102
103
|
except Exception as e:
|
|
103
104
|
err = f"Response validation error: {e}"
|
|
104
105
|
return 500, [("content-type", "text/plain; charset=utf-8")], err.encode()
|
|
@@ -182,12 +183,12 @@ async def serialize_json_data(result: Any, response_tp: Optional[Any], meta: Dic
|
|
|
182
183
|
if response_tp is not None:
|
|
183
184
|
try:
|
|
184
185
|
validated = await coerce_to_response_type_async(result, response_tp)
|
|
185
|
-
data =
|
|
186
|
+
data = _json.encode(validated)
|
|
186
187
|
except Exception as e:
|
|
187
188
|
err = f"Response validation error: {e}"
|
|
188
189
|
return 500, [("content-type", "text/plain; charset=utf-8")], err.encode()
|
|
189
190
|
else:
|
|
190
|
-
data =
|
|
191
|
+
data = _json.encode(result)
|
|
191
192
|
|
|
192
193
|
status = int(meta.get("default_status_code", 200))
|
|
193
194
|
return status, [("content-type", "application/json")], data
|