django-bolt 0.1.0__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.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.abi3.so +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
@@ -0,0 +1,159 @@
1
+ """
2
+ Decorators for Django-Bolt.
3
+
4
+ Provides decorators for ViewSet custom actions similar to Django REST Framework's @action decorator.
5
+ """
6
+ from typing import Any, Callable, List, Optional
7
+
8
+
9
+ class ActionHandler:
10
+ """
11
+ Marker class for ViewSet custom actions decorated with @action.
12
+
13
+ When @action is used inside a ViewSet class, it returns an ActionHandler instance
14
+ that stores metadata about the action. The api.viewset() method discovers these
15
+ ActionHandler instances and auto-generates routes.
16
+
17
+ Similar to Django REST Framework's @action decorator approach.
18
+
19
+ Attributes:
20
+ fn: The wrapped function
21
+ methods: List of HTTP methods (e.g., ["GET", "POST"])
22
+ detail: Whether this is a detail (instance-level) or list (collection-level) action
23
+ path: Custom path segment (defaults to function name)
24
+ auth: Optional authentication backends
25
+ guards: Optional permission guards
26
+ response_model: Optional response model for serialization
27
+ status_code: Optional HTTP status code
28
+ """
29
+
30
+ __slots__ = ('fn', 'methods', 'detail', 'path', 'auth', 'guards', 'response_model', 'status_code')
31
+
32
+ def __init__(
33
+ self,
34
+ fn: Callable,
35
+ methods: List[str],
36
+ detail: bool,
37
+ path: Optional[str] = None,
38
+ auth: Optional[List[Any]] = None,
39
+ guards: Optional[List[Any]] = None,
40
+ response_model: Optional[Any] = None,
41
+ status_code: Optional[int] = None,
42
+ ):
43
+ self.fn = fn
44
+ self.methods = [m.upper() for m in methods] # Normalize to uppercase
45
+ self.detail = detail
46
+ self.path = path or fn.__name__ # Default to function name
47
+ self.auth = auth
48
+ self.guards = guards
49
+ self.response_model = response_model
50
+ self.status_code = status_code
51
+
52
+
53
+ def __call__(self, *args, **kwargs):
54
+ """Make the handler callable (delegates to wrapped function)."""
55
+ return self.fn(*args, **kwargs)
56
+
57
+ def __repr__(self):
58
+ methods_str = '|'.join(self.methods)
59
+ detail_str = 'detail' if self.detail else 'list'
60
+ return f"ActionHandler({methods_str}, {detail_str}, path={self.path}, fn={self.fn.__name__})"
61
+
62
+
63
+ def action(
64
+ methods: List[str],
65
+ detail: bool,
66
+ path: Optional[str] = None,
67
+ *,
68
+ auth: Optional[List[Any]] = None,
69
+ guards: Optional[List[Any]] = None,
70
+ response_model: Optional[Any] = None,
71
+ status_code: Optional[int] = None
72
+ ) -> Callable:
73
+ """
74
+ Decorator for ViewSet custom actions (DRF-style).
75
+
76
+ Marks a ViewSet method as a custom action with automatic route generation.
77
+
78
+ Auto-generated paths:
79
+ - detail=True (instance-level): /{resource}/{pk}/{action_name}
80
+ Example: @action(methods=["POST"], detail=True) -> POST /users/{id}/activate
81
+
82
+ - detail=False (collection-level): /{resource}/{action_name}
83
+ Example: @action(methods=["GET"], detail=False) -> GET /users/active
84
+
85
+ Multiple methods on single action:
86
+ - @action(methods=["GET", "POST"], detail=True, path="preferences")
87
+ Generates both: GET /users/{id}/preferences and POST /users/{id}/preferences
88
+
89
+ Args:
90
+ methods: List of HTTP methods (e.g., ["GET"], ["POST"], ["GET", "POST"])
91
+ detail: True for instance-level (requires pk), False for collection-level
92
+ path: Optional custom action name (defaults to function name)
93
+ auth: Optional authentication backends (overrides class-level auth)
94
+ guards: Optional permission guards (overrides class-level guards)
95
+ response_model: Optional response model for serialization
96
+ status_code: Optional HTTP status code
97
+
98
+ Returns:
99
+ ActionHandler instance that wraps the function with metadata
100
+
101
+ Example:
102
+ @api.viewset("/users")
103
+ class UserViewSet(ViewSet):
104
+ async def list(self, request) -> list[UserMini]:
105
+ return User.objects.all()[:100]
106
+
107
+ # Instance-level action: POST /users/{id}/activate
108
+ @action(methods=["POST"], detail=True)
109
+ async def activate(self, request, id: int) -> UserFull:
110
+ user = await User.objects.aget(id=id)
111
+ user.is_active = True
112
+ await user.asave()
113
+ return user
114
+
115
+ # Collection-level action: GET /users/active
116
+ @action(methods=["GET"], detail=False)
117
+ async def active(self, request) -> list[UserMini]:
118
+ return User.objects.filter(is_active=True)[:100]
119
+
120
+ # Custom path: GET/POST /users/{id}/preferences
121
+ @action(methods=["GET", "POST"], detail=True, path="preferences")
122
+ async def user_preferences(self, request, id: int, data: dict | None = None):
123
+ if data: # POST
124
+ # update preferences
125
+ pass
126
+ else: # GET
127
+ # return preferences
128
+ pass
129
+
130
+ Notes:
131
+ - Actions inherit class-level auth and guards unless explicitly overridden
132
+ - The function must be async
133
+ - Path parameters are automatically extracted from the route
134
+ - For detail=True actions, the lookup field parameter (e.g., 'id', 'pk') is required
135
+ """
136
+ def decorator(fn: Callable) -> ActionHandler:
137
+ """Wrap the function with ActionHandler metadata."""
138
+ # Validate methods
139
+ valid_methods = {'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'}
140
+ for method in methods:
141
+ if method.upper() not in valid_methods:
142
+ raise ValueError(
143
+ f"Invalid HTTP method '{method}'. "
144
+ f"Valid methods: {', '.join(sorted(valid_methods))}"
145
+ )
146
+
147
+ # Create and return ActionHandler
148
+ return ActionHandler(
149
+ fn=fn,
150
+ methods=methods,
151
+ detail=detail,
152
+ path=path,
153
+ auth=auth,
154
+ guards=guards,
155
+ response_model=response_model,
156
+ status_code=status_code
157
+ )
158
+
159
+ return decorator
@@ -0,0 +1,128 @@
1
+ """Dependency injection utilities."""
2
+ import inspect
3
+ from typing import Any, Callable, Dict, List
4
+ from .params import Depends as DependsMarker
5
+ from .binding import convert_primitive
6
+
7
+
8
+ async def resolve_dependency(
9
+ dep_fn: Callable,
10
+ depends_marker: DependsMarker,
11
+ request: Dict[str, Any],
12
+ dep_cache: Dict[Any, Any],
13
+ params_map: Dict[str, Any],
14
+ query_map: Dict[str, Any],
15
+ headers_map: Dict[str, str],
16
+ cookies_map: Dict[str, str],
17
+ handler_meta: Dict[Callable, Dict[str, Any]],
18
+ compile_binder: Callable,
19
+ http_method: str,
20
+ path: str
21
+ ) -> Any:
22
+ """
23
+ Resolve a dependency injection.
24
+
25
+ Args:
26
+ dep_fn: Dependency function to resolve
27
+ depends_marker: Depends marker with cache settings
28
+ request: Request dict
29
+ dep_cache: Cache for resolved dependencies
30
+ params_map: Path parameters
31
+ query_map: Query parameters
32
+ headers_map: Request headers
33
+ cookies_map: Request cookies
34
+ handler_meta: Metadata cache for handlers
35
+ compile_binder: Function to compile parameter binding metadata
36
+ http_method: HTTP method of the handler using this dependency
37
+ path: Path of the handler using this dependency
38
+
39
+ Returns:
40
+ Resolved dependency value
41
+ """
42
+ if depends_marker.use_cache and dep_fn in dep_cache:
43
+ return dep_cache[dep_fn]
44
+
45
+ dep_meta = handler_meta.get(dep_fn)
46
+ if dep_meta is None:
47
+ # Compile dependency metadata with the actual HTTP method and path
48
+ # Dependencies MUST be validated against HTTP method constraints
49
+ # e.g., a dependency with Body() can't be used in GET handlers
50
+ dep_meta = compile_binder(dep_fn, http_method, path)
51
+ handler_meta[dep_fn] = dep_meta
52
+
53
+ if dep_meta.get("mode") == "request_only":
54
+ value = await dep_fn(request)
55
+ else:
56
+ value = await call_dependency(
57
+ dep_fn, dep_meta, request, params_map,
58
+ query_map, headers_map, cookies_map
59
+ )
60
+
61
+ if depends_marker.use_cache:
62
+ dep_cache[dep_fn] = value
63
+
64
+ return value
65
+
66
+
67
+ async def call_dependency(
68
+ dep_fn: Callable,
69
+ dep_meta: Dict[str, Any],
70
+ request: Dict[str, Any],
71
+ params_map: Dict[str, Any],
72
+ query_map: Dict[str, Any],
73
+ headers_map: Dict[str, str],
74
+ cookies_map: Dict[str, str]
75
+ ) -> Any:
76
+ """Call a dependency function with resolved parameters."""
77
+ dep_args: List[Any] = []
78
+ dep_kwargs: Dict[str, Any] = {}
79
+
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":
87
+ dval = request
88
+ else:
89
+ dval = extract_dependency_value(dp, params_map, query_map, headers_map, cookies_map)
90
+
91
+ if dp["kind"] in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
92
+ dep_args.append(dval)
93
+ else:
94
+ dep_kwargs[dname] = dval
95
+
96
+ return await dep_fn(*dep_args, **dep_kwargs)
97
+
98
+
99
+ def extract_dependency_value(
100
+ param: Dict[str, Any],
101
+ params_map: Dict[str, Any],
102
+ query_map: Dict[str, Any],
103
+ headers_map: Dict[str, str],
104
+ cookies_map: Dict[str, str]
105
+ ) -> 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
112
+
113
+ if key in params_map:
114
+ return convert_primitive(str(params_map[key]), dan)
115
+ elif key in query_map:
116
+ return convert_primitive(str(query_map[key]), dan)
117
+ elif dsrc == "header":
118
+ raw = headers_map.get(key.lower())
119
+ if raw is None:
120
+ raise ValueError(f"Missing required header: {key}")
121
+ return convert_primitive(str(raw), dan)
122
+ elif dsrc == "cookie":
123
+ raw = cookies_map.get(key)
124
+ if raw is None:
125
+ raise ValueError(f"Missing required cookie: {key}")
126
+ return convert_primitive(str(raw), dan)
127
+ else:
128
+ return None
@@ -0,0 +1,305 @@
1
+ """Error handlers for Django-Bolt.
2
+
3
+ Provides default exception handlers that convert Python exceptions into
4
+ structured HTTP error responses.
5
+ """
6
+
7
+ import msgspec
8
+ import traceback
9
+ from typing import Any, Dict, List, Tuple, Optional
10
+ from .exceptions import (
11
+ HTTPException,
12
+ RequestValidationError,
13
+ ResponseValidationError,
14
+ ValidationException,
15
+ InternalServerError,
16
+ )
17
+
18
+
19
+ def format_error_response(
20
+ status_code: int,
21
+ detail: Any,
22
+ headers: Optional[Dict[str, str]] = None,
23
+ extra: Optional[Dict[str, Any] | List[Any]] = None,
24
+ ) -> Tuple[int, List[Tuple[str, str]], bytes]:
25
+ """Format an error response.
26
+
27
+ Args:
28
+ status_code: HTTP status code
29
+ detail: Error detail (string or structured data)
30
+ headers: Optional HTTP headers
31
+ extra: Optional extra data to include in response
32
+
33
+ Returns:
34
+ Tuple of (status_code, headers, body)
35
+ """
36
+ error_body: Dict[str, Any] = {"detail": detail}
37
+
38
+ if extra is not None:
39
+ error_body["extra"] = extra
40
+
41
+ body_bytes = msgspec.json.encode(error_body)
42
+
43
+ response_headers = [("content-type", "application/json")]
44
+ if headers:
45
+ response_headers.extend(headers.items())
46
+
47
+ return status_code, response_headers, body_bytes
48
+
49
+
50
+ def http_exception_handler(exc: HTTPException) -> Tuple[int, List[Tuple[str, str]], bytes]:
51
+ """Handle HTTPException and convert to error response.
52
+
53
+ Args:
54
+ exc: HTTPException instance
55
+
56
+ Returns:
57
+ Tuple of (status_code, headers, body)
58
+ """
59
+ return format_error_response(
60
+ status_code=exc.status_code,
61
+ detail=exc.detail,
62
+ headers=exc.headers,
63
+ extra=exc.extra,
64
+ )
65
+
66
+
67
+ def msgspec_validation_error_to_dict(error: msgspec.ValidationError) -> List[Dict[str, Any]]:
68
+ """Convert msgspec ValidationError to structured error list.
69
+
70
+ Args:
71
+ error: msgspec.ValidationError instance
72
+
73
+ Returns:
74
+ List of error dictionaries with 'loc', 'msg', 'type' fields
75
+ """
76
+ # msgspec.ValidationError doesn't provide structured error info like pydantic
77
+ # We'll do our best to parse the error message
78
+ error_msg = str(error)
79
+
80
+ # Try to extract field path from error message
81
+ # Example: "Expected `int`, got `str` - at `$[0].age`"
82
+ # Example: "Object missing required field `name`"
83
+
84
+ errors = []
85
+
86
+ # Check if error message contains field location
87
+ if " - at `" in error_msg:
88
+ msg_part, loc_part = error_msg.split(" - at `", 1)
89
+ loc_path = loc_part.rstrip("`")
90
+ # Parse location like $[0].age into ["body", 0, "age"]
91
+ loc_parts = ["body"]
92
+ # Simple parsing - can be improved
93
+ loc_parts.append(loc_path.replace("$", "").replace("[", ".").replace("]", "").strip("."))
94
+ elif "missing required field" in error_msg.lower():
95
+ # Extract field name from message
96
+ import re
97
+ match = re.search(r"`(\w+)`", error_msg)
98
+ field = match.group(1) if match else "unknown"
99
+ errors.append({
100
+ "loc": ["body", field],
101
+ "msg": error_msg,
102
+ "type": "missing_field",
103
+ })
104
+ return errors
105
+ else:
106
+ # Generic error without location
107
+ errors.append({
108
+ "loc": ["body"],
109
+ "msg": error_msg,
110
+ "type": "validation_error",
111
+ })
112
+ return errors
113
+
114
+ errors.append({
115
+ "loc": loc_parts if isinstance(loc_parts, list) else ["body"],
116
+ "msg": error_msg.split(" - at `")[0] if " - at `" in error_msg else error_msg,
117
+ "type": "validation_error",
118
+ })
119
+
120
+ return errors
121
+
122
+
123
+ def request_validation_error_handler(
124
+ exc: RequestValidationError,
125
+ ) -> Tuple[int, List[Tuple[str, str]], bytes]:
126
+ """Handle RequestValidationError and convert to 422 response.
127
+
128
+ Args:
129
+ exc: RequestValidationError instance
130
+
131
+ Returns:
132
+ Tuple of (status_code, headers, body)
133
+ """
134
+ errors = exc.errors()
135
+
136
+ # Convert errors to structured format if needed
137
+ formatted_errors = []
138
+ for error in errors:
139
+ if isinstance(error, dict):
140
+ formatted_errors.append(error)
141
+ elif isinstance(error, msgspec.ValidationError):
142
+ formatted_errors.extend(msgspec_validation_error_to_dict(error))
143
+ else:
144
+ # Generic error
145
+ formatted_errors.append({
146
+ "loc": ["body"],
147
+ "msg": str(error),
148
+ "type": "validation_error",
149
+ })
150
+
151
+ return format_error_response(
152
+ status_code=422,
153
+ detail=formatted_errors,
154
+ )
155
+
156
+
157
+ def response_validation_error_handler(
158
+ exc: ResponseValidationError,
159
+ ) -> Tuple[int, List[Tuple[str, str]], bytes]:
160
+ """Handle ResponseValidationError and convert to 500 response.
161
+
162
+ Args:
163
+ exc: ResponseValidationError instance
164
+
165
+ Returns:
166
+ Tuple of (status_code, headers, body)
167
+ """
168
+ # Log the error (if logging is configured)
169
+ errors = exc.errors()
170
+
171
+ formatted_errors = []
172
+ for error in errors:
173
+ if isinstance(error, dict):
174
+ formatted_errors.append(error)
175
+ elif isinstance(error, msgspec.ValidationError):
176
+ formatted_errors.extend(msgspec_validation_error_to_dict(error))
177
+ else:
178
+ formatted_errors.append({
179
+ "loc": ["response"],
180
+ "msg": str(error),
181
+ "type": "validation_error",
182
+ })
183
+
184
+ return format_error_response(
185
+ status_code=500,
186
+ detail="Response validation error",
187
+ extra={"validation_errors": formatted_errors},
188
+ )
189
+
190
+
191
+ def generic_exception_handler(
192
+ exc: Exception,
193
+ debug: bool = False,
194
+ request: Optional[Any] = None, # noqa: ARG001 - kept for API compatibility
195
+ ) -> Tuple[int, List[Tuple[str, str]], bytes]:
196
+ """Handle generic exceptions and convert to 500 response.
197
+
198
+ Args:
199
+ exc: Exception instance
200
+ debug: Whether to include traceback in response
201
+ request: Optional request dict or Django request object for ExceptionReporter
202
+
203
+ Returns:
204
+ Tuple of (status_code, headers, body)
205
+ """
206
+ detail = "Internal Server Error"
207
+ extra = None
208
+
209
+ if debug:
210
+ # Try to use Django's ExceptionReporter HTML page
211
+ try:
212
+ from django.views.debug import ExceptionReporter
213
+
214
+ # ExceptionReporter works fine with None request (avoids URL resolution issues)
215
+ reporter = ExceptionReporter(None, type(exc), exc, exc.__traceback__)
216
+ html_content = reporter.get_traceback_html()
217
+
218
+ # Return HTML response instead of JSON
219
+ return (
220
+ 500,
221
+ [("content-type", "text/html; charset=utf-8")],
222
+ html_content.encode("utf-8")
223
+ )
224
+ except Exception:
225
+ # Fallback to standard traceback formatting in JSON
226
+ pass
227
+
228
+ # Fallback to JSON with traceback
229
+ tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
230
+ # Split into individual lines for better JSON display, stripping trailing newlines
231
+ tb_formatted = [line.rstrip('\n') for line in ''.join(tb_lines).split('\n') if line.strip()]
232
+ extra = {
233
+ "exception": str(exc),
234
+ "exception_type": type(exc).__name__,
235
+ "traceback": tb_formatted,
236
+ }
237
+ detail = f"{type(exc).__name__}: {str(exc)}"
238
+
239
+ return format_error_response(
240
+ status_code=500,
241
+ detail=detail,
242
+ extra=extra,
243
+ )
244
+
245
+
246
+ def handle_exception(
247
+ exc: Exception,
248
+ debug: Optional[bool] = None,
249
+ request: Optional[Any] = None,
250
+ ) -> Tuple[int, List[Tuple[str, str]], bytes]:
251
+ """Main exception handler that routes to specific handlers.
252
+
253
+ Args:
254
+ exc: Exception instance
255
+ debug: Whether to include debug information. If None, will check Django DEBUG setting.
256
+ If explicitly False, will not show debug info even if Django DEBUG=True.
257
+ request: Optional request object for Django ExceptionReporter
258
+
259
+ Returns:
260
+ Tuple of (status_code, headers, body)
261
+ """
262
+ # Check Django's DEBUG setting dynamically only if debug is not explicitly set
263
+ if debug is None:
264
+ try:
265
+ from django.conf import settings
266
+ if settings.configured:
267
+ debug = settings.DEBUG
268
+ else:
269
+ debug = False
270
+ except (ImportError, AttributeError):
271
+ debug = False
272
+
273
+ if isinstance(exc, HTTPException):
274
+ return http_exception_handler(exc)
275
+ elif isinstance(exc, RequestValidationError):
276
+ return request_validation_error_handler(exc)
277
+ elif isinstance(exc, ResponseValidationError):
278
+ return response_validation_error_handler(exc)
279
+ elif isinstance(exc, ValidationException):
280
+ # Generic validation exception
281
+ return request_validation_error_handler(
282
+ RequestValidationError(exc.errors())
283
+ )
284
+ elif isinstance(exc, msgspec.ValidationError):
285
+ # Direct msgspec validation error
286
+ errors = msgspec_validation_error_to_dict(exc)
287
+ return format_error_response(
288
+ status_code=422,
289
+ detail=errors,
290
+ )
291
+ elif isinstance(exc, FileNotFoundError):
292
+ # FileNotFoundError from FileResponse - return 404
293
+ return format_error_response(
294
+ status_code=404,
295
+ detail=str(exc) or "File not found",
296
+ )
297
+ elif isinstance(exc, PermissionError):
298
+ # PermissionError from FileResponse path validation - return 403
299
+ return format_error_response(
300
+ status_code=403,
301
+ detail=str(exc) or "Permission denied",
302
+ )
303
+ else:
304
+ # Generic exception
305
+ return generic_exception_handler(exc, debug=debug, request=request)