diff --git a/docs/Tutorial/UI_Templates.md b/docs/Tutorial/UI_Templates.md new file mode 100644 index 00000000..4b30de3a --- /dev/null +++ b/docs/Tutorial/UI_Templates.md @@ -0,0 +1,91 @@ +Flask OpenAPI3 supports [Swagger](https://github.com/swagger-api/swagger-ui), [Redoc](https://github.com/Redocly/redoc) +and [RapiDoc](https://github.com/rapi-doc/RapiDoc) templates by default. + +You can customize templates use `ui_templates` in initializing OpenAPI. + +```python +ui_templates = { + "swagger": swagger_html_string, + "rapipdf": rapipdf_html_string +} + +app = OpenAPI(__name__, info=info, ui_templates=ui_templates) +``` + +In the above example, `swagger` will overwrite the original template and `rapipdf` is a new template. + +You can do anything with `swagger_html_string`, `rapipdf_html_string` or `any_html_string`. + +**swagger_html_string:** + +```html hl_lines="5 32" + + + + + Custom Title + + + + +
+ + + + + +``` + +**rapipdf_html_string:** + +```html hl_lines="9" + + + + + + + + + +``` + +!!! info + + `api_doc_url` is a necessary parameter for rendering template, so you must define it in your template. \ No newline at end of file diff --git a/examples/custom_ui_templates_demo.py b/examples/custom_ui_templates_demo.py new file mode 100644 index 00000000..7e3530c6 --- /dev/null +++ b/examples/custom_ui_templates_demo.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# @Author : llc +# @Time : 2023/2/3 15:14 +from pydantic import BaseModel + +from flask_openapi3 import Info, Tag +from flask_openapi3 import OpenAPI + +info = Info(title="book API", version="1.0.0") +swagger_html_string = """ + + + + + Custom Title + + + + +
+ + + + + +""" + +rapipdf_html_string = """ + + + + + + + + + +""" +ui_templates = { + "swagger": swagger_html_string, + "rapipdf": rapipdf_html_string +} +app = OpenAPI(__name__, info=info, ui_templates=ui_templates) + +book_tag = Tag(name='book', description='Some Book') + + +class BookQuery(BaseModel): + age: int + author: str + + +@app.get('/book', tags=[book_tag]) +def get_book(query: BookQuery): + """get books + get all books + """ + return { + "code": 0, + "message": "ok", + "data": [ + {"bid": 1, "age": query.age, "author": query.author}, + {"bid": 2, "age": query.age, "author": query.author} + ] + } + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask_openapi3/openapi.py b/flask_openapi3/openapi.py index 60b4acec..43854e2e 100644 --- a/flask_openapi3/openapi.py +++ b/flask_openapi3/openapi.py @@ -7,7 +7,7 @@ from copy import deepcopy from typing import Optional, List, Dict, Union, Any, Type, Callable, Tuple -from flask import Flask, Blueprint, render_template +from flask import Flask, Blueprint, render_template, render_template_string from pydantic import BaseModel from .blueprint import APIBlueprint @@ -39,6 +39,7 @@ def __init__( swagger_url: str = "/swagger", redoc_url: str = "/redoc", rapidoc_url: str = "/rapidoc", + ui_templates: Optional[Dict[str, str]] = None, servers: Optional[List[Server]] = None, external_docs: Optional[ExternalDocumentation] = None, **kwargs: Any @@ -64,6 +65,7 @@ def __init__( swagger_url: The Swagger UI documentation. Defaults to `/swagger`. redoc_url: The Redoc UI documentation. Defaults to `/redoc`. rapidoc_url: The RapiDoc UI documentation. Defaults to `/rapidoc`. + ui_templates: Custom UI templates, which used to overwrite or add UI documents. servers: An array of Server Objects, which provide connectivity information to a target server. external_docs: Allows referencing an external resource for extended documentation. See: https://spec.openapis.org/oas/v3.0.3#external-documentation-object @@ -92,11 +94,14 @@ def __init__( if not isinstance(oauth_config, OAuthConfig): raise TypeError("`initOAuth` must be `OAuthConfig`") self.oauth_config = oauth_config - if doc_ui: - self._init_doc() self.doc_expansion = doc_expansion + if ui_templates is None: + ui_templates = dict() + self.ui_templates = ui_templates self.severs = servers self.external_docs = external_docs + if doc_ui: + self._init_doc() # add openapi command self.cli.add_command(openapi_command) @@ -120,32 +125,46 @@ def _init_doc(self) -> None: endpoint="api_doc", view_func=lambda: self.api_doc ) - blueprint.add_url_rule( - rule=self.swagger_url, - endpoint="swagger", - view_func=lambda: render_template( - "swagger.html", - api_doc_url=self.api_doc_url.lstrip("/"), - doc_expansion=self.doc_expansion, - oauth_config=self.oauth_config.dict() if self.oauth_config else None + # iter ui_templates + for key, value in self.ui_templates.items(): + blueprint.add_url_rule( + rule=f"/{key}", + endpoint=key, + # pass default value to source + view_func=lambda source=value: render_template_string( + source, + api_doc_url=self.api_doc_url.lstrip("/") + ) ) - ) - blueprint.add_url_rule( - rule=self.redoc_url, - endpoint="redoc", - view_func=lambda: render_template( - "redoc.html", - api_doc_url=self.api_doc_url.lstrip("/") + if self.swagger_url.strip("/") not in self.ui_templates.keys(): + blueprint.add_url_rule( + rule=self.swagger_url, + endpoint="swagger", + view_func=lambda: render_template( + "swagger.html", + api_doc_url=self.api_doc_url.lstrip("/"), + doc_expansion=self.doc_expansion, + oauth_config=self.oauth_config.dict() if self.oauth_config else None + ) ) - ) - blueprint.add_url_rule( - rule=self.rapidoc_url, - endpoint="rapidoc", - view_func=lambda: render_template( - "rapidoc.html", - api_doc_url=self.api_doc_url.lstrip("/") + if self.redoc_url.strip("/") not in self.ui_templates.keys(): + blueprint.add_url_rule( + rule=self.redoc_url, + endpoint="redoc", + view_func=lambda: render_template( + "redoc.html", + api_doc_url=self.api_doc_url.lstrip("/") + ) + ) + if self.rapidoc_url.strip("/") not in self.ui_templates.keys(): + blueprint.add_url_rule( + rule=self.rapidoc_url, + endpoint="rapidoc", + view_func=lambda: render_template( + "rapidoc.html", + api_doc_url=self.api_doc_url.lstrip("/") + ) ) - ) blueprint.add_url_rule( rule="/", endpoint="index", diff --git a/mkdocs.yml b/mkdocs.yml index 18c61ec4..b3d6f4fd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ plugins: Operation: 路由操作 Request: 请求 Response: 响应 + UI Templates: 自定义模板 Example: 示例 API Reference: API 参考 LICENSE: 许可 @@ -85,6 +86,7 @@ nav: - Operation: Tutorial/Operation.md - Request: Tutorial/Request.md - Response: Tutorial/Response.md + - UI Templates: Tutorial/UI_Templates.md - JSON: Tutorial/JSON.md - Example: Example.md - API Reference: diff --git a/tests/test_custom_ui_templates_demo.py b/tests/test_custom_ui_templates_demo.py new file mode 100644 index 00000000..d482976a --- /dev/null +++ b/tests/test_custom_ui_templates_demo.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# @Author : llc +# @Time : 2023/2/3 15:14 +import pytest +from pydantic import BaseModel + +from flask_openapi3 import Info, Tag +from flask_openapi3 import OpenAPI + +info = Info(title="book API", version="1.0.0") +swagger_html_string = """ + + + + + Custom Title + + + + +
+ + + + + +""" + +rapipdf_html_string = """ + + + + + + + + + +""" +ui_templates = { + "swagger": swagger_html_string, + "rapipdf": rapipdf_html_string +} +app = OpenAPI(__name__, info=info, ui_templates=ui_templates) +app.config["TESTING"] = True +book_tag = Tag(name='book', description='Some Book') + + +class BookQuery(BaseModel): + age: int + author: str + + +@app.get('/book', tags=[book_tag]) +def get_book(query: BookQuery): + """get books + get all books + """ + return { + "code": 0, + "message": "ok", + "data": [ + {"bid": 1, "age": query.age, "author": query.author}, + {"bid": 2, "age": query.age, "author": query.author} + ] + } + + +@pytest.fixture +def client(): + client = app.test_client() + + return client + + +def test_openapi(client): + resp = client.get("/openapi/openapi.json") + assert resp.status_code == 200 + assert resp.json == app.api_doc + + +def test_swagger(client): + resp = client.get("/openapi/swagger") + assert resp.status_code == 200 + assert "Custom Title" in resp.text + + +def test_rapipdf(client): + resp = client.get("/openapi/rapipdf") + assert resp.status_code == 200