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
@@ -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}")