django-bolt 0.1.0__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 (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.pyd +0 -0
  3. django_bolt/admin/__init__.py +25 -0
  4. django_bolt/admin/admin_detection.py +179 -0
  5. django_bolt/admin/asgi_bridge.py +267 -0
  6. django_bolt/admin/routes.py +91 -0
  7. django_bolt/admin/static.py +155 -0
  8. django_bolt/admin/static_routes.py +111 -0
  9. django_bolt/api.py +1011 -0
  10. django_bolt/apps.py +7 -0
  11. django_bolt/async_collector.py +228 -0
  12. django_bolt/auth/README.md +464 -0
  13. django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
  14. django_bolt/auth/__init__.py +84 -0
  15. django_bolt/auth/backends.py +236 -0
  16. django_bolt/auth/guards.py +224 -0
  17. django_bolt/auth/jwt_utils.py +212 -0
  18. django_bolt/auth/revocation.py +286 -0
  19. django_bolt/auth/token.py +335 -0
  20. django_bolt/binding.py +363 -0
  21. django_bolt/bootstrap.py +77 -0
  22. django_bolt/cli.py +133 -0
  23. django_bolt/compression.py +104 -0
  24. django_bolt/decorators.py +159 -0
  25. django_bolt/dependencies.py +128 -0
  26. django_bolt/error_handlers.py +305 -0
  27. django_bolt/exceptions.py +294 -0
  28. django_bolt/health.py +129 -0
  29. django_bolt/logging/__init__.py +6 -0
  30. django_bolt/logging/config.py +357 -0
  31. django_bolt/logging/middleware.py +296 -0
  32. django_bolt/management/__init__.py +1 -0
  33. django_bolt/management/commands/__init__.py +0 -0
  34. django_bolt/management/commands/runbolt.py +427 -0
  35. django_bolt/middleware/__init__.py +32 -0
  36. django_bolt/middleware/compiler.py +131 -0
  37. django_bolt/middleware/middleware.py +247 -0
  38. django_bolt/openapi/__init__.py +23 -0
  39. django_bolt/openapi/config.py +196 -0
  40. django_bolt/openapi/plugins.py +439 -0
  41. django_bolt/openapi/routes.py +152 -0
  42. django_bolt/openapi/schema_generator.py +581 -0
  43. django_bolt/openapi/spec/__init__.py +68 -0
  44. django_bolt/openapi/spec/base.py +74 -0
  45. django_bolt/openapi/spec/callback.py +24 -0
  46. django_bolt/openapi/spec/components.py +72 -0
  47. django_bolt/openapi/spec/contact.py +21 -0
  48. django_bolt/openapi/spec/discriminator.py +25 -0
  49. django_bolt/openapi/spec/encoding.py +67 -0
  50. django_bolt/openapi/spec/enums.py +41 -0
  51. django_bolt/openapi/spec/example.py +36 -0
  52. django_bolt/openapi/spec/external_documentation.py +21 -0
  53. django_bolt/openapi/spec/header.py +132 -0
  54. django_bolt/openapi/spec/info.py +50 -0
  55. django_bolt/openapi/spec/license.py +28 -0
  56. django_bolt/openapi/spec/link.py +66 -0
  57. django_bolt/openapi/spec/media_type.py +51 -0
  58. django_bolt/openapi/spec/oauth_flow.py +36 -0
  59. django_bolt/openapi/spec/oauth_flows.py +28 -0
  60. django_bolt/openapi/spec/open_api.py +87 -0
  61. django_bolt/openapi/spec/operation.py +105 -0
  62. django_bolt/openapi/spec/parameter.py +147 -0
  63. django_bolt/openapi/spec/path_item.py +78 -0
  64. django_bolt/openapi/spec/paths.py +27 -0
  65. django_bolt/openapi/spec/reference.py +38 -0
  66. django_bolt/openapi/spec/request_body.py +38 -0
  67. django_bolt/openapi/spec/response.py +48 -0
  68. django_bolt/openapi/spec/responses.py +44 -0
  69. django_bolt/openapi/spec/schema.py +678 -0
  70. django_bolt/openapi/spec/security_requirement.py +28 -0
  71. django_bolt/openapi/spec/security_scheme.py +69 -0
  72. django_bolt/openapi/spec/server.py +34 -0
  73. django_bolt/openapi/spec/server_variable.py +32 -0
  74. django_bolt/openapi/spec/tag.py +32 -0
  75. django_bolt/openapi/spec/xml.py +44 -0
  76. django_bolt/pagination.py +669 -0
  77. django_bolt/param_functions.py +49 -0
  78. django_bolt/params.py +337 -0
  79. django_bolt/request_parsing.py +128 -0
  80. django_bolt/responses.py +214 -0
  81. django_bolt/router.py +48 -0
  82. django_bolt/serialization.py +193 -0
  83. django_bolt/status_codes.py +321 -0
  84. django_bolt/testing/__init__.py +10 -0
  85. django_bolt/testing/client.py +274 -0
  86. django_bolt/testing/helpers.py +93 -0
  87. django_bolt/tests/__init__.py +0 -0
  88. django_bolt/tests/admin_tests/__init__.py +1 -0
  89. django_bolt/tests/admin_tests/conftest.py +6 -0
  90. django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
  91. django_bolt/tests/admin_tests/urls.py +9 -0
  92. django_bolt/tests/cbv/__init__.py +0 -0
  93. django_bolt/tests/cbv/test_class_views.py +570 -0
  94. django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
  95. django_bolt/tests/cbv/test_class_views_features.py +1173 -0
  96. django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
  97. django_bolt/tests/conftest.py +165 -0
  98. django_bolt/tests/test_action_decorator.py +399 -0
  99. django_bolt/tests/test_auth_secret_key.py +83 -0
  100. django_bolt/tests/test_decorator_syntax.py +159 -0
  101. django_bolt/tests/test_error_handling.py +481 -0
  102. django_bolt/tests/test_file_response.py +192 -0
  103. django_bolt/tests/test_global_cors.py +172 -0
  104. django_bolt/tests/test_guards_auth.py +441 -0
  105. django_bolt/tests/test_guards_integration.py +303 -0
  106. django_bolt/tests/test_health.py +283 -0
  107. django_bolt/tests/test_integration_validation.py +400 -0
  108. django_bolt/tests/test_json_validation.py +536 -0
  109. django_bolt/tests/test_jwt_auth.py +327 -0
  110. django_bolt/tests/test_jwt_token.py +458 -0
  111. django_bolt/tests/test_logging.py +837 -0
  112. django_bolt/tests/test_logging_merge.py +419 -0
  113. django_bolt/tests/test_middleware.py +492 -0
  114. django_bolt/tests/test_middleware_server.py +230 -0
  115. django_bolt/tests/test_model_viewset.py +323 -0
  116. django_bolt/tests/test_models.py +24 -0
  117. django_bolt/tests/test_pagination.py +1258 -0
  118. django_bolt/tests/test_parameter_validation.py +178 -0
  119. django_bolt/tests/test_syntax.py +626 -0
  120. django_bolt/tests/test_testing_utilities.py +163 -0
  121. django_bolt/tests/test_testing_utilities_simple.py +123 -0
  122. django_bolt/tests/test_viewset_unified.py +346 -0
  123. django_bolt/typing.py +273 -0
  124. django_bolt/views.py +1110 -0
  125. django_bolt-0.1.0.dist-info/METADATA +629 -0
  126. django_bolt-0.1.0.dist-info/RECORD +128 -0
  127. django_bolt-0.1.0.dist-info/WHEEL +4 -0
  128. django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
django_bolt/binding.py ADDED
@@ -0,0 +1,363 @@
1
+ """
2
+ Parameter binding and extraction with pre-compiled extractors.
3
+
4
+ This module provides high-performance parameter extraction using pre-compiled
5
+ extractor functions that avoid runtime type checking.
6
+ """
7
+ import inspect
8
+ import msgspec
9
+ from typing import Any, Dict, List, Tuple, Callable, Optional
10
+ from functools import lru_cache
11
+
12
+ from .typing import is_msgspec_struct, is_optional, unwrap_optional
13
+
14
+ __all__ = [
15
+ "convert_primitive",
16
+ "create_extractor",
17
+ "coerce_to_response_type",
18
+ "coerce_to_response_type_async",
19
+ ]
20
+
21
+
22
+ # Cache for msgspec decoders (performance optimization)
23
+ _DECODER_CACHE: Dict[Any, msgspec.json.Decoder] = {}
24
+
25
+
26
+ def get_msgspec_decoder(type_: Any) -> msgspec.json.Decoder:
27
+ """Get or create a cached msgspec decoder for a type."""
28
+ if type_ not in _DECODER_CACHE:
29
+ _DECODER_CACHE[type_] = msgspec.json.Decoder(type_)
30
+ return _DECODER_CACHE[type_]
31
+
32
+
33
+ def convert_primitive(value: str, annotation: Any) -> Any:
34
+ """
35
+ Convert string value to the appropriate type based on annotation.
36
+
37
+ Args:
38
+ value: Raw string value from request
39
+ annotation: Target type annotation
40
+
41
+ Returns:
42
+ Converted value
43
+ """
44
+ tp = unwrap_optional(annotation)
45
+
46
+ if tp is str or tp is Any or tp is None or tp is inspect._empty:
47
+ return value
48
+
49
+ if tp is int:
50
+ try:
51
+ return int(value)
52
+ except ValueError:
53
+ from .exceptions import HTTPException
54
+ raise HTTPException(422, detail=f"Invalid integer value: '{value}'")
55
+
56
+ if tp is float:
57
+ try:
58
+ return float(value)
59
+ except ValueError:
60
+ from .exceptions import HTTPException
61
+ raise HTTPException(422, detail=f"Invalid float value: '{value}'")
62
+
63
+ if tp is bool:
64
+ v = value.lower()
65
+ if v in ("1", "true", "t", "yes", "y", "on"):
66
+ return True
67
+ if v in ("0", "false", "f", "no", "n", "off"):
68
+ return False
69
+ return bool(value)
70
+
71
+ # Fallback: try msgspec decode for JSON in value
72
+ try:
73
+ return msgspec.json.decode(value.encode())
74
+ except Exception:
75
+ return value
76
+
77
+
78
+ def create_path_extractor(name: str, annotation: Any, alias: Optional[str] = None) -> Callable:
79
+ """Create a pre-compiled extractor for path parameters."""
80
+ key = alias or name
81
+ converter = lambda v: convert_primitive(str(v), annotation)
82
+
83
+ def extract(params_map: Dict[str, Any]) -> Any:
84
+ if key not in params_map:
85
+ raise ValueError(f"Missing required path parameter: {key}")
86
+ return converter(params_map[key])
87
+
88
+ return extract
89
+
90
+
91
+ def create_query_extractor(
92
+ name: str,
93
+ annotation: Any,
94
+ default: Any,
95
+ alias: Optional[str] = None
96
+ ) -> Callable:
97
+ """Create a pre-compiled extractor for query parameters."""
98
+ key = alias or name
99
+ optional = default is not inspect.Parameter.empty or is_optional(annotation)
100
+ converter = lambda v: convert_primitive(str(v), annotation)
101
+
102
+ if optional:
103
+ default_value = None if default is inspect.Parameter.empty else default
104
+ def extract(query_map: Dict[str, Any]) -> Any:
105
+ return converter(query_map[key]) if key in query_map else default_value
106
+ else:
107
+ def extract(query_map: Dict[str, Any]) -> Any:
108
+ if key not in query_map:
109
+ raise ValueError(f"Missing required query parameter: {key}")
110
+ return converter(query_map[key])
111
+
112
+ return extract
113
+
114
+
115
+ def create_header_extractor(
116
+ name: str,
117
+ annotation: Any,
118
+ default: Any,
119
+ alias: Optional[str] = None
120
+ ) -> Callable:
121
+ """Create a pre-compiled extractor for HTTP headers."""
122
+ key = (alias or name).lower()
123
+ optional = default is not inspect.Parameter.empty or is_optional(annotation)
124
+ converter = lambda v: convert_primitive(str(v), annotation)
125
+
126
+ if optional:
127
+ default_value = None if default is inspect.Parameter.empty else default
128
+ def extract(headers_map: Dict[str, str]) -> Any:
129
+ return converter(headers_map[key]) if key in headers_map else default_value
130
+ else:
131
+ def extract(headers_map: Dict[str, str]) -> Any:
132
+ if key not in headers_map:
133
+ raise ValueError(f"Missing required header: {key}")
134
+ return converter(headers_map[key])
135
+
136
+ return extract
137
+
138
+
139
+ def create_cookie_extractor(
140
+ name: str,
141
+ annotation: Any,
142
+ default: Any,
143
+ alias: Optional[str] = None
144
+ ) -> Callable:
145
+ """Create a pre-compiled extractor for cookies."""
146
+ key = alias or name
147
+ optional = default is not inspect.Parameter.empty or is_optional(annotation)
148
+ converter = lambda v: convert_primitive(str(v), annotation)
149
+
150
+ if optional:
151
+ default_value = None if default is inspect.Parameter.empty else default
152
+ def extract(cookies_map: Dict[str, str]) -> Any:
153
+ return converter(cookies_map[key]) if key in cookies_map else default_value
154
+ else:
155
+ def extract(cookies_map: Dict[str, str]) -> Any:
156
+ if key not in cookies_map:
157
+ raise ValueError(f"Missing required cookie: {key}")
158
+ return converter(cookies_map[key])
159
+
160
+ return extract
161
+
162
+
163
+ def create_form_extractor(
164
+ name: str,
165
+ annotation: Any,
166
+ default: Any,
167
+ alias: Optional[str] = None
168
+ ) -> Callable:
169
+ """Create a pre-compiled extractor for form fields."""
170
+ key = alias or name
171
+ optional = default is not inspect.Parameter.empty or is_optional(annotation)
172
+ converter = lambda v: convert_primitive(str(v), annotation)
173
+
174
+ if optional:
175
+ default_value = None if default is inspect.Parameter.empty else default
176
+ def extract(form_map: Dict[str, Any]) -> Any:
177
+ return converter(form_map[key]) if key in form_map else default_value
178
+ else:
179
+ def extract(form_map: Dict[str, Any]) -> Any:
180
+ if key not in form_map:
181
+ raise ValueError(f"Missing required form field: {key}")
182
+ return converter(form_map[key])
183
+
184
+ return extract
185
+
186
+
187
+ def create_file_extractor(
188
+ name: str,
189
+ annotation: Any,
190
+ default: Any,
191
+ alias: Optional[str] = None
192
+ ) -> Callable:
193
+ """Create a pre-compiled extractor for file uploads."""
194
+ key = alias or name
195
+ optional = default is not inspect.Parameter.empty or is_optional(annotation)
196
+
197
+ if optional:
198
+ default_value = None if default is inspect.Parameter.empty else default
199
+ def extract(files_map: Dict[str, Any]) -> Any:
200
+ return files_map.get(key, default_value)
201
+ else:
202
+ def extract(files_map: Dict[str, Any]) -> Any:
203
+ if key not in files_map:
204
+ raise ValueError(f"Missing required file: {key}")
205
+ return files_map[key]
206
+
207
+ return extract
208
+
209
+
210
+ def create_body_extractor(name: str, annotation: Any) -> Callable:
211
+ """
212
+ Create a pre-compiled extractor for request body.
213
+
214
+ Uses cached msgspec decoder for maximum performance.
215
+ Converts msgspec.DecodeError (JSON parsing errors) to RequestValidationError for proper 422 responses.
216
+ """
217
+ from .exceptions import RequestValidationError, parse_msgspec_decode_error
218
+
219
+ if is_msgspec_struct(annotation):
220
+ decoder = get_msgspec_decoder(annotation)
221
+ def extract(body_bytes: bytes) -> Any:
222
+ try:
223
+ return decoder.decode(body_bytes)
224
+ except msgspec.ValidationError:
225
+ # Re-raise ValidationError as-is (field validation errors handled by error_handlers.py)
226
+ # IMPORTANT: Must catch ValidationError BEFORE DecodeError since ValidationError subclasses DecodeError
227
+ raise
228
+ except msgspec.DecodeError as e:
229
+ # JSON parsing error (malformed JSON) - return 422 with error details including line/column
230
+ error_detail = parse_msgspec_decode_error(e, body_bytes)
231
+ raise RequestValidationError(
232
+ errors=[error_detail],
233
+ body=body_bytes,
234
+ ) from e
235
+ else:
236
+ # Fallback to generic msgspec decode
237
+ def extract(body_bytes: bytes) -> Any:
238
+ try:
239
+ return msgspec.json.decode(body_bytes, type=annotation)
240
+ except msgspec.ValidationError:
241
+ # Re-raise ValidationError as-is (field validation errors handled by error_handlers.py)
242
+ # IMPORTANT: Must catch ValidationError BEFORE DecodeError since ValidationError subclasses DecodeError
243
+ raise
244
+ except msgspec.DecodeError as e:
245
+ # JSON parsing error (malformed JSON) - return 422 with error details including line/column
246
+ error_detail = parse_msgspec_decode_error(e, body_bytes)
247
+ raise RequestValidationError(
248
+ errors=[error_detail],
249
+ body=body_bytes,
250
+ ) from e
251
+
252
+ return extract
253
+
254
+
255
+ def create_extractor(field: Dict[str, Any]) -> Callable:
256
+ """
257
+ Create an optimized extractor function for a parameter field.
258
+
259
+ This is a factory that returns a specialized extractor based on the
260
+ parameter source. The returned function is optimized to avoid runtime
261
+ type checking.
262
+
263
+ Args:
264
+ field: Field metadata dictionary
265
+
266
+ Returns:
267
+ Extractor function that takes request data and returns parameter value
268
+ """
269
+ source = field["source"]
270
+ name = field["name"]
271
+ annotation = field["annotation"]
272
+ default = field["default"]
273
+ alias = field.get("alias")
274
+
275
+ # Return appropriate extractor based on source
276
+ if source == "path":
277
+ return create_path_extractor(name, annotation, alias)
278
+ elif source == "query":
279
+ return create_query_extractor(name, annotation, default, alias)
280
+ elif source == "header":
281
+ return create_header_extractor(name, annotation, default, alias)
282
+ elif source == "cookie":
283
+ return create_cookie_extractor(name, annotation, default, alias)
284
+ elif source == "form":
285
+ return create_form_extractor(name, annotation, default, alias)
286
+ elif source == "file":
287
+ return create_file_extractor(name, annotation, default, alias)
288
+ elif source == "body":
289
+ return create_body_extractor(name, annotation)
290
+ elif source == "request":
291
+ # Request object is passed through directly
292
+ return lambda request: request
293
+ else:
294
+ # Fallback for unknown sources
295
+ def extract(*args, **kwargs):
296
+ if default is not inspect.Parameter.empty:
297
+ return default
298
+ raise ValueError(f"Cannot extract parameter {name} with source {source}")
299
+ return extract
300
+
301
+
302
+ async def coerce_to_response_type_async(value: Any, annotation: Any) -> Any:
303
+ """
304
+ Async version that handles Django QuerySets.
305
+
306
+ Args:
307
+ value: Value to coerce
308
+ annotation: Target type annotation
309
+
310
+ Returns:
311
+ Coerced value
312
+ """
313
+ # Check if value is a Django QuerySet
314
+ if hasattr(value, '_iterable_class') and hasattr(value, 'model'):
315
+ # It's a QuerySet - convert to list asynchronously
316
+ result = []
317
+ async for item in value:
318
+ result.append(item)
319
+ value = result
320
+
321
+ return coerce_to_response_type(value, annotation)
322
+
323
+
324
+ def coerce_to_response_type(value: Any, annotation: Any) -> Any:
325
+ """
326
+ Coerce arbitrary Python objects (including Django models) into the
327
+ declared response type using msgspec.
328
+
329
+ Supports:
330
+ - msgspec.Struct: build mapping from attributes if needed
331
+ - list[T]: recursively coerce elements
332
+ - dict/primitive: defer to msgspec.convert
333
+
334
+ Args:
335
+ value: Value to coerce
336
+ annotation: Target type annotation
337
+
338
+ Returns:
339
+ Coerced value
340
+ """
341
+ from typing import get_origin, get_args, List
342
+
343
+ origin = get_origin(annotation)
344
+
345
+ # Handle List[T]
346
+ if origin in (list, List):
347
+ args = get_args(annotation)
348
+ elem_type = args[0] if args else Any
349
+ return [coerce_to_response_type(elem, elem_type) for elem in (value or [])]
350
+
351
+ # Handle Struct
352
+ if is_msgspec_struct(annotation):
353
+ if isinstance(value, annotation):
354
+ return value
355
+ if isinstance(value, dict):
356
+ return msgspec.convert(value, annotation)
357
+ # Build mapping from attributes based on struct annotations
358
+ fields = getattr(annotation, "__annotations__", {})
359
+ mapped = {name: getattr(value, name, None) for name in fields.keys()}
360
+ return msgspec.convert(mapped, annotation)
361
+
362
+ # Default convert path
363
+ return msgspec.convert(value, annotation)
@@ -0,0 +1,77 @@
1
+ import importlib
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from django.conf import settings
7
+
8
+
9
+ def ensure_django_ready() -> dict:
10
+ """Ensure Django is properly configured using the project's settings module."""
11
+ if settings.configured:
12
+ return _info()
13
+
14
+ settings_module = os.getenv("DJANGO_SETTINGS_MODULE")
15
+ if not settings_module:
16
+ # Try to detect settings module from manage.py location
17
+ settings_module = _detect_settings_module()
18
+ if settings_module:
19
+ os.environ["DJANGO_SETTINGS_MODULE"] = settings_module
20
+
21
+ if not settings_module:
22
+ raise RuntimeError(
23
+ "Django settings module not found. Please ensure:\n"
24
+ "1. You are running from a Django project directory (with manage.py)\n"
25
+ "2. DJANGO_SETTINGS_MODULE environment variable is set\n"
26
+ "3. Your project has a valid settings.py file"
27
+ )
28
+
29
+ try:
30
+ importlib.import_module(settings_module)
31
+ import django
32
+ django.setup()
33
+ return _info()
34
+ except ImportError as e:
35
+ raise RuntimeError(
36
+ f"Failed to import Django settings module '{settings_module}': {e}\n"
37
+ "Please check that your settings module exists and is valid."
38
+ )
39
+
40
+
41
+ def _detect_settings_module() -> str | None:
42
+ """Try to auto-detect Django settings module from project structure."""
43
+ # Look for manage.py in current directory or parent directories
44
+ current = Path.cwd()
45
+ for path in [current] + list(current.parents)[:3]: # Check up to 3 levels up
46
+ manage_py = path / "manage.py"
47
+ if manage_py.exists():
48
+ # Read manage.py to find settings module
49
+ content = manage_py.read_text()
50
+ if "DJANGO_SETTINGS_MODULE" in content:
51
+ # Extract the settings module from manage.py
52
+ for line in content.split('\n'):
53
+ if "DJANGO_SETTINGS_MODULE" in line and "setdefault" in line:
54
+ # Parse line like: os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
55
+ parts = line.split("'")
56
+ if len(parts) >= 4:
57
+ return parts[3] # Return the settings module string
58
+ parts = line.split('"')
59
+ if len(parts) >= 4:
60
+ return parts[3]
61
+ return None
62
+
63
+
64
+ def _info() -> dict:
65
+ """Get information about the current Django configuration."""
66
+ from django.conf import settings as dj_settings
67
+ db = dj_settings.DATABASES.get("default", {})
68
+ return {
69
+ "mode": "django_project",
70
+ "debug": bool(getattr(dj_settings, "DEBUG", False)),
71
+ "database": db.get("ENGINE"),
72
+ "database_name": db.get("NAME"),
73
+ "settings_module": os.getenv("DJANGO_SETTINGS_MODULE"),
74
+ "base_dir": str(getattr(dj_settings, "BASE_DIR", "")),
75
+ }
76
+
77
+
django_bolt/cli.py ADDED
@@ -0,0 +1,133 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+ import click
5
+
6
+ from .api import BoltAPI
7
+
8
+
9
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
10
+ def main():
11
+ """Django-Bolt command line interface."""
12
+
13
+
14
+ @main.command()
15
+ def init():
16
+ """Initialize Django-Bolt in an existing Django project."""
17
+ # Find Django project root (look for manage.py)
18
+ current_dir = Path.cwd()
19
+ project_root = None
20
+ for path in [current_dir] + list(current_dir.parents):
21
+ if (path / "manage.py").exists():
22
+ project_root = path
23
+ break
24
+
25
+ if not project_root:
26
+ click.echo("Error: No Django project found (manage.py not found)", err=True)
27
+ click.echo("Please run this command from within a Django project directory.")
28
+ return
29
+
30
+ click.echo(f"Found Django project at: {project_root}")
31
+
32
+ # Find settings.py to determine project name
33
+ settings_files = list(project_root.glob("*/settings.py"))
34
+ if not settings_files:
35
+ click.echo("Error: Could not find settings.py", err=True)
36
+ return
37
+
38
+ project_name = settings_files[0].parent.name
39
+ settings_file = settings_files[0]
40
+
41
+ click.echo(f"Project name: {project_name}")
42
+
43
+ # 1. Add django_bolt to INSTALLED_APPS
44
+ settings_content = settings_file.read_text()
45
+ if "'django_bolt'" not in settings_content and '"django_bolt"' not in settings_content:
46
+ if "INSTALLED_APPS" in settings_content:
47
+ # Find INSTALLED_APPS and add django_bolt
48
+ lines = settings_content.splitlines()
49
+ new_lines = []
50
+ in_installed_apps = False
51
+ added = False
52
+
53
+ for line in lines:
54
+ if "INSTALLED_APPS" in line and "=" in line:
55
+ in_installed_apps = True
56
+ elif in_installed_apps and not added:
57
+ # Look for the first app entry and add django_bolt before it
58
+ if line.strip().startswith(("'", '"')) and not added:
59
+ new_lines.append(' "django_bolt",')
60
+ added = True
61
+ elif "]" in line and not added:
62
+ # End of INSTALLED_APPS, add before closing
63
+ new_lines.append(' "django_bolt",')
64
+ added = True
65
+ in_installed_apps = False
66
+
67
+ new_lines.append(line)
68
+
69
+ if added:
70
+ settings_file.write_text("\n".join(new_lines))
71
+ click.echo("āœ“ Added 'django_bolt' to INSTALLED_APPS")
72
+ else:
73
+ click.echo("Warning: Could not automatically add to INSTALLED_APPS. Please add 'django_bolt' manually.")
74
+ else:
75
+ click.echo("Warning: INSTALLED_APPS not found in settings.py. Please add 'django_bolt' manually.")
76
+ else:
77
+ click.echo("āœ“ 'django_bolt' already in INSTALLED_APPS")
78
+
79
+ # 2. Create api.py template
80
+ api_file = project_root / project_name / "api.py"
81
+ if not api_file.exists():
82
+ api_template = '''"""Django-Bolt API routes."""
83
+ from django_bolt import BoltAPI
84
+ import msgspec
85
+ from typing import Optional
86
+
87
+ api = BoltAPI()
88
+
89
+
90
+ @api.get("/")
91
+ async def root():
92
+ """Root endpoint."""
93
+ return {"message": "Welcome to Django-Bolt!"}
94
+
95
+
96
+ @api.get("/health")
97
+ async def health():
98
+ """Health check endpoint."""
99
+ return {"status": "ok", "service": "django-bolt"}
100
+
101
+
102
+ # Example with path parameters
103
+ @api.get("/items/{item_id}")
104
+ async def get_item(item_id: int, q: Optional[str] = None):
105
+ """Get an item by ID."""
106
+ return {"item_id": item_id, "q": q}
107
+
108
+
109
+ # Example with request body validation using msgspec
110
+ class Item(msgspec.Struct):
111
+ name: str
112
+ price: float
113
+ is_offer: Optional[bool] = None
114
+
115
+
116
+ @api.post("/items")
117
+ async def create_item(item: Item):
118
+ """Create a new item."""
119
+ return {"item": item, "created": True}
120
+ '''
121
+ api_file.write_text(api_template)
122
+ click.echo(f"āœ“ Created {api_file.relative_to(project_root)}")
123
+ else:
124
+ click.echo(f"āœ“ {api_file.relative_to(project_root)} already exists")
125
+
126
+ click.echo("\nšŸš€ Django-Bolt initialization complete!")
127
+ click.echo("\nNext steps:")
128
+ click.echo("1. Run migrations: python manage.py migrate")
129
+ click.echo("2. Start the server: python manage.py runbolt")
130
+ click.echo(f"3. Edit your API routes in {project_name}/api.py")
131
+ click.echo("\nFor more information, visit: https://github.com/yourusername/django-bolt")
132
+
133
+
@@ -0,0 +1,104 @@
1
+ """
2
+ Compression configuration for Django-Bolt.
3
+
4
+ Provides configuration options for response compression (gzip, brotli, zstd).
5
+ Inspired by Litestar's compression config.
6
+ """
7
+ from typing import Optional, Literal
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass
12
+ class CompressionConfig:
13
+ """Configuration for response compression.
14
+
15
+ To enable response compression, pass an instance of this class to the BoltAPI
16
+ constructor using the compression parameter.
17
+
18
+ Args:
19
+ backend: The compression backend to use (default: "brotli")
20
+ If the value is "gzip", "brotli", or "zstd", built-in compression is used.
21
+ minimum_size: Minimum response size in bytes to enable compression (default: 500)
22
+ Responses smaller than this will not be compressed.
23
+ gzip_compress_level: Range [0-9] for gzip compression (default: 9)
24
+ Higher values provide better compression but are slower.
25
+ brotli_quality: Range [0-11] for brotli compression (default: 5)
26
+ Controls compression-speed vs compression-density tradeoff.
27
+ brotli_lgwin: Base 2 logarithm of window size for brotli (default: 22)
28
+ Range is 10 to 24. Larger values can improve compression for large files.
29
+ zstd_level: Range [1-22] for zstd compression (default: 3)
30
+ Higher values provide better compression but are slower.
31
+ gzip_fallback: Use GZIP if the client doesn't support the configured backend (default: True)
32
+
33
+ Examples:
34
+ # Default compression (brotli with gzip fallback)
35
+ api = BoltAPI(compression=CompressionConfig())
36
+
37
+ # Aggressive brotli compression
38
+ api = BoltAPI(compression=CompressionConfig(
39
+ backend="brotli",
40
+ brotli_quality=11,
41
+ minimum_size=256
42
+ ))
43
+
44
+ # Gzip only (maximum compression)
45
+ api = BoltAPI(compression=CompressionConfig(
46
+ backend="gzip",
47
+ gzip_compress_level=9,
48
+ gzip_fallback=False
49
+ ))
50
+
51
+ # Fast zstd compression
52
+ api = BoltAPI(compression=CompressionConfig(
53
+ backend="zstd",
54
+ zstd_level=1
55
+ ))
56
+
57
+ # Disable compression
58
+ api = BoltAPI(compression=None)
59
+ """
60
+ backend: Literal["gzip", "brotli", "zstd"] = "brotli"
61
+ minimum_size: int = 500
62
+ gzip_compress_level: int = 9
63
+ brotli_quality: int = 5
64
+ brotli_lgwin: int = 22
65
+ zstd_level: int = 3
66
+ gzip_fallback: bool = True
67
+
68
+ def __post_init__(self):
69
+ # Validate backend
70
+ valid_backends = {"gzip", "brotli", "zstd"}
71
+ if self.backend not in valid_backends:
72
+ raise ValueError(f"Invalid backend: {self.backend}. Must be one of {valid_backends}")
73
+
74
+ # Validate minimum_size
75
+ if self.minimum_size < 0:
76
+ raise ValueError("minimum_size must be non-negative")
77
+
78
+ # Validate gzip_compress_level
79
+ if not (0 <= self.gzip_compress_level <= 9):
80
+ raise ValueError("gzip_compress_level must be between 0 and 9")
81
+
82
+ # Validate brotli_quality
83
+ if not (0 <= self.brotli_quality <= 11):
84
+ raise ValueError("brotli_quality must be between 0 and 11")
85
+
86
+ # Validate brotli_lgwin
87
+ if not (10 <= self.brotli_lgwin <= 24):
88
+ raise ValueError("brotli_lgwin must be between 10 and 24")
89
+
90
+ # Validate zstd_level
91
+ if not (1 <= self.zstd_level <= 22):
92
+ raise ValueError("zstd_level must be between 1 and 22")
93
+
94
+ def to_rust_config(self) -> dict:
95
+ """Convert to dictionary for passing to Rust."""
96
+ return {
97
+ "backend": self.backend,
98
+ "minimum_size": self.minimum_size,
99
+ "gzip_compress_level": self.gzip_compress_level,
100
+ "brotli_quality": self.brotli_quality,
101
+ "brotli_lgwin": self.brotli_lgwin,
102
+ "zstd_level": self.zstd_level,
103
+ "gzip_fallback": self.gzip_fallback,
104
+ }