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.

Files changed (56) hide show
  1. django_bolt/__init__.py +2 -2
  2. django_bolt/_core.pyd +0 -0
  3. django_bolt/_json.py +169 -0
  4. django_bolt/admin/static_routes.py +15 -21
  5. django_bolt/api.py +181 -61
  6. django_bolt/auth/__init__.py +2 -2
  7. django_bolt/decorators.py +15 -3
  8. django_bolt/dependencies.py +30 -24
  9. django_bolt/error_handlers.py +2 -1
  10. django_bolt/openapi/plugins.py +3 -2
  11. django_bolt/openapi/schema_generator.py +65 -20
  12. django_bolt/pagination.py +2 -1
  13. django_bolt/responses.py +3 -2
  14. django_bolt/serialization.py +5 -4
  15. {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/METADATA +181 -201
  16. {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/RECORD +18 -55
  17. django_bolt/auth/README.md +0 -464
  18. django_bolt/auth/REVOCATION_EXAMPLE.md +0 -391
  19. django_bolt/tests/__init__.py +0 -0
  20. django_bolt/tests/admin_tests/__init__.py +0 -1
  21. django_bolt/tests/admin_tests/conftest.py +0 -6
  22. django_bolt/tests/admin_tests/test_admin_with_django.py +0 -278
  23. django_bolt/tests/admin_tests/urls.py +0 -9
  24. django_bolt/tests/cbv/__init__.py +0 -0
  25. django_bolt/tests/cbv/test_class_views.py +0 -570
  26. django_bolt/tests/cbv/test_class_views_django_orm.py +0 -703
  27. django_bolt/tests/cbv/test_class_views_features.py +0 -1173
  28. django_bolt/tests/cbv/test_class_views_with_client.py +0 -622
  29. django_bolt/tests/conftest.py +0 -165
  30. django_bolt/tests/test_action_decorator.py +0 -399
  31. django_bolt/tests/test_auth_secret_key.py +0 -83
  32. django_bolt/tests/test_decorator_syntax.py +0 -159
  33. django_bolt/tests/test_error_handling.py +0 -481
  34. django_bolt/tests/test_file_response.py +0 -192
  35. django_bolt/tests/test_global_cors.py +0 -172
  36. django_bolt/tests/test_guards_auth.py +0 -441
  37. django_bolt/tests/test_guards_integration.py +0 -303
  38. django_bolt/tests/test_health.py +0 -283
  39. django_bolt/tests/test_integration_validation.py +0 -400
  40. django_bolt/tests/test_json_validation.py +0 -536
  41. django_bolt/tests/test_jwt_auth.py +0 -327
  42. django_bolt/tests/test_jwt_token.py +0 -458
  43. django_bolt/tests/test_logging.py +0 -837
  44. django_bolt/tests/test_logging_merge.py +0 -419
  45. django_bolt/tests/test_middleware.py +0 -492
  46. django_bolt/tests/test_middleware_server.py +0 -230
  47. django_bolt/tests/test_model_viewset.py +0 -323
  48. django_bolt/tests/test_models.py +0 -24
  49. django_bolt/tests/test_pagination.py +0 -1258
  50. django_bolt/tests/test_parameter_validation.py +0 -178
  51. django_bolt/tests/test_syntax.py +0 -626
  52. django_bolt/tests/test_testing_utilities.py +0 -163
  53. django_bolt/tests/test_testing_utilities_simple.py +0 -123
  54. django_bolt/tests/test_viewset_unified.py +0 -346
  55. {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/WHEEL +0 -0
  56. {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
- param: Dict[str, Any],
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 (backward compatibility function).
60
-
61
- This function maintains backward compatibility while using the new
62
- extractor-based system internally.
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 = param["name"]
65
- annotation = param["annotation"]
66
- default = param["default"]
67
- source = param["source"]
68
- alias = param.get("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
- return files_map[key], body_obj, body_loaded
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(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
251
- return self._route_decorator("GET", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
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(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
254
- return self._route_decorator("POST", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
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(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
257
- return self._route_decorator("PUT", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
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(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
260
- return self._route_decorator("PATCH", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
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(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
263
- return self._route_decorator("DELETE", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
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(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
266
- return self._route_decorator("HEAD", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
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(self, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
269
- return self._route_decorator("OPTIONS", path, response_model=response_model, status_code=status_code, guards=guards, auth=auth)
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(self, method: str, path: str, *, response_model: Optional[Any] = None, status_code: Optional[int] = None, guards: Optional[List[Any]] = None, auth: Optional[List[Any]] = None):
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
- # Convert FieldDefinitions to dict format for backward compatibility
721
- # (We'll optimize this away in Phase 4)
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
- for p in meta["params"]:
782
- name = p["name"]
783
- source = p["source"]
784
- depends_marker = p.get("dependency")
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
- dep_fn = depends_marker.dependency if depends_marker else None
790
- if dep_fn is None:
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
- dep_fn, depends_marker, request, dep_cache,
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
- p, request, params_map, query_map, headers_map, cookies_map,
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 p["kind"] in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
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
- body = msgspec.json.encode({"detail": he.detail})
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=False, request=request) # debug will be checked dynamically
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.
@@ -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