Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
docs_handler.py
Go to the documentation of this file.
1r"""
2# +==== BEGIN CatFeeder =================+
3# LOGO:
4# ..............(..../\
5# ...............)..(.')
6# ..............(../..)
7# ...............\‍(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
10# animals/cats
11# /STOP
12# PROJECT: CatFeeder
13# FILE: docs_handler.py
14# CREATION DATE: 26-11-2025
15# LAST Modified: 2:9:10 24-01-2026
16# DESCRIPTION:
17# This is the backend server in charge of making the actual website work.
18# /STOP
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: The file containing the class in charge of injecting the documentation handler desired by the user.
21# // AR
22# +==== END CatFeeder =================+
23"""
24from typing import Optional, Dict, Any
25from fastapi import FastAPI, Request, Response
26from fastapi.openapi.utils import get_openapi
27from display_tty import Disp, initialise_logger
28from . import docs_constants as DOCS_CONST
29from .swagger import SwaggerHandler
30from .redoc import RedocHandler
31from .rapidoc import RapiDocProvider
32from .scalar import ScalarProvider
33from .elements import StoplightElementsProvider
34from .editor import SwaggerEditorProvider
35from .explorer import OpenAPIExplorerProvider
36from .rapipdf import RapiPDFProvider
37from ..core import FinalClass, RuntimeControl
38from ..http_codes import HCI, HttpDataTypes
39from ..core.runtime_manager import RuntimeManager, RI
40from ..server_header import ServerHeaders
41from ..path_manager import PathManager
42from ..boilerplates import BoilerplateResponses, BoilerplateIncoming
43
44
45class DocumentationHandler(metaclass=FinalClass):
46 """Unified documentation handler for managing multiple API documentation providers.
47
48 This class provides a centralized interface for enabling and managing different
49 API documentation providers (Swagger UI, ReDoc, RapiDoc, Scalar, etc.). It handles
50 the registration of documentation endpoints and the serving of OpenAPI schemas.
51
52 Attributes:
53 disp (Disp): Logger instance for this class.
54 debug (bool): Debug mode flag.
55 success (int): Success return code.
56 error (int): Error return code.
57 runtime_manager (RuntimeManager): Shared runtime manager instance.
58 enabled_providers (tuple): Tuple of enabled documentation providers.
59 providers (Dict): Dictionary mapping provider names to their instances.
60 """
61
62 disp: Disp = initialise_logger(__qualname__, False)
63
65 self,
66 providers: Optional[tuple[DOCS_CONST.DocumentationProvider, ...]] = None,
67 openapi_url: str = DOCS_CONST.OPENAPI_URL,
68 api_title: str = DOCS_CONST.OPENAPI_TITLE,
69 api_version: str = DOCS_CONST.OPENAPI_VERSION,
70 api_description: str = DOCS_CONST.OPENAPI_DESCRIPTION,
71 success: int = 0,
72 error: int = 84,
73 debug: bool = False
74 ) -> None:
75 """Initialize the DocumentationHandler.
76
77 Args:
78 providers (Optional[tuple], optional): Tuple of documentation providers to enable.
79 Defaults to DOCS_CONST.DEFAULT_PROVIDERS.
80 openapi_url (str, optional): URL path for OpenAPI JSON schema.
81 Defaults to DOCS_CONST.OPENAPI_URL.
82 api_title (str, optional): API title for documentation.
83 Defaults to DOCS_CONST.OPENAPI_TITLE.
84 api_version (str, optional): API version string.
85 Defaults to DOCS_CONST.OPENAPI_VERSION.
86 api_description (str, optional): API description for documentation.
87 Defaults to DOCS_CONST.OPENAPI_DESCRIPTION.
88 success (int, optional): Success return code. Defaults to 0.
89 error (int, optional): Error return code. Defaults to 84.
90 debug (bool, optional): Enable debug logging. Defaults to False.
91 """
92 # ------------------------ The logging function ------------------------
93 self.disp.update_disp_debug(debug)
94 self.disp.log_debug("Initialising...")
95 # -------------------------- Inherited values --------------------------
96 self.debug: bool = debug
97 self.success: int = success
98 self.error: int = error
99 # ----------------------- Shared instance handler ----------------------
100 self.runtime_manager: RuntimeManager = RI
101 # -------------------------- Shared instances --------------------------
102 self.path_manager_initialised: PathManager = self.runtime_manager.get(
103 PathManager)
104 self.runtime_control_initialised: RuntimeControl = self.runtime_manager.get(
105 RuntimeControl)
106 self.server_headers_initialised: ServerHeaders = self.runtime_manager.get(
107 ServerHeaders)
108 self.boilerplate_responses_initialised: BoilerplateResponses = self.runtime_manager.get(
109 BoilerplateResponses)
110 self.boilerplate_incoming_initialised: BoilerplateIncoming = self.runtime_manager.get(
111 BoilerplateIncoming)
112 # ------------------ Inherited documentation settings ------------------
113 self.openapi_url: str = openapi_url
114 self.api_title: str = api_title
115 self.api_version: str = api_version
116 self.api_description: str = api_description
117 # ------------------------- Provider handling -------------------------
118 if providers is None:
119 self.enabled_providers = DOCS_CONST.DEFAULT_PROVIDERS
120 else:
121 self.enabled_providers = providers
122
123 self.providersproviders: Dict[str, Any] = {}
124 # Note: Providers are initialized in inject() to avoid accessing FastAPI before it exists
125 # ----- The actual classes in charge of handling the documentation -----
127 DOCS_CONST.DocumentationProvider.SWAGGER: lambda: SwaggerHandler(
128 success=self.success,
129 error=self.error,
130 debug=self.debug
131 ),
132 DOCS_CONST.DocumentationProvider.REDOC: lambda: RedocHandler(
133 success=self.success,
134 error=self.error,
135 debug=self.debug
136 ),
137 DOCS_CONST.DocumentationProvider.RAPIDOC: lambda: RapiDocProvider(
138 openapi_url=self.openapi_url,
139 api_title=self.api_title,
140 debug=self.debug
141 ),
142 DOCS_CONST.DocumentationProvider.SCALAR: lambda: ScalarProvider(
143 openapi_url=self.openapi_url,
144 api_title=self.api_title,
145 debug=self.debug
146 ),
147 DOCS_CONST.DocumentationProvider.ELEMENTS: lambda: StoplightElementsProvider(
148 openapi_url=self.openapi_url,
149 api_title=self.api_title,
150 debug=self.debug
151 ),
152 DOCS_CONST.DocumentationProvider.EDITOR: lambda: SwaggerEditorProvider(
153 openapi_url=self.openapi_url,
154 api_title=self.api_title,
155 debug=self.debug
156 ),
157 DOCS_CONST.DocumentationProvider.EXPLORER: lambda: OpenAPIExplorerProvider(
158 openapi_url=self.openapi_url,
159 api_title=self.api_title,
160 debug=self.debug
161 ),
162 DOCS_CONST.DocumentationProvider.RAPIPDF: lambda: RapiPDFProvider(
163 openapi_url=self.openapi_url,
164 api_title=self.api_title,
165 success=self.success,
166 error=self.error,
167 debug=self.debug
168 ),
169 }
170 self.disp.log_debug("Initialised")
171
172 def _initialize_providers(self) -> None:
173 """Initialize the enabled documentation providers.
174
175 Creates instances of the selected providers and stores them in the providers dictionary.
176 """
177 func_title = "_initialize_providers"
178 self.disp.log_debug(
179 "Initializing documentation providers...", func_title)
180
181 for provider in self.enabled_providers:
182 factory = self.provider_factories.get(provider)
183 if factory:
184 self.providersproviders[provider.value] = factory()
185 self.disp.log_debug(
186 f"Initialized {provider.value} provider", func_title
187 )
188
189 self.disp.log_debug(
190 f"Initialized {len(self.providers)} documentation provider(s)", func_title
191 )
192
193 def _get_custom_openapi_schema(self, app: Optional["FastAPI"]) -> Dict[str, Any]:
194 """Generate custom OpenAPI schema with metadata.
195
196 Args:
197 app (Optional[FastAPI]): The FastAPI application instance.
198
199 Returns:
200 Dict[str, Any]: The custom OpenAPI schema.
201 """
202 func_title = "_get_custom_openapi_schema"
203
204 if app is None:
205 self.disp.log_error("FastAPI app is None", func_title)
206 return {}
207
208 if app.openapi_schema:
209 self.disp.log_debug("Returning cached OpenAPI schema", func_title)
210 return app.openapi_schema
211
212 self.disp.log_debug("Generating custom OpenAPI schema", func_title)
213
214 openapi_schema = get_openapi(
215 title=self.api_title,
216 version=self.api_version,
217 description=self.api_description,
218 routes=app.routes,
219 )
220
221 openapi_schema["info"]["x-logo"] = {
222 "url": "/static/logo.png"
223 }
224
225 if DOCS_CONST.ENABLE_OAUTH2_DOCS and DOCS_CONST.OAUTH2_AUTHORIZATION_URL and DOCS_CONST.OAUTH2_TOKEN_URL:
226 openapi_schema["components"] = openapi_schema.get("components", {})
227 openapi_schema["components"]["securitySchemes"] = {
228 "OAuth2": {
229 "type": "oauth2",
230 "flows": {
231 "authorizationCode": {
232 "authorizationUrl": DOCS_CONST.OAUTH2_AUTHORIZATION_URL,
233 "tokenUrl": DOCS_CONST.OAUTH2_TOKEN_URL,
234 "scopes": DOCS_CONST.OAUTH2_SCOPES
235 }
236 }
237 },
238 "BearerAuth": {
239 "type": "http",
240 "scheme": "bearer",
241 "bearerFormat": "JWT"
242 }
243 }
244 self.disp.log_debug(
245 "Added OAuth2 security scheme to OpenAPI schema", func_title)
246
247 app.openapi_schema = openapi_schema
248 self.disp.log_debug("OpenAPI schema generated and cached", func_title)
249 return app.openapi_schema
250
251 def _custom_openapi_wrapper(self, request: Request) -> Response:
252 """Wrapper for custom OpenAPI endpoint.
253
254 Args:
255 request (Request): The incoming request.
256
257 Returns:
258 Response: JSON response containing the OpenAPI schema.
259 """
260 func_title = "_custom_openapi_wrapper"
261 self.disp.log_debug("Serving OpenAPI schema", func_title)
262
263 token = self.boilerplate_incoming_initialised.get_token_if_present(
264 request)
265 self.disp.log_debug(f"token = {token}", func_title)
266
267 app = self.runtime_control_initialised.app
268 openapi_schema = self._get_custom_openapi_schema(app)
269
270 return HCI.success(content=openapi_schema, content_type=HttpDataTypes.JSON)
271
272 def _oauth2_redirect_handler(self, request: Request) -> Response:
273 """Handle OAuth2 redirect for Swagger UI authentication.
274
275 This endpoint is called by OAuth2 providers after user authentication.
276 It extracts the authorization code/token and passes it back to Swagger UI.
277
278 Args:
279 request (Request): The incoming request with OAuth2 callback parameters.
280
281 Returns:
282 Response: HTML response that passes credentials back to Swagger UI.
283 """
284 func_title = "_oauth2_redirect_handler"
285 self.disp.log_debug("Handling OAuth2 redirect", func_title)
286
287 token = self.boilerplate_incoming_initialised.get_token_if_present(
288 request)
289 self.disp.log_debug(f"token = {token}", func_title)
290
291 html_content = """
292<!DOCTYPE html>
293<html lang="en">
294<head>
295 <meta charset="UTF-8">
296 <title>OAuth2 Redirect</title>
297</head>
298<body>
299 <script>
300 // This script passes the OAuth2 response back to Swagger UI
301 'use strict';
302 function run() {
303 var oauth2 = window.opener.swaggerUIRedirectOauth2;
304 var sentState = oauth2.state;
305 var redirectUrl = oauth2.redirectUrl;
306 var isValid, qp, arr;
307
308 if (/code|token|error/.test(window.location.hash)) {
309 qp = window.location.hash.substring(1);
310 } else {
311 qp = location.search.substring(1);
312 }
313
314 arr = qp.split("&");
315 arr.forEach(function (v, i, _arr) {
316 var _arr2 = v.split("=");
317 if (_arr2[0] === "state") {
318 isValid = _arr2[1] === sentState;
319 }
320 });
321
322 if (oauth2.auth.schema.get("flow") === "accessCode" && !oauth2.auth.code) {
323 if (!isValid) {
324 oauth2.errCb({
325 authId: oauth2.auth.name,
326 source: "auth",
327 level: "warning",
328 message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
329 });
330 }
331
332 if (qp) {
333 arr = qp.split("&");
334 arr.forEach(function (v, i, _arr) {
335 var _arr3 = v.split("=");
336 if (_arr3[0] === "code") {
337 oauth2.auth.code = _arr3[1];
338 } else if (_arr3[0] === "error") {
339 oauth2.auth.error = _arr3[1];
340 }
341 });
342 }
343 }
344
345 if (oauth2.auth.code || oauth2.auth.error) {
346 window.close();
347 }
348 }
349
350 window.addEventListener('DOMContentLoaded', function () {
351 run();
352 });
353 </script>
354</body>
355</html>
356"""
357 return HCI.success(content=html_content, content_type=HttpDataTypes.HTML)
358
359 def _create_provider_handler(self, provider_instance: Any, provider_name: str) -> Any:
360 """Create an async handler function for a documentation provider.
361
362 Args:
363 provider_instance (Any): The provider instance to create a handler for.
364 provider_name (str): Name of the provider for unique operation ID.
365
366 Returns:
367 Any: An async handler function with unique name.
368 """
369 async def handler(request: Request) -> Response:
370 return await provider_instance.get_documentation(request)
371
372 # Give each handler a unique name to avoid duplicate Operation IDs
373 handler.__name__ = f"{provider_name}_documentation_handler"
374
375 return handler
376
377 def inject(self, providers: Optional[tuple[DOCS_CONST.DocumentationProvider, ...]] = None) -> int:
378 """Inject documentation endpoints into the FastAPI application.
379
380 Registers all enabled documentation providers and the OpenAPI schema endpoint.
381
382 Args:
383 providers (Optional[tuple], optional): Tuple of documentation providers to enable.
384 If None, uses the providers set in the constructor. Defaults to None.
385
386 Returns:
387 int: Success or error code.
388 """
389 func_title = "inject"
390 self.disp.log_debug("Injecting documentation endpoints...", func_title)
391
392 if providers is not None:
393 self.enabled_providers = providers
394 self.providersproviders = {}
395
396 # Initialize providers here (not in __init__) to ensure FastAPI app exists
399 self.disp.log_debug(
400 "Initialized providers during inject()", func_title
401 )
402
403 result = self.path_manager_initialised.add_path_if_not_exists(
404 path=self.openapi_url,
405 endpoint=self._custom_openapi_wrapper,
406 method="GET"
407 )
408 if result != self.success:
409 self.disp.log_error(
410 f"Failed to register OpenAPI schema endpoint at {self.openapi_url}",
411 func_title
412 )
413 return self.error
414
415 self.disp.log_debug(
416 f"Registered OpenAPI schema endpoint: {self.openapi_url}", func_title
417 )
418
419 if DOCS_CONST.ENABLE_OAUTH2_DOCS:
420 result = self.path_manager_initialised.add_path_if_not_exists(
421 path=DOCS_CONST.OAUTH2_REDIRECT_URL,
422 endpoint=self._oauth2_redirect_handler,
423 method="GET"
424 )
425 if result != self.success:
426 self.disp.log_error(
427 f"Failed to register OAuth2 redirect endpoint at {DOCS_CONST.OAUTH2_REDIRECT_URL}",
428 func_title
429 )
430 return self.error
431 self.disp.log_debug(
432 f"Registered OAuth2 redirect endpoint: {DOCS_CONST.OAUTH2_REDIRECT_URL}", func_title)
433
434 for provider_name, provider_instance in self.providersproviders.items():
435 if provider_name in [DOCS_CONST.DocumentationProvider.SWAGGER.value, DOCS_CONST.DocumentationProvider.REDOC.value]:
436 inject_result = provider_instance.inject()
437 if inject_result != self.success:
438 self.disp.log_error(
439 f"Failed to inject {provider_name} endpoints",
440 func_title
441 )
442 return self.error
443 self.disp.log_debug(
444 f"Injected {provider_name} endpoints via inject() method", func_title
445 )
446 elif provider_name in [DOCS_CONST.DocumentationProvider.RAPIPDF.value]:
447 doc_url = provider_instance.get_url()
448
449 handler_func = self._create_provider_handler(
450 provider_instance, provider_name)
451
452 result = self.path_manager_initialised.add_path_if_not_exists(
453 path=doc_url,
454 endpoint=handler_func,
455 method="GET"
456 )
457 if result != self.success:
458 self.disp.log_error(
459 f"Failed to register {provider_name} endpoint at {doc_url}",
460 func_title
461 )
462 return self.error
463 result = provider_instance.inject_js_ressource(
465 )
466 if result != self.success:
467 self.disp.log_error(
468 f"Failed to register {provider_name} child javascript ressources"
469 )
470 return self.error
471 self.disp.log_debug(
472 f"Registered {provider_name} endpoint: {doc_url}", func_title
473 )
474 else:
475 doc_url = provider_instance.get_url()
476
477 handler_func = self._create_provider_handler(
478 provider_instance, provider_name)
479
480 result = self.path_manager_initialised.add_path_if_not_exists(
481 path=doc_url,
482 endpoint=handler_func,
483 method="GET"
484 )
485 if result != self.success:
486 self.disp.log_error(
487 f"Failed to register {provider_name} endpoint at {doc_url}",
488 func_title
489 )
490 return self.error
491
492 self.disp.log_debug(
493 f"Registered {provider_name} endpoint: {doc_url}", func_title
494 )
495
496 self.disp.log_debug(
497 f"Successfully injected {len(self.providers)} documentation provider(s)", func_title
498 )
499 return self.success
int inject(self, Optional[tuple[DOCS_CONST.DocumentationProvider,...]] providers=None)
None __init__(self, Optional[tuple[DOCS_CONST.DocumentationProvider,...]] providers=None, str openapi_url=DOCS_CONST.OPENAPI_URL, str api_title=DOCS_CONST.OPENAPI_TITLE, str api_version=DOCS_CONST.OPENAPI_VERSION, str api_description=DOCS_CONST.OPENAPI_DESCRIPTION, int success=0, int error=84, bool debug=False)
Response _oauth2_redirect_handler(self, Request request)
Any _create_provider_handler(self, Any provider_instance, str provider_name)
Response _custom_openapi_wrapper(self, Request request)
Dict[str, Any] _get_custom_openapi_schema(self, Optional["FastAPI"] app)