diff --git a/examples/openapi_extra.py b/examples/openapi_extra.py new file mode 100644 index 00000000..98992084 --- /dev/null +++ b/examples/openapi_extra.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# @Author : llc +# @Time : 2023/6/1 15:04 +from typing import List + +from pydantic import BaseModel + +from flask_openapi3 import OpenAPI, FileStorage + +app = OpenAPI(__name__) + + +class UploadFilesForm(BaseModel): + file: FileStorage + str_list: List[str] + + class Config: + openapi_extra = { + # "example": {"a": 123}, + "examples": { + "Example 01": { + "summary": "An example", + "value": { + "file": "Example-01.jpg", + "str_list": ["a", "b", "c"] + } + }, + "Example 02": { + "summary": "Another example", + "value": { + "str_list": ["1", "2", "3"] + } + } + } + } + + +class BookBody(BaseModel): + age: int + author: str + + class Config: + openapi_extra = { + "description": "This is post RequestBody", + "example": {"age": 12, "author": "author1"}, + "examples": { + "example1": { + "summary": "example summary1", + "description": "example description1", + "value": { + "age": 24, + "author": "author2" + } + }, + "example2": { + "summary": "example summary2", + "description": "example description2", + "value": { + "age": 48, + "author": "author3" + } + } + + }} + + +@app.post('/upload/files') +def upload_files(form: UploadFilesForm): + print(form.file) + print(form.str_list) + return {"code": 0, "message": "ok"} + + +@app.post('/book', ) +def create_book(body: BookBody): + print(body) + return {"code": 0, "message": "ok"} + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/examples/response_demo.py b/examples/response_demo.py index 04de6f89..2fb771ab 100644 --- a/examples/response_demo.py +++ b/examples/response_demo.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# @Author : [martinatseequent](https://github.com/martinatseequent) +# @Author : llc # @Time : 2021/6/22 9:32 import json @@ -23,6 +23,25 @@ class HelloPath(BaseModel): class Message(BaseModel): message: str = Field(..., description="The message") + class Config: + openapi_extra = { + # "example": {"message": "aaa"}, + "examples": { + "example1": { + "summary": "example1 summary", + "value": { + "message": "bbb" + } + }, + "example2": { + "summary": "example2 summary", + "value": { + "message": "ccc" + } + } + } + } + @bp.get("/hello/", responses={"200": Message}) def hello(path: HelloPath): diff --git a/examples/rest_demo.py b/examples/rest_demo.py index 48673690..86d96ff5 100644 --- a/examples/rest_demo.py +++ b/examples/rest_demo.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field -from flask_openapi3 import ExternalDocumentation, ExtraRequestBody, Example +from flask_openapi3 import ExternalDocumentation from flask_openapi3 import Info, Tag, Server from flask_openapi3 import OpenAPI @@ -128,32 +128,7 @@ def get_books(query: BookQuery): } -extra_body = ExtraRequestBody( - description="This is post RequestBody", - required=True, - example="ttt", - examples={ - "example1": Example( - summary="example summary1", - description="example description1", - value={ - "age": 24, - "author": "author1" - } - ), - "example2": Example( - summary="example summary2", - description="example description2", - value={ - "age": 48, - "author": "author2" - } - ) - } -) - - -@app.post('/book', tags=[book_tag], responses={"200": BookResponse}, extra_body=extra_body) +@app.post('/book', tags=[book_tag], responses={"200": BookResponse}) def create_book(body: BookBody): print(body) return {"code": 0, "message": "ok"}, HTTPStatus.OK diff --git a/examples/upload_file_demo.py b/examples/upload_file_demo.py index 3c04f8b4..c4ab2d63 100644 --- a/examples/upload_file_demo.py +++ b/examples/upload_file_demo.py @@ -6,8 +6,7 @@ from pydantic import BaseModel, Field -from flask_openapi3 import OpenAPI, FileStorage, ExtraRequestBody -from flask_openapi3 import Encoding +from flask_openapi3 import OpenAPI, FileStorage app = OpenAPI(__name__) @@ -23,14 +22,6 @@ class UploadFilesForm(BaseModel): int_list: List[int] -extra_form = ExtraRequestBody( - description="This is form RequestBody", - required=True, - # replace style (default to form) - encoding={"str_list": Encoding(style="simple")} -) - - @app.post('/upload/file') def upload_file(form: UploadFileForm): print(form.file.filename) @@ -39,7 +30,7 @@ def upload_file(form: UploadFileForm): return {"code": 0, "message": "ok"} -@app.post('/upload/files', extra_form=extra_form) +@app.post('/upload/files') def upload_files(form: UploadFilesForm): print(form.files) print(form.str_list) diff --git a/flask_openapi3/scaffold.py b/flask_openapi3/scaffold.py index 1177e06e..526907bd 100644 --- a/flask_openapi3/scaffold.py +++ b/flask_openapi3/scaffold.py @@ -4,6 +4,7 @@ import functools import inspect import sys +import warnings from abc import ABC from functools import wraps from typing import Callable, List, Optional, Dict, Type, Any, Tuple @@ -31,6 +32,8 @@ def iscoroutinefunction(func: Any) -> bool: return inspect.iscoroutinefunction(func) +warnings.simplefilter("once") + class APIScaffold(Scaffold, ABC): def _do_decorator( @@ -174,6 +177,15 @@ def get( doc_ui: Add openapi document UI(swagger, rapidoc and redoc). Defaults to True. """ + if extra_form is not None: + warnings.warn( + """`extra_form` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) + if extra_body is not None: + warnings.warn( + """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) + def decorator(func) -> Callable: header, cookie, path, query, form, body = \ self._do_decorator( @@ -245,6 +257,14 @@ def post( openapi_extensions: Allows extensions to the OpenAPI Schema. doc_ui: Declares this operation to be shown. """ + if extra_form is not None: + warnings.warn( + """`extra_form` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) + if extra_body is not None: + warnings.warn( + """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) def decorator(func) -> Callable: header, cookie, path, query, form, body = \ @@ -317,6 +337,14 @@ def put( openapi_extensions: Allows extensions to the OpenAPI Schema. doc_ui: Declares this operation to be shown. """ + if extra_form is not None: + warnings.warn( + """`extra_form` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) + if extra_body is not None: + warnings.warn( + """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) def decorator(func) -> Callable: header, cookie, path, query, form, body = \ @@ -389,6 +417,14 @@ def delete( openapi_extensions: Allows extensions to the OpenAPI Schema. doc_ui: Declares this operation to be shown. """ + if extra_form is not None: + warnings.warn( + """`extra_form` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) + if extra_body is not None: + warnings.warn( + """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) def decorator(func) -> Callable: header, cookie, path, query, form, body = \ @@ -461,6 +497,14 @@ def patch( openapi_extensions: Allows extensions to the OpenAPI Schema. doc_ui: Declares this operation to be shown. """ + if extra_form is not None: + warnings.warn( + """`extra_form` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) + if extra_body is not None: + warnings.warn( + """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) def decorator(func) -> Callable: header, cookie, path, query, form, body = \ diff --git a/flask_openapi3/utils.py b/flask_openapi3/utils.py index 9a3faf05..80a593db 100644 --- a/flask_openapi3/utils.py +++ b/flask_openapi3/utils.py @@ -298,6 +298,17 @@ def get_responses( ) } ) + + model_config = response.Config + if hasattr(model_config, "openapi_extra"): + _responses[key].description = model_config.openapi_extra.get("description") + _responses[key].headers = model_config.openapi_extra.get("headers") + _responses[key].links = model_config.openapi_extra.get("links") + _content = _responses[key].content + _content["application/json"].example = model_config.openapi_extra.get("example") # type: ignore + _content["application/json"].examples = model_config.openapi_extra.get("examples") # type: ignore + _content["application/json"].encoding = model_config.openapi_extra.get("encoding") # type: ignore + _schemas[response.__name__] = Schema(**schema) definitions = schema.get("definitions") if definitions: @@ -407,6 +418,13 @@ def parse_parameters( request_body = RequestBody(**{ "content": _content, }) + model_config = form.Config + if hasattr(model_config, "openapi_extra"): + request_body.description = model_config.openapi_extra.get("description") + request_body.content["multipart/form-data"].example = model_config.openapi_extra.get("example") + request_body.content["multipart/form-data"].examples = model_config.openapi_extra.get("examples") + if model_config.openapi_extra.get("encoding"): + request_body.content["multipart/form-data"].encoding = model_config.openapi_extra.get("encoding") operation.requestBody = request_body if body: _content, _components_schemas = parse_body(body, extra_body) @@ -419,6 +437,12 @@ def parse_parameters( ) else: request_body = RequestBody(content=_content) + model_config = body.Config + if hasattr(model_config, "openapi_extra"): + request_body.description = model_config.openapi_extra.get("description") + request_body.content["application/json"].example = model_config.openapi_extra.get("example") + request_body.content["application/json"].examples = model_config.openapi_extra.get("examples") + request_body.content["application/json"].encoding = model_config.openapi_extra.get("encoding") operation.requestBody = request_body operation.parameters = parameters if parameters else None diff --git a/flask_openapi3/view.py b/flask_openapi3/view.py index 496d828e..3c468351 100644 --- a/flask_openapi3/view.py +++ b/flask_openapi3/view.py @@ -3,6 +3,7 @@ # @Time : 2022/10/14 16:09 import re import typing +import warnings if typing.TYPE_CHECKING: from .openapi import OpenAPI @@ -19,6 +20,8 @@ from .utils import get_operation, parse_and_store_tags, parse_parameters, get_responses, parse_method, \ get_operation_id_for_path +warnings.simplefilter("once") + class APIView: def __init__( @@ -135,6 +138,15 @@ def doc( doc_ui: Add openapi document UI(swagger, rapidoc and redoc). Defaults to True. """ + if extra_form is not None: + warnings.warn( + """`extra_form` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) + if extra_body is not None: + warnings.warn( + """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", + DeprecationWarning) + if responses is None: responses = {} if extra_responses is None: diff --git a/tests/test_openapi.py b/tests/test_openapi.py index c2453537..fa62e113 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -2,8 +2,7 @@ from pydantic import BaseModel -from flask_openapi3 import Example -from flask_openapi3 import OpenAPI, ExtraRequestBody +from flask_openapi3 import OpenAPI def test_responses_and_extra_responses_are_replicated_in_open_api(request): @@ -237,26 +236,28 @@ def test_body_examples_are_replicated_in_open_api(request): test_app = OpenAPI(request.node.name) test_app.config["TESTING"] = True - @test_app.post( - "/test", - extra_body=ExtraRequestBody( - examples={ - "Example 01": Example( - summary="An example", - value={ + class Config: + openapi_extra = { + "examples": { + "Example 01": { + "summary": "An example", + "value": { "test_int": -1, "test_str": "negative", } - ), - "Example 02": Example( - externalValue="https://example.org/examples/second-example.xml" - ), - "Example 03": Example(**{ + }, + "Example 02": { + "externalValue": "https://example.org/examples/second-example.xml" + }, + "Example 03": { "$ref": "#/components/examples/third-example" - }) + } } - ) - ) + } + + BaseRequest.Config = Config + + @test_app.post("/test") def endpoint_test(body: BaseRequest): return body.json(), 200 @@ -278,54 +279,26 @@ def endpoint_test(body: BaseRequest): } -def test_body_examples_are_not_replicated_with_form(request): +def test_form_examples(request): test_app = OpenAPI(request.node.name) test_app.config["TESTING"] = True - @test_app.post( - "/test", - extra_body=ExtraRequestBody(example={ - "Example 01": Example(**{ - "summary": "An example", - "value": { - "test_int": -1, - "test_str": "negative", - } - }), - }) - ) - def endpoint_test(form: BaseRequest): - return form.json(), 200 - - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json["paths"]["/test"]["post"]["requestBody"] == { - "content": { - "multipart/form-data": { - "schema": {"$ref": "#/components/schemas/BaseRequest"} + class Config: + openapi_extra = { + "examples": { + "Example 01": { + "summary": "An example", + "value": { + "test_int": -1, + "test_str": "negative", + } } - }, - "required": True + } } + BaseRequest.Config = Config -def test_form_examples(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.post( - "/test", - extra_form=ExtraRequestBody(examples={ - "Example 01": Example(**{ - "summary": "An example", - "value": { - "test_int": -1, - "test_str": "negative", - } - }), - }) - ) + @test_app.post("/test") def endpoint_test(form: BaseRequest): return form.json(), 200