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.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.abi3.so +0 -0
- django_bolt/admin/__init__.py +25 -0
- django_bolt/admin/admin_detection.py +179 -0
- django_bolt/admin/asgi_bridge.py +267 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +155 -0
- django_bolt/admin/static_routes.py +111 -0
- django_bolt/api.py +1011 -0
- django_bolt/apps.py +7 -0
- django_bolt/async_collector.py +228 -0
- django_bolt/auth/README.md +464 -0
- django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
- django_bolt/auth/__init__.py +84 -0
- django_bolt/auth/backends.py +236 -0
- django_bolt/auth/guards.py +224 -0
- django_bolt/auth/jwt_utils.py +212 -0
- django_bolt/auth/revocation.py +286 -0
- django_bolt/auth/token.py +335 -0
- django_bolt/binding.py +363 -0
- django_bolt/bootstrap.py +77 -0
- django_bolt/cli.py +133 -0
- django_bolt/compression.py +104 -0
- django_bolt/decorators.py +159 -0
- django_bolt/dependencies.py +128 -0
- django_bolt/error_handlers.py +305 -0
- django_bolt/exceptions.py +294 -0
- django_bolt/health.py +129 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +357 -0
- django_bolt/logging/middleware.py +296 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +427 -0
- django_bolt/middleware/__init__.py +32 -0
- django_bolt/middleware/compiler.py +131 -0
- django_bolt/middleware/middleware.py +247 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +439 -0
- django_bolt/openapi/routes.py +152 -0
- django_bolt/openapi/schema_generator.py +581 -0
- django_bolt/openapi/spec/__init__.py +68 -0
- django_bolt/openapi/spec/base.py +74 -0
- django_bolt/openapi/spec/callback.py +24 -0
- django_bolt/openapi/spec/components.py +72 -0
- django_bolt/openapi/spec/contact.py +21 -0
- django_bolt/openapi/spec/discriminator.py +25 -0
- django_bolt/openapi/spec/encoding.py +67 -0
- django_bolt/openapi/spec/enums.py +41 -0
- django_bolt/openapi/spec/example.py +36 -0
- django_bolt/openapi/spec/external_documentation.py +21 -0
- django_bolt/openapi/spec/header.py +132 -0
- django_bolt/openapi/spec/info.py +50 -0
- django_bolt/openapi/spec/license.py +28 -0
- django_bolt/openapi/spec/link.py +66 -0
- django_bolt/openapi/spec/media_type.py +51 -0
- django_bolt/openapi/spec/oauth_flow.py +36 -0
- django_bolt/openapi/spec/oauth_flows.py +28 -0
- django_bolt/openapi/spec/open_api.py +87 -0
- django_bolt/openapi/spec/operation.py +105 -0
- django_bolt/openapi/spec/parameter.py +147 -0
- django_bolt/openapi/spec/path_item.py +78 -0
- django_bolt/openapi/spec/paths.py +27 -0
- django_bolt/openapi/spec/reference.py +38 -0
- django_bolt/openapi/spec/request_body.py +38 -0
- django_bolt/openapi/spec/response.py +48 -0
- django_bolt/openapi/spec/responses.py +44 -0
- django_bolt/openapi/spec/schema.py +678 -0
- django_bolt/openapi/spec/security_requirement.py +28 -0
- django_bolt/openapi/spec/security_scheme.py +69 -0
- django_bolt/openapi/spec/server.py +34 -0
- django_bolt/openapi/spec/server_variable.py +32 -0
- django_bolt/openapi/spec/tag.py +32 -0
- django_bolt/openapi/spec/xml.py +44 -0
- django_bolt/pagination.py +669 -0
- django_bolt/param_functions.py +49 -0
- django_bolt/params.py +337 -0
- django_bolt/request_parsing.py +128 -0
- django_bolt/responses.py +214 -0
- django_bolt/router.py +48 -0
- django_bolt/serialization.py +193 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +10 -0
- django_bolt/testing/client.py +274 -0
- django_bolt/testing/helpers.py +93 -0
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +1 -0
- django_bolt/tests/admin_tests/conftest.py +6 -0
- django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
- django_bolt/tests/admin_tests/urls.py +9 -0
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +570 -0
- django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
- django_bolt/tests/cbv/test_class_views_features.py +1173 -0
- django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
- django_bolt/tests/conftest.py +165 -0
- django_bolt/tests/test_action_decorator.py +399 -0
- django_bolt/tests/test_auth_secret_key.py +83 -0
- django_bolt/tests/test_decorator_syntax.py +159 -0
- django_bolt/tests/test_error_handling.py +481 -0
- django_bolt/tests/test_file_response.py +192 -0
- django_bolt/tests/test_global_cors.py +172 -0
- django_bolt/tests/test_guards_auth.py +441 -0
- django_bolt/tests/test_guards_integration.py +303 -0
- django_bolt/tests/test_health.py +283 -0
- django_bolt/tests/test_integration_validation.py +400 -0
- django_bolt/tests/test_json_validation.py +536 -0
- django_bolt/tests/test_jwt_auth.py +327 -0
- django_bolt/tests/test_jwt_token.py +458 -0
- django_bolt/tests/test_logging.py +837 -0
- django_bolt/tests/test_logging_merge.py +419 -0
- django_bolt/tests/test_middleware.py +492 -0
- django_bolt/tests/test_middleware_server.py +230 -0
- django_bolt/tests/test_model_viewset.py +323 -0
- django_bolt/tests/test_models.py +24 -0
- django_bolt/tests/test_pagination.py +1258 -0
- django_bolt/tests/test_parameter_validation.py +178 -0
- django_bolt/tests/test_syntax.py +626 -0
- django_bolt/tests/test_testing_utilities.py +163 -0
- django_bolt/tests/test_testing_utilities_simple.py +123 -0
- django_bolt/tests/test_viewset_unified.py +346 -0
- django_bolt/typing.py +273 -0
- django_bolt/views.py +1110 -0
- django_bolt-0.1.0.dist-info/METADATA +629 -0
- django_bolt-0.1.0.dist-info/RECORD +128 -0
- django_bolt-0.1.0.dist-info/WHEEL +4 -0
- django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional, List, Sequence
|
|
2
|
+
from http import HTTPStatus
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BoltException(Exception):
|
|
7
|
+
"""Base exception class for all Django-Bolt exceptions."""
|
|
8
|
+
|
|
9
|
+
detail: str
|
|
10
|
+
|
|
11
|
+
def __init__(self, *args: Any, detail: str = "") -> None:
|
|
12
|
+
"""Initialize BoltException.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
*args: Additional arguments
|
|
16
|
+
detail: Exception detail message
|
|
17
|
+
"""
|
|
18
|
+
str_args = [str(arg) for arg in args if arg]
|
|
19
|
+
if not detail:
|
|
20
|
+
if str_args:
|
|
21
|
+
detail, *str_args = str_args
|
|
22
|
+
elif hasattr(self, "detail"):
|
|
23
|
+
detail = self.detail
|
|
24
|
+
self.detail = detail
|
|
25
|
+
super().__init__(*str_args)
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
if self.detail:
|
|
29
|
+
return f"{self.__class__.__name__} - {self.detail}"
|
|
30
|
+
return self.__class__.__name__
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return " ".join((*self.args, self.detail)).strip()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HTTPException(BoltException):
|
|
37
|
+
"""Base exception for HTTP error responses.
|
|
38
|
+
|
|
39
|
+
These exceptions carry information to construct an HTTP response.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
status_code: int = 500
|
|
43
|
+
"""HTTP status code for this exception."""
|
|
44
|
+
detail: str = ""
|
|
45
|
+
"""Exception details or message."""
|
|
46
|
+
headers: Dict[str, str]
|
|
47
|
+
"""Headers to attach to the response."""
|
|
48
|
+
extra: Dict[str, Any] | List[Any] | None = None
|
|
49
|
+
"""Additional data to include in the response."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
status_code: int | None = None,
|
|
54
|
+
detail: Any | None = None,
|
|
55
|
+
headers: Optional[Dict[str, str]] = None,
|
|
56
|
+
extra: Dict[str, Any] | List[Any] | None = None,
|
|
57
|
+
):
|
|
58
|
+
"""Initialize HTTPException.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
status_code: HTTP status code (defaults to class status_code)
|
|
62
|
+
detail: Exception details or message
|
|
63
|
+
headers: HTTP headers to include in response
|
|
64
|
+
extra: Additional data to include in response
|
|
65
|
+
"""
|
|
66
|
+
# Handle detail
|
|
67
|
+
if detail is None:
|
|
68
|
+
detail = ""
|
|
69
|
+
detail_str = str(detail) if detail else ""
|
|
70
|
+
|
|
71
|
+
super().__init__(detail=detail_str)
|
|
72
|
+
|
|
73
|
+
self.status_code = status_code if status_code is not None else self.status_code
|
|
74
|
+
self.detail = detail_str if detail_str else HTTPStatus(self.status_code).phrase
|
|
75
|
+
self.headers = headers or {}
|
|
76
|
+
self.extra = extra
|
|
77
|
+
|
|
78
|
+
# Update args for better error messages
|
|
79
|
+
self.args = (f"{self.status_code}: {self.detail}",)
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
return f"{self.status_code} - {self.__class__.__name__} - {self.detail}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ValidationException(BoltException, ValueError):
|
|
86
|
+
"""Base exception for validation errors."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, errors: Sequence[Any]) -> None:
|
|
89
|
+
"""Initialize ValidationException.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
errors: Sequence of validation errors
|
|
93
|
+
"""
|
|
94
|
+
self._errors = errors
|
|
95
|
+
super().__init__(detail="Validation error")
|
|
96
|
+
|
|
97
|
+
def errors(self) -> Sequence[Any]:
|
|
98
|
+
"""Return validation errors."""
|
|
99
|
+
return self._errors
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RequestValidationError(ValidationException):
|
|
103
|
+
"""Request data validation error.
|
|
104
|
+
|
|
105
|
+
Raised when request data (body, query params, headers, etc.) fails validation.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
|
109
|
+
"""Initialize RequestValidationError.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
errors: Sequence of validation errors
|
|
113
|
+
body: The request body that failed validation
|
|
114
|
+
"""
|
|
115
|
+
super().__init__(errors)
|
|
116
|
+
self.body = body
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ResponseValidationError(ValidationException):
|
|
120
|
+
"""Response data validation error.
|
|
121
|
+
|
|
122
|
+
Raised when handler return value fails validation against response_model.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
|
126
|
+
"""Initialize ResponseValidationError.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
errors: Sequence of validation errors
|
|
130
|
+
body: The response body that failed validation
|
|
131
|
+
"""
|
|
132
|
+
super().__init__(errors)
|
|
133
|
+
self.body = body
|
|
134
|
+
|
|
135
|
+
def __str__(self) -> str:
|
|
136
|
+
message = f"{len(self._errors)} validation error(s):\n"
|
|
137
|
+
for err in self._errors:
|
|
138
|
+
message += f" {err}\n"
|
|
139
|
+
return message
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# HTTP 4xx Client Error Exceptions
|
|
143
|
+
|
|
144
|
+
class ClientException(HTTPException):
|
|
145
|
+
"""Base class for 4xx client errors."""
|
|
146
|
+
status_code: int = 400
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class BadRequest(ClientException):
|
|
150
|
+
"""400 Bad Request - Invalid request data."""
|
|
151
|
+
status_code = 400
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Unauthorized(ClientException):
|
|
155
|
+
"""401 Unauthorized - Authentication required or failed."""
|
|
156
|
+
status_code = 401
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Forbidden(ClientException):
|
|
160
|
+
"""403 Forbidden - Insufficient permissions."""
|
|
161
|
+
status_code = 403
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class NotFound(ClientException):
|
|
165
|
+
"""404 Not Found - Resource not found."""
|
|
166
|
+
status_code = 404
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class MethodNotAllowed(ClientException):
|
|
170
|
+
"""405 Method Not Allowed - HTTP method not supported for this endpoint."""
|
|
171
|
+
status_code = 405
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class NotAcceptable(ClientException):
|
|
175
|
+
"""406 Not Acceptable - Cannot produce response in requested format."""
|
|
176
|
+
status_code = 406
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Conflict(ClientException):
|
|
180
|
+
"""409 Conflict - Request conflicts with current state."""
|
|
181
|
+
status_code = 409
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Gone(ClientException):
|
|
185
|
+
"""410 Gone - Resource permanently deleted."""
|
|
186
|
+
status_code = 410
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class UnprocessableEntity(ClientException):
|
|
190
|
+
"""422 Unprocessable Entity - Semantic validation error."""
|
|
191
|
+
status_code = 422
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TooManyRequests(ClientException):
|
|
195
|
+
"""429 Too Many Requests - Rate limit exceeded."""
|
|
196
|
+
status_code = 429
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# HTTP 5xx Server Error Exceptions
|
|
200
|
+
|
|
201
|
+
class ServerException(HTTPException):
|
|
202
|
+
"""Base class for 5xx server errors."""
|
|
203
|
+
status_code: int = 500
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class InternalServerError(ServerException):
|
|
207
|
+
"""500 Internal Server Error - Unexpected server error."""
|
|
208
|
+
status_code = 500
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class NotImplemented(ServerException):
|
|
212
|
+
"""501 Not Implemented - Endpoint not implemented."""
|
|
213
|
+
status_code = 501
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class BadGateway(ServerException):
|
|
217
|
+
"""502 Bad Gateway - Invalid response from upstream server."""
|
|
218
|
+
status_code = 502
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ServiceUnavailable(ServerException):
|
|
222
|
+
"""503 Service Unavailable - Server temporarily unavailable."""
|
|
223
|
+
status_code = 503
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class GatewayTimeout(ServerException):
|
|
227
|
+
"""504 Gateway Timeout - Upstream server timeout."""
|
|
228
|
+
status_code = 504
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# Helper functions for better error messages
|
|
232
|
+
|
|
233
|
+
def parse_msgspec_decode_error(error: Exception, body_bytes: bytes) -> Dict[str, Any]:
|
|
234
|
+
"""Parse msgspec.DecodeError to extract line/column information.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
error: The msgspec.DecodeError exception
|
|
238
|
+
body_bytes: The JSON bytes that failed to parse
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Dict with error details including line/column information
|
|
242
|
+
"""
|
|
243
|
+
error_msg = str(error)
|
|
244
|
+
|
|
245
|
+
# Try to extract byte position from error message
|
|
246
|
+
# Format: "JSON is malformed: invalid character (byte 78)"
|
|
247
|
+
match = re.search(r'byte (\d+)', error_msg)
|
|
248
|
+
|
|
249
|
+
if match:
|
|
250
|
+
byte_pos = int(match.group(1))
|
|
251
|
+
|
|
252
|
+
# Calculate line and column from byte position
|
|
253
|
+
lines = body_bytes.split(b'\n')
|
|
254
|
+
current_pos = 0
|
|
255
|
+
line_num = 1
|
|
256
|
+
col_num = 0
|
|
257
|
+
error_line_content = ""
|
|
258
|
+
|
|
259
|
+
for i, line in enumerate(lines, 1):
|
|
260
|
+
line_len = len(line) + 1 # +1 for newline
|
|
261
|
+
if current_pos + line_len > byte_pos:
|
|
262
|
+
line_num = i
|
|
263
|
+
col_num = byte_pos - current_pos + 1 # +1 for human-readable column (1-indexed)
|
|
264
|
+
error_line_content = line.decode('utf-8', errors='replace')
|
|
265
|
+
break
|
|
266
|
+
current_pos += line_len
|
|
267
|
+
|
|
268
|
+
# Build descriptive error message
|
|
269
|
+
msg = f"Invalid JSON at line {line_num}, column {col_num}: {error_msg}"
|
|
270
|
+
if error_line_content:
|
|
271
|
+
msg += f"\n {error_line_content}\n {' ' * (col_num - 1)}^"
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
"type": "json_invalid",
|
|
275
|
+
"loc": ("body", line_num, col_num),
|
|
276
|
+
"msg": msg,
|
|
277
|
+
"input": body_bytes.decode("utf-8", errors="replace")[:200] if body_bytes else "",
|
|
278
|
+
"ctx": {
|
|
279
|
+
"line": line_num,
|
|
280
|
+
"column": col_num,
|
|
281
|
+
"byte_position": byte_pos,
|
|
282
|
+
"error": error_msg,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Fallback if we can't parse the byte position
|
|
287
|
+
return {
|
|
288
|
+
"type": "json_invalid",
|
|
289
|
+
"loc": ("body",),
|
|
290
|
+
"msg": error_msg,
|
|
291
|
+
"input": body_bytes.decode("utf-8", errors="replace")[:100] if body_bytes else "",
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
django_bolt/health.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Health check endpoints for Django-Bolt.
|
|
2
|
+
|
|
3
|
+
Provides standard health check endpoints for monitoring and load balancers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Callable, List, Optional
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HealthCheck:
|
|
11
|
+
"""Health check configuration and helpers."""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self._checks: List[Callable] = []
|
|
15
|
+
|
|
16
|
+
def add_check(self, check_func: Callable) -> None:
|
|
17
|
+
"""Add a custom health check.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
check_func: Async function that returns (bool, str) - (is_healthy, message)
|
|
21
|
+
"""
|
|
22
|
+
self._checks.append(check_func)
|
|
23
|
+
|
|
24
|
+
async def run_checks(self) -> Dict[str, Any]:
|
|
25
|
+
"""Run all configured health checks.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dictionary with health check results
|
|
29
|
+
"""
|
|
30
|
+
results = {}
|
|
31
|
+
all_healthy = True
|
|
32
|
+
|
|
33
|
+
for check in self._checks:
|
|
34
|
+
try:
|
|
35
|
+
is_healthy, message = await check()
|
|
36
|
+
results[check.__name__] = {
|
|
37
|
+
"healthy": is_healthy,
|
|
38
|
+
"message": message,
|
|
39
|
+
}
|
|
40
|
+
if not is_healthy:
|
|
41
|
+
all_healthy = False
|
|
42
|
+
except Exception as e:
|
|
43
|
+
results[check.__name__] = {
|
|
44
|
+
"healthy": False,
|
|
45
|
+
"message": f"Check failed: {str(e)}",
|
|
46
|
+
}
|
|
47
|
+
all_healthy = False
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"status": "healthy" if all_healthy else "unhealthy",
|
|
51
|
+
"checks": results,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Global health check instance
|
|
56
|
+
_health_check = HealthCheck()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def check_database() -> tuple[bool, str]:
|
|
60
|
+
"""Check database connectivity.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (is_healthy, message)
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
from django.db import connection
|
|
67
|
+
from asgiref.sync import sync_to_async
|
|
68
|
+
|
|
69
|
+
# Try a simple query
|
|
70
|
+
await sync_to_async(connection.ensure_connection)()
|
|
71
|
+
|
|
72
|
+
return True, "Database connection OK"
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return False, f"Database error: {str(e)}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Health endpoint handlers
|
|
78
|
+
|
|
79
|
+
async def health_handler() -> Dict[str, str]:
|
|
80
|
+
"""Simple liveness check.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dictionary with status
|
|
84
|
+
"""
|
|
85
|
+
return {"status": "ok"}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def ready_handler() -> Dict[str, Any]:
|
|
89
|
+
"""Readiness check with dependency checks.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dictionary with readiness status and checks
|
|
93
|
+
"""
|
|
94
|
+
# Add database check by default
|
|
95
|
+
if not _health_check._checks:
|
|
96
|
+
_health_check.add_check(check_database)
|
|
97
|
+
|
|
98
|
+
results = await _health_check.run_checks()
|
|
99
|
+
return results
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def register_health_checks(api) -> None:
|
|
103
|
+
"""Register health check endpoints on a BoltAPI instance.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
api: BoltAPI instance
|
|
107
|
+
"""
|
|
108
|
+
api.get("/health")(health_handler)
|
|
109
|
+
api.get("/ready")(ready_handler)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def add_health_check(check_func: Callable) -> None:
|
|
113
|
+
"""Add a custom health check.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
check_func: Async function that returns (bool, str)
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
```python
|
|
120
|
+
from django_bolt.health import add_health_check
|
|
121
|
+
|
|
122
|
+
async def check_redis():
|
|
123
|
+
# Check redis connection
|
|
124
|
+
return True, "Redis OK"
|
|
125
|
+
|
|
126
|
+
add_health_check(check_redis)
|
|
127
|
+
```
|
|
128
|
+
"""
|
|
129
|
+
_health_check.add_check(check_func)
|