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.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.pyd +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,439 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Any, List, Union
|
|
5
|
+
import msgspec
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Dict
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
"OpenAPIRenderPlugin",
|
|
12
|
+
"JsonRenderPlugin",
|
|
13
|
+
"YamlRenderPlugin",
|
|
14
|
+
"SwaggerRenderPlugin",
|
|
15
|
+
"RedocRenderPlugin",
|
|
16
|
+
"ScalarRenderPlugin",
|
|
17
|
+
"RapidocRenderPlugin",
|
|
18
|
+
"StoplightRenderPlugin",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_favicon_url = "https://cdn.jsdelivr.net/gh/FarhanAliRaza/django-bolt@master/docs/favicon.png"
|
|
22
|
+
_default_favicon = f"<link rel='icon' type='image/png' href='{_favicon_url}'>"
|
|
23
|
+
_default_style = "<style>body { margin: 0; padding: 0 }</style>"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OpenAPIRenderPlugin(ABC):
|
|
27
|
+
"""Base class for OpenAPI UI render plugins."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
path: Union[str, List[str]],
|
|
33
|
+
media_type: str = "text/html; charset=utf-8",
|
|
34
|
+
favicon: str = _default_favicon,
|
|
35
|
+
style: str = _default_style,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize the OpenAPI UI render plugin.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
path: Path(s) to serve the UI at (relative to openapi_config.path).
|
|
41
|
+
media_type: Media type for the response.
|
|
42
|
+
favicon: HTML <link> tag for the favicon.
|
|
43
|
+
style: Base styling of the html body.
|
|
44
|
+
"""
|
|
45
|
+
self.paths = [path] if isinstance(path, str) else list(path)
|
|
46
|
+
self.media_type = media_type
|
|
47
|
+
self.favicon = favicon
|
|
48
|
+
self.style = style
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def render_json(openapi_schema: Dict[str, Any]) -> str:
|
|
52
|
+
"""Render the OpenAPI schema as JSON string.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
openapi_schema: The OpenAPI schema as a dictionary.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The rendered JSON as string.
|
|
59
|
+
"""
|
|
60
|
+
return msgspec.json.encode(openapi_schema).decode('utf-8')
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def render(self, openapi_schema: Dict[str, Any], schema_url: str) -> str:
|
|
64
|
+
"""Render the OpenAPI UI.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
openapi_schema: The OpenAPI schema as a dictionary.
|
|
68
|
+
schema_url: URL to the OpenAPI JSON schema.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The rendered HTML or data as string.
|
|
72
|
+
"""
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
def has_path(self, path: str) -> bool:
|
|
76
|
+
"""Check if the plugin serves a specific path.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
path: The path to check.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if the plugin has the path, False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
return path in self.paths
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class JsonRenderPlugin(OpenAPIRenderPlugin):
|
|
88
|
+
"""Render the OpenAPI schema as JSON."""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
path: Union[str, List[str]] = "/openapi.json",
|
|
94
|
+
media_type: str = "application/vnd.oai.openapi+json",
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
) -> None:
|
|
97
|
+
super().__init__(path=path, media_type=media_type, **kwargs)
|
|
98
|
+
|
|
99
|
+
def render(self, openapi_schema: Dict[str, Any], schema_url: str) -> Dict[str, Any]:
|
|
100
|
+
"""Render OpenAPI schema as dict.
|
|
101
|
+
|
|
102
|
+
Returns the schema dict directly so django-bolt's serialization
|
|
103
|
+
layer can handle it with proper JSON encoding.
|
|
104
|
+
"""
|
|
105
|
+
return openapi_schema
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class YamlRenderPlugin(OpenAPIRenderPlugin):
|
|
109
|
+
"""Render the OpenAPI schema as YAML."""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
path: Union[str, List[str]] = ["/openapi.yaml", "/openapi.yml"],
|
|
115
|
+
media_type: str = "text/yaml; charset=utf-8",
|
|
116
|
+
**kwargs: Any,
|
|
117
|
+
) -> None:
|
|
118
|
+
super().__init__(path=path, media_type=media_type, **kwargs)
|
|
119
|
+
|
|
120
|
+
def render(self, openapi_schema: Dict[str, Any], schema_url: str) -> str:
|
|
121
|
+
"""Render OpenAPI schema as YAML."""
|
|
122
|
+
try:
|
|
123
|
+
import yaml
|
|
124
|
+
return yaml.dump(openapi_schema, default_flow_style=False)
|
|
125
|
+
except ImportError:
|
|
126
|
+
# Fallback to JSON if PyYAML not installed
|
|
127
|
+
return "# PyYAML not installed. Install with: pip install pyyaml\n" + self.render_json(openapi_schema)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SwaggerRenderPlugin(OpenAPIRenderPlugin):
|
|
131
|
+
"""Render the OpenAPI schema using Swagger UI."""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
version: str = "5.18.2",
|
|
137
|
+
js_url: str | None = None,
|
|
138
|
+
css_url: str | None = None,
|
|
139
|
+
standalone_preset_js_url: str | None = None,
|
|
140
|
+
path: Union[str, List[str]] = "/swagger",
|
|
141
|
+
**kwargs: Any,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Initialize Swagger UI plugin.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
version: Swagger UI version to download from CDN.
|
|
147
|
+
js_url: Custom JS bundle URL (overrides version).
|
|
148
|
+
css_url: Custom CSS bundle URL (overrides version).
|
|
149
|
+
standalone_preset_js_url: Custom preset JS URL (overrides version).
|
|
150
|
+
path: Path(s) to serve Swagger UI at.
|
|
151
|
+
**kwargs: Additional arguments to pass to base class.
|
|
152
|
+
"""
|
|
153
|
+
self.js_url = js_url or f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{version}/swagger-ui-bundle.js"
|
|
154
|
+
self.css_url = css_url or f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{version}/swagger-ui.css"
|
|
155
|
+
self.standalone_preset_js_url = (
|
|
156
|
+
standalone_preset_js_url
|
|
157
|
+
or f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{version}/swagger-ui-standalone-preset.js"
|
|
158
|
+
)
|
|
159
|
+
super().__init__(path=path, **kwargs)
|
|
160
|
+
|
|
161
|
+
def render(self, openapi_schema: Dict[str, Any], schema_url: str) -> str:
|
|
162
|
+
"""Render Swagger UI HTML page."""
|
|
163
|
+
head = f"""
|
|
164
|
+
<head>
|
|
165
|
+
<title>{openapi_schema["info"]["title"]}</title>
|
|
166
|
+
{self.favicon}
|
|
167
|
+
<meta charset="utf-8"/>
|
|
168
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
169
|
+
<link href="{self.css_url}" rel="stylesheet">
|
|
170
|
+
<script src="{self.js_url}" crossorigin></script>
|
|
171
|
+
<script src="{self.standalone_preset_js_url}" crossorigin></script>
|
|
172
|
+
{self.style}
|
|
173
|
+
</head>
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
body = f"""
|
|
177
|
+
<body>
|
|
178
|
+
<div id='swagger-container'/>
|
|
179
|
+
<script type='text/javascript'>
|
|
180
|
+
const ui = SwaggerUIBundle({{
|
|
181
|
+
spec: {self.render_json(openapi_schema)},
|
|
182
|
+
dom_id: '#swagger-container',
|
|
183
|
+
deepLinking: true,
|
|
184
|
+
showExtensions: true,
|
|
185
|
+
showCommonExtensions: true,
|
|
186
|
+
presets: [
|
|
187
|
+
SwaggerUIBundle.presets.apis,
|
|
188
|
+
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
189
|
+
],
|
|
190
|
+
}})
|
|
191
|
+
ui.initOAuth({{}})
|
|
192
|
+
</script>
|
|
193
|
+
</body>
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
return f"<!DOCTYPE html><html>{head}{body}</html>"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class RedocRenderPlugin(OpenAPIRenderPlugin):
|
|
200
|
+
"""Render the OpenAPI schema using Redoc."""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
*,
|
|
205
|
+
version: str = "next",
|
|
206
|
+
js_url: str | None = None,
|
|
207
|
+
google_fonts: bool = True,
|
|
208
|
+
path: Union[str, List[str]] = "/redoc",
|
|
209
|
+
**kwargs: Any,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Initialize Redoc plugin.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
version: Redoc version to download from CDN.
|
|
215
|
+
js_url: Custom JS bundle URL (overrides version).
|
|
216
|
+
google_fonts: Download Google fonts via CDN.
|
|
217
|
+
path: Path(s) to serve Redoc at.
|
|
218
|
+
**kwargs: Additional arguments to pass to base class.
|
|
219
|
+
"""
|
|
220
|
+
self.js_url = js_url or f"https://cdn.jsdelivr.net/npm/redoc@{version}/bundles/redoc.standalone.js"
|
|
221
|
+
self.google_fonts = google_fonts
|
|
222
|
+
super().__init__(path=path, **kwargs)
|
|
223
|
+
|
|
224
|
+
def render(self, openapi_schema: Dict[str, Any], schema_url: str) -> str:
|
|
225
|
+
"""Render Redoc HTML page."""
|
|
226
|
+
head = f"""
|
|
227
|
+
<head>
|
|
228
|
+
<title>{openapi_schema["info"]["title"]}</title>
|
|
229
|
+
{self.favicon}
|
|
230
|
+
<meta charset="utf-8"/>
|
|
231
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
if self.google_fonts:
|
|
235
|
+
head += """
|
|
236
|
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
head += f"""
|
|
240
|
+
<script src="{self.js_url}" crossorigin></script>
|
|
241
|
+
{self.style}
|
|
242
|
+
</head>
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
body = f"<body><div id='redoc-container'/><script type='text/javascript'>Redoc.init({self.render_json(openapi_schema)},undefined,document.getElementById('redoc-container'))</script></body>"
|
|
246
|
+
|
|
247
|
+
return f"<!DOCTYPE html><html>{head}{body}</html>"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class ScalarRenderPlugin(OpenAPIRenderPlugin):
|
|
251
|
+
"""Render the OpenAPI schema using Scalar."""
|
|
252
|
+
|
|
253
|
+
_default_css_url = "https://cdn.jsdelivr.net/gh/litestar-org/branding@main/assets/openapi/scalar.css"
|
|
254
|
+
|
|
255
|
+
def __init__(
|
|
256
|
+
self,
|
|
257
|
+
*,
|
|
258
|
+
version: str = "latest",
|
|
259
|
+
js_url: str | None = None,
|
|
260
|
+
css_url: str | None = None,
|
|
261
|
+
path: Union[str, List[str]] = ["/scalar", "/"],
|
|
262
|
+
options: Dict[str, Any] | None = None,
|
|
263
|
+
**kwargs: Any,
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Initialize Scalar plugin.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
version: Scalar version to download from CDN.
|
|
269
|
+
js_url: Custom JS bundle URL (overrides version).
|
|
270
|
+
css_url: Custom CSS bundle URL (uses Litestar branding by default).
|
|
271
|
+
path: Path(s) to serve Scalar at.
|
|
272
|
+
options: Scalar configuration options.
|
|
273
|
+
**kwargs: Additional arguments to pass to base class.
|
|
274
|
+
"""
|
|
275
|
+
self.js_url = js_url or f"https://cdn.jsdelivr.net/npm/@scalar/api-reference@{version}"
|
|
276
|
+
self.css_url = css_url or self._default_css_url
|
|
277
|
+
self.options = options
|
|
278
|
+
super().__init__(path=path, **kwargs)
|
|
279
|
+
|
|
280
|
+
def render(self, openapi_schema: Dict[str, Any], schema_url: str) -> str:
|
|
281
|
+
"""Render Scalar HTML page."""
|
|
282
|
+
head = f"""
|
|
283
|
+
<head>
|
|
284
|
+
<title>{openapi_schema["info"]["title"]}</title>
|
|
285
|
+
{self.style}
|
|
286
|
+
<meta charset="utf-8"/>
|
|
287
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
288
|
+
{self.favicon}
|
|
289
|
+
<link rel="stylesheet" type="text/css" href="{self.css_url}">
|
|
290
|
+
</head>
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
options_script = ""
|
|
294
|
+
if self.options:
|
|
295
|
+
options_script = f"""
|
|
296
|
+
<script>
|
|
297
|
+
document.getElementById('api-reference').dataset.configuration = '{msgspec.json.encode(self.options).decode()}'
|
|
298
|
+
</script>
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
body = f"""
|
|
302
|
+
<noscript>
|
|
303
|
+
Scalar requires Javascript to function. Please enable it to browse the documentation.
|
|
304
|
+
</noscript>
|
|
305
|
+
<script
|
|
306
|
+
id="api-reference"
|
|
307
|
+
data-url="{schema_url}">
|
|
308
|
+
</script>
|
|
309
|
+
{options_script}
|
|
310
|
+
<script src="{self.js_url}" crossorigin></script>
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
return f"""
|
|
314
|
+
<!DOCTYPE html>
|
|
315
|
+
<html>
|
|
316
|
+
{head}
|
|
317
|
+
{body}
|
|
318
|
+
</html>
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class RapidocRenderPlugin(OpenAPIRenderPlugin):
|
|
323
|
+
"""Render the OpenAPI schema using Rapidoc."""
|
|
324
|
+
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
*,
|
|
328
|
+
version: str = "9.3.4",
|
|
329
|
+
js_url: str | None = None,
|
|
330
|
+
path: Union[str, List[str]] = "/rapidoc",
|
|
331
|
+
**kwargs: Any,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Initialize Rapidoc plugin.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
version: Rapidoc version to download from CDN.
|
|
337
|
+
js_url: Custom JS bundle URL (overrides version).
|
|
338
|
+
path: Path(s) to serve Rapidoc at.
|
|
339
|
+
**kwargs: Additional arguments to pass to base class.
|
|
340
|
+
"""
|
|
341
|
+
self.js_url = js_url or f"https://unpkg.com/rapidoc@{version}/dist/rapidoc-min.js"
|
|
342
|
+
super().__init__(path=path, **kwargs)
|
|
343
|
+
|
|
344
|
+
def render(self, openapi_schema: Dict[str, Any], schema_url: str) -> str:
|
|
345
|
+
"""Render Rapidoc HTML page."""
|
|
346
|
+
head = f"""
|
|
347
|
+
<head>
|
|
348
|
+
<title>{openapi_schema["info"]["title"]}</title>
|
|
349
|
+
{self.favicon}
|
|
350
|
+
<meta charset="utf-8"/>
|
|
351
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
352
|
+
<script src="{self.js_url}" crossorigin></script>
|
|
353
|
+
{self.style}
|
|
354
|
+
</head>
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
body = f"""
|
|
358
|
+
<body>
|
|
359
|
+
<rapi-doc spec-url="{schema_url}" />
|
|
360
|
+
</body>
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
return f"""
|
|
364
|
+
<!DOCTYPE html>
|
|
365
|
+
<html>
|
|
366
|
+
{head}
|
|
367
|
+
{body}
|
|
368
|
+
</html>
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
class StoplightRenderPlugin(OpenAPIRenderPlugin):
|
|
372
|
+
"""Render an OpenAPI schema using StopLight Elements."""
|
|
373
|
+
|
|
374
|
+
def __init__(
|
|
375
|
+
self,
|
|
376
|
+
*,
|
|
377
|
+
version: str = "7.7.18",
|
|
378
|
+
js_url: str | None = None,
|
|
379
|
+
css_url: str | None = None,
|
|
380
|
+
path: Union[str, List[str]] = "/elements",
|
|
381
|
+
**kwargs: Any,
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Initialize the OpenAPI UI render plugin.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
version: StopLight Elements version to download from the CDN. If js_url is provided, this is ignored.
|
|
387
|
+
js_url: Download url for the StopLight Elements JS bundle. If not provided, the version will be used to
|
|
388
|
+
construct the url.
|
|
389
|
+
css_url: Download url for the StopLight Elements CSS bundle. If not provided, the version will be used to
|
|
390
|
+
construct the url.
|
|
391
|
+
path: Path to serve the OpenAPI UI at.
|
|
392
|
+
**kwargs: Additional arguments to pass to the base class.
|
|
393
|
+
"""
|
|
394
|
+
self.js_url = js_url or f"https://unpkg.com/@stoplight/elements@{version}/web-components.min.js"
|
|
395
|
+
self.css_url = css_url or f"https://unpkg.com/@stoplight/elements@{version}/styles.min.css"
|
|
396
|
+
super().__init__(path=path, **kwargs)
|
|
397
|
+
|
|
398
|
+
def render(self, openapi_schema: dict[str, Any], schema_url: str) -> str:
|
|
399
|
+
"""Render an HTML page for StopLight Elements.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
openapi_schema: The OpenAPI schema as a dictionary.
|
|
403
|
+
schema_url: URL to the OpenAPI JSON schema.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
A rendered HTML string.
|
|
407
|
+
"""
|
|
408
|
+
head = f"""
|
|
409
|
+
<head>
|
|
410
|
+
<title>{openapi_schema["info"]["title"]}</title>
|
|
411
|
+
{self.favicon}
|
|
412
|
+
<meta charset="utf-8"/>
|
|
413
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
414
|
+
<link rel="stylesheet" href="{self.css_url}">
|
|
415
|
+
<script src="{self.js_url}" crossorigin></script>
|
|
416
|
+
{self.style}
|
|
417
|
+
</head>
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
body = f"""
|
|
421
|
+
<body>
|
|
422
|
+
<elements-api
|
|
423
|
+
apiDescriptionUrl="{schema_url}"
|
|
424
|
+
router="hash"
|
|
425
|
+
layout="sidebar"
|
|
426
|
+
/>
|
|
427
|
+
</body>
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
return f"""
|
|
431
|
+
<!DOCTYPE html>
|
|
432
|
+
<html>
|
|
433
|
+
{head}
|
|
434
|
+
{body}
|
|
435
|
+
</html>
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI route registration for BoltAPI.
|
|
3
|
+
|
|
4
|
+
This module handles the registration of OpenAPI documentation routes
|
|
5
|
+
(JSON, YAML, and UI plugins) separately from the main BoltAPI class.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Dict, Any, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from django_bolt.api import BoltAPI
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenAPIRouteRegistrar:
|
|
14
|
+
"""Handles registration of OpenAPI documentation routes."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, api: 'BoltAPI'):
|
|
17
|
+
"""Initialize the registrar with a BoltAPI instance.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
api: The BoltAPI instance to register routes on
|
|
21
|
+
"""
|
|
22
|
+
self.api = api
|
|
23
|
+
|
|
24
|
+
def register_routes(self) -> None:
|
|
25
|
+
"""Register OpenAPI documentation routes.
|
|
26
|
+
|
|
27
|
+
This registers:
|
|
28
|
+
- /docs/openapi.json - JSON schema endpoint
|
|
29
|
+
- /docs/openapi.yaml - YAML schema endpoint
|
|
30
|
+
- /docs/openapi.yml - YAML schema endpoint (alternative)
|
|
31
|
+
- UI plugin routes (e.g., /docs/swagger, /docs/redoc)
|
|
32
|
+
- Root redirect to default UI
|
|
33
|
+
"""
|
|
34
|
+
if not self.api.openapi_config or self.api._openapi_routes_registered:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
from django_bolt.openapi.plugins import JsonRenderPlugin, YamlRenderPlugin
|
|
38
|
+
from django_bolt.responses import HTML, Redirect, JSON, PlainText
|
|
39
|
+
|
|
40
|
+
# Always register JSON endpoint
|
|
41
|
+
json_plugin = JsonRenderPlugin()
|
|
42
|
+
|
|
43
|
+
@self.api.get(f"{self.api.openapi_config.path}/openapi.json")
|
|
44
|
+
async def openapi_json_handler(request):
|
|
45
|
+
"""Serve OpenAPI schema as JSON."""
|
|
46
|
+
try:
|
|
47
|
+
schema = self._get_schema()
|
|
48
|
+
rendered = json_plugin.render(schema, "")
|
|
49
|
+
# Return with proper OpenAPI JSON content-type
|
|
50
|
+
return JSON(
|
|
51
|
+
rendered,
|
|
52
|
+
status_code=200,
|
|
53
|
+
headers={"content-type": json_plugin.media_type}
|
|
54
|
+
)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
# Re-raise with more context for debugging
|
|
57
|
+
raise Exception(f"Failed to generate OpenAPI JSON schema: {type(e).__name__}: {str(e)}") from e
|
|
58
|
+
|
|
59
|
+
# Always register YAML endpoints
|
|
60
|
+
yaml_plugin = YamlRenderPlugin()
|
|
61
|
+
|
|
62
|
+
@self.api.get(f"{self.api.openapi_config.path}/openapi.yaml")
|
|
63
|
+
async def openapi_yaml_handler(request):
|
|
64
|
+
"""Serve OpenAPI schema as YAML."""
|
|
65
|
+
schema = self._get_schema()
|
|
66
|
+
rendered = yaml_plugin.render(schema, "")
|
|
67
|
+
# Return with proper YAML content-type
|
|
68
|
+
return PlainText(
|
|
69
|
+
rendered,
|
|
70
|
+
status_code=200,
|
|
71
|
+
headers={"content-type": yaml_plugin.media_type}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@self.api.get(f"{self.api.openapi_config.path}/openapi.yml")
|
|
75
|
+
async def openapi_yml_handler(request):
|
|
76
|
+
"""Serve OpenAPI schema as YAML (alternative extension)."""
|
|
77
|
+
schema = self._get_schema()
|
|
78
|
+
rendered = yaml_plugin.render(schema, "")
|
|
79
|
+
# Return with proper YAML content-type
|
|
80
|
+
return PlainText(
|
|
81
|
+
rendered,
|
|
82
|
+
status_code=200,
|
|
83
|
+
headers={"content-type": yaml_plugin.media_type}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Register UI plugin routes
|
|
87
|
+
self._register_ui_plugins()
|
|
88
|
+
|
|
89
|
+
# Add root redirect to default plugin
|
|
90
|
+
self._register_root_redirect()
|
|
91
|
+
|
|
92
|
+
self.api._openapi_routes_registered = True
|
|
93
|
+
|
|
94
|
+
def _get_schema(self) -> Dict[str, Any]:
|
|
95
|
+
"""Get or generate OpenAPI schema.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
OpenAPI schema as dictionary
|
|
99
|
+
"""
|
|
100
|
+
if self.api._openapi_schema is None:
|
|
101
|
+
from django_bolt.openapi.schema_generator import SchemaGenerator
|
|
102
|
+
|
|
103
|
+
generator = SchemaGenerator(self.api, self.api.openapi_config)
|
|
104
|
+
openapi = generator.generate()
|
|
105
|
+
self.api._openapi_schema = openapi.to_schema()
|
|
106
|
+
|
|
107
|
+
return self.api._openapi_schema
|
|
108
|
+
|
|
109
|
+
def _register_ui_plugins(self) -> None:
|
|
110
|
+
"""Register UI plugin routes (Swagger UI, ReDoc, etc.)."""
|
|
111
|
+
from django_bolt.responses import HTML
|
|
112
|
+
|
|
113
|
+
schema_url = f"{self.api.openapi_config.path}/openapi.json"
|
|
114
|
+
|
|
115
|
+
for plugin in self.api.openapi_config.render_plugins:
|
|
116
|
+
for plugin_path in plugin.paths:
|
|
117
|
+
full_path = f"{self.api.openapi_config.path}{plugin_path}"
|
|
118
|
+
|
|
119
|
+
# Create closure to capture plugin reference
|
|
120
|
+
def make_handler(p):
|
|
121
|
+
async def ui_handler():
|
|
122
|
+
"""Serve OpenAPI UI."""
|
|
123
|
+
try:
|
|
124
|
+
schema = self._get_schema()
|
|
125
|
+
rendered = p.render(schema, schema_url)
|
|
126
|
+
# Return with proper content-type from plugin
|
|
127
|
+
return HTML(
|
|
128
|
+
rendered,
|
|
129
|
+
status_code=200,
|
|
130
|
+
headers={"content-type": p.media_type}
|
|
131
|
+
)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
# Re-raise with more context for debugging
|
|
134
|
+
raise Exception(
|
|
135
|
+
f"Failed to render OpenAPI UI plugin {p.__class__.__name__}: "
|
|
136
|
+
f"{type(e).__name__}: {str(e)}"
|
|
137
|
+
) from e
|
|
138
|
+
return ui_handler
|
|
139
|
+
|
|
140
|
+
self.api.get(full_path)(make_handler(plugin))
|
|
141
|
+
|
|
142
|
+
def _register_root_redirect(self) -> None:
|
|
143
|
+
"""Register root redirect to default UI plugin."""
|
|
144
|
+
from django_bolt.responses import Redirect
|
|
145
|
+
|
|
146
|
+
if self.api.openapi_config.default_plugin:
|
|
147
|
+
default_path = self.api.openapi_config.default_plugin.paths[0]
|
|
148
|
+
|
|
149
|
+
@self.api.get(self.api.openapi_config.path)
|
|
150
|
+
async def openapi_root_redirect():
|
|
151
|
+
"""Redirect to default OpenAPI UI."""
|
|
152
|
+
return Redirect(f"{self.api.openapi_config.path}{default_path}")
|