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.

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.1.dist-info}/METADATA +179 -197
  16. {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.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.1.dist-info}/WHEEL +0 -0
  56. {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/entry_points.txt +0 -0
@@ -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
- for dp in dep_meta["params"]:
81
- dname = dp["name"]
82
- dan = dp["annotation"]
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(dp, params_map, query_map, headers_map, cookies_map)
88
+ dval = extract_dependency_value(field, params_map, query_map, headers_map, cookies_map)
90
89
 
91
- if dp["kind"] in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
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[dname] = dval
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
- param: Dict[str, Any],
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
- dname = param["name"]
108
- dan = param["annotation"]
109
- dsrc = param["source"]
110
- dalias = param.get("alias")
111
- key = dalias or dname
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]), dan)
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]), dan)
117
- elif dsrc == "header":
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), dan)
122
- elif dsrc == "cookie":
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), dan)
132
+ return convert_primitive(str(raw), field.annotation)
127
133
  else:
128
134
  return None
@@ -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 = msgspec.json.encode(error_body)
42
+ body_bytes = _json.encode(error_body)
42
43
 
43
44
  response_headers = [("content-type", "application/json")]
44
45
  if headers:
@@ -57,7 +57,8 @@ class OpenAPIRenderPlugin(ABC):
57
57
  Returns:
58
58
  The rendered JSON as string.
59
59
  """
60
- return msgspec.json.encode(openapi_schema).decode('utf-8')
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 = '{msgspec.json.encode(self.options).decode()}'
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
- # Get description from docstring
116
- description = None
117
- summary = None
118
- if self.config.use_handler_docstrings and handler.__doc__:
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 = lines[0]
122
- if len(lines) > 1:
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
- # Extract tags (use handler module or class name)
138
- tags = self._extract_tags(handler)
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
- source = field.get("source")
170
- name = field.get("name")
171
- alias = field.get("alias") or name
172
- annotation = field.get("annotation")
173
- default = field.get("default")
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.get("source") in ("form", "file")]
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
- name = field.get("alias") or field.get("name")
233
- annotation = field.get("annotation")
234
- default = field.get("default")
235
- source = field.get("source")
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.get("source") in ("body", "form", "file")
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 = msgspec.json.encode({"v": value})
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 msgspec.json.encode(self.content)
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 msgspec.json.encode(self.data)
90
+ return _json.encode(self.data)
90
91
 
91
92
 
92
93
 
@@ -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 = msgspec.json.encode(validated) if result.media_type == "application/json" else result.to_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 = msgspec.json.encode(validated)
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 = msgspec.json.encode(validated)
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 = msgspec.json.encode(result)
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