From 7d41436aa16b1e0d183bde0c0b4c6b4a3bb4d3e5 Mon Sep 17 00:00:00 2001 From: luolingchun Date: Fri, 2 Jun 2023 10:21:14 +0800 Subject: [PATCH] Merge `extra_responses` to `responses` and deprecate `extra_responses` --- examples/api_blueprint_demo.py | 2 +- examples/response_demo.py | 5 +-- examples/rest_demo.py | 2 -- flask_openapi3/blueprint.py | 10 +++--- flask_openapi3/openapi.py | 8 ++--- flask_openapi3/scaffold.py | 44 +++++++++++++++++++-------- flask_openapi3/utils.py | 25 +++++++-------- flask_openapi3/view.py | 14 ++++++--- tests/test_openapi.py | 28 ++++++++--------- tests/test_restapi.py | 1 - tests/test_restapi_with_doc_prefix.py | 8 ++++- 11 files changed, 86 insertions(+), 61 deletions(-) diff --git a/examples/api_blueprint_demo.py b/examples/api_blueprint_demo.py index 378dcfa9..8d837e71 100644 --- a/examples/api_blueprint_demo.py +++ b/examples/api_blueprint_demo.py @@ -55,7 +55,7 @@ def get_book(): return {"code": 0, "message": "ok"} -@api.post('/book', extra_responses={"200": {"content": {"text/csv": {"schema": {"type": "string"}}}}}) +@api.post('/book', responses={"200": {"content": {"text/csv": {"schema": {"type": "string"}}}}}) def create_book(body: BookBody): assert body.age == 3 return {"code": 0, "message": "ok"} diff --git a/examples/response_demo.py b/examples/response_demo.py index 2fb771ab..4ef40838 100644 --- a/examples/response_demo.py +++ b/examples/response_demo.py @@ -11,7 +11,7 @@ from flask_openapi3 import Info from flask_openapi3 import OpenAPI, APIBlueprint -app = OpenAPI(__name__, info=Info(title="Hello API", version="1.0.0"), ) +app = OpenAPI(__name__, info=Info(title="Hello API", version="1.0.0")) bp = APIBlueprint("Hello BP", __name__) @@ -43,7 +43,8 @@ class Config: } -@bp.get("/hello/", responses={"200": Message}) +@bp.get("/hello/", + responses={"200": Message, "201": {"content": {"text/csv": {"schema": {"type": "string"}}}}}) def hello(path: HelloPath): message = {"message": f"""Hello {path.name}!"""} diff --git a/examples/rest_demo.py b/examples/rest_demo.py index 86d96ff5..905f4eba 100644 --- a/examples/rest_demo.py +++ b/examples/rest_demo.py @@ -95,9 +95,7 @@ class BookResponse(BaseModel): external_docs=ExternalDocumentation( url="https://www.openapis.org/", description="Something great got better, get excited!"), - responses={"200": BookResponse}, - extra_responses={"200": {"content": {"text/csv": {"schema": {"type": "string"}}}}}, security=security, servers=[Server(url="https://www.openapis.org/", description="openapi")] ) diff --git a/flask_openapi3/blueprint.py b/flask_openapi3/blueprint.py index 759060d1..9537deb9 100644 --- a/flask_openapi3/blueprint.py +++ b/flask_openapi3/blueprint.py @@ -3,7 +3,7 @@ # @Time : 2022/4/1 16:54 import re from copy import deepcopy -from typing import Optional, List, Dict, Any, Type, Callable, Tuple +from typing import Optional, List, Dict, Any, Type, Callable, Tuple, Union from flask import Blueprint from pydantic import BaseModel @@ -25,7 +25,7 @@ def __init__( *, abp_tags: Optional[List[Tag]] = None, abp_security: Optional[List[Dict[str, List[str]]]] = None, - abp_responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + abp_responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, doc_ui: bool = True, operation_id_callback: Callable = get_operation_id_for_path, **kwargs: Any @@ -39,7 +39,7 @@ def __init__( ``__name__``. This helps locate the ``root_path`` for the blueprint. abp_tags: APIBlueprint tags for every api abp_security: APIBlueprint security for every api - abp_responses: APIBlueprint response model + abp_responses: API responses, should be BaseModel, dict or None. doc_ui: Add openapi document UI(swagger, rapidoc and redoc). Defaults to True. operation_id_callback: Callback function for custom operation_id generation. Receives name (str), path (str) and method (str) parameters. @@ -92,7 +92,7 @@ def _do_decorator( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, Dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -113,7 +113,7 @@ def _do_decorator( operation_id: Unique string used to identify the operation. extra_form: Extra information describing the request body(application/form). extra_body: Extra information describing the request body(application/json). - responses: response's model must be pydantic BaseModel. + responses: API responses, should be BaseModel, dict or None. extra_responses: Extra information for responses. deprecated: Declares this operation to be deprecated. security: A declaration of which security mechanisms can be used for this operation. diff --git a/flask_openapi3/openapi.py b/flask_openapi3/openapi.py index 752d70ec..e0536b6d 100644 --- a/flask_openapi3/openapi.py +++ b/flask_openapi3/openapi.py @@ -32,7 +32,7 @@ def __init__( info: Optional[Info] = None, security_schemes: Optional[Dict[str, Union[SecurityScheme, Dict[str, Any]]]] = None, oauth_config: Optional[OAuthConfig] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, doc_ui: bool = True, doc_expansion: str = "list", doc_prefix: str = "/openapi", @@ -56,7 +56,7 @@ def __init__( security_schemes: See https://spec.openapis.org/oas/v3.0.3#security-scheme-object oauth_config: OAuth 2.0 configuration, see https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md - responses: OpenAPI response model + responses: API responses, should be BaseModel, dict or None. doc_ui: Add openapi document UI(swagger and redoc). Defaults to True. doc_expansion: String=["list"*, "full", "none"]. Controls the default expansion setting for the operations and tags. @@ -220,7 +220,7 @@ def _do_decorator( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, Dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -241,7 +241,7 @@ def _do_decorator( operation_id: Unique string used to identify the operation. extra_form: Extra information describing the request body(application/form). extra_body: Extra information describing the request body(application/json). - responses: response's model must be pydantic BaseModel. + responses: API responses, should be BaseModel, dict or None. extra_responses: Extra information for responses. deprecated: Declares this operation to be deprecated. security: A declaration of which security mechanisms can be used for this operation. diff --git a/flask_openapi3/scaffold.py b/flask_openapi3/scaffold.py index 526907bd..e5e07356 100644 --- a/flask_openapi3/scaffold.py +++ b/flask_openapi3/scaffold.py @@ -7,7 +7,7 @@ import warnings from abc import ABC from functools import wraps -from typing import Callable, List, Optional, Dict, Type, Any, Tuple +from typing import Callable, List, Optional, Dict, Type, Any, Tuple, Union from flask.scaffold import Scaffold from flask.wrappers import Response @@ -48,7 +48,7 @@ def _do_decorator( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -146,7 +146,7 @@ def get( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -168,7 +168,7 @@ def get( operation_id: Unique string used to identify the operation. extra_form: Extra information describing the request body(application/form). extra_body: Extra information describing the request body(application/json). - responses: response's model must be pydantic BaseModel. + responses: API responses, should be BaseModel, dict or None. extra_responses: Extra information for responses. deprecated: Declares this operation to be deprecated. security: A declaration of which security mechanisms can be used for this operation. @@ -185,6 +185,10 @@ def get( warnings.warn( """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", DeprecationWarning) + if extra_responses is not None: + warnings.warn( + """`extra_responses` will be deprecated in v3.x, please use `responses` instead.""", + DeprecationWarning) def decorator(func) -> Callable: header, cookie, path, query, form, body = \ @@ -227,7 +231,7 @@ def post( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -249,7 +253,7 @@ def post( operation_id: Unique string used to identify the operation. extra_form: Extra information describing the request body(application/form). extra_body: Extra information describing the request body(application/json). - responses: response's model must be pydantic BaseModel. + responses: API responses, should be BaseModel, dict or None. extra_responses: Extra information for responses. deprecated: Declares this operation to be deprecated. security: A declaration of which security mechanisms can be used for this operation. @@ -265,6 +269,10 @@ def post( warnings.warn( """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", DeprecationWarning) + if extra_responses is not None: + warnings.warn( + """`extra_responses` will be deprecated in v3.x, please use `responses` instead.""", + DeprecationWarning) def decorator(func) -> Callable: header, cookie, path, query, form, body = \ @@ -307,7 +315,7 @@ def put( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -329,7 +337,7 @@ def put( operation_id: Unique string used to identify the operation. extra_form: Extra information describing the request body(application/form). extra_body: Extra information describing the request body(application/json). - responses: response's model must be pydantic BaseModel. + responses: API responses, should be BaseModel, dict or None. extra_responses: Extra information for responses. deprecated: Declares this operation to be deprecated. security: A declaration of which security mechanisms can be used for this operation. @@ -345,6 +353,10 @@ def put( warnings.warn( """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", DeprecationWarning) + if extra_responses is not None: + warnings.warn( + """`extra_responses` will be deprecated in v3.x, please use `responses` instead.""", + DeprecationWarning) def decorator(func) -> Callable: header, cookie, path, query, form, body = \ @@ -387,7 +399,7 @@ def delete( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -409,7 +421,7 @@ def delete( operation_id: Unique string used to identify the operation. extra_form: Extra information describing the request body(application/form). extra_body: Extra information describing the request body(application/json). - responses: response's model must be pydantic BaseModel. + responses: API responses, should be BaseModel, dict or None. extra_responses: Extra information for responses. deprecated: Declares this operation to be deprecated. security: A declaration of which security mechanisms can be used for this operation. @@ -425,6 +437,10 @@ def delete( warnings.warn( """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", DeprecationWarning) + if extra_responses is not None: + warnings.warn( + """`extra_responses` will be deprecated in v3.x, please use `responses` instead.""", + DeprecationWarning) def decorator(func) -> Callable: header, cookie, path, query, form, body = \ @@ -467,7 +483,7 @@ def patch( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -489,7 +505,7 @@ def patch( operation_id: Unique string used to identify the operation. extra_form: Extra information describing the request body(application/form). extra_body: Extra information describing the request body(application/json). - responses: response's model must be pydantic BaseModel. + responses: API responses, should be BaseModel, dict or None. extra_responses: Extra information for responses. deprecated: Declares this operation to be deprecated. security: A declaration of which security mechanisms can be used for this operation. @@ -505,6 +521,10 @@ def patch( warnings.warn( """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", DeprecationWarning) + if extra_responses is not None: + warnings.warn( + """`extra_responses` will be deprecated in v3.x, please use `responses` 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 80a593db..929b0e6c 100644 --- a/flask_openapi3/utils.py +++ b/flask_openapi3/utils.py @@ -4,7 +4,7 @@ import inspect import re -from typing import get_type_hints, Dict, Type, Callable, List, Tuple, Optional, Any +from typing import get_type_hints, Dict, Type, Callable, List, Tuple, Optional, Any, Union from pydantic import BaseModel @@ -248,13 +248,13 @@ def parse_body( def get_responses( - responses: dict, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]], extra_responses: Dict[str, dict], components_schemas: dict, operation: Operation ) -> None: """ - :param responses: Dict[str, BaseModel] + :param responses: API responses, should be BaseModel, dict or None. :param extra_responses: Dict[str, dict] :param components_schemas: `models.component.py` `Components.schemas` :param operation: `models.path.py` Operation @@ -281,8 +281,13 @@ def get_responses( ) _schemas[UnprocessableEntity.__name__] = Schema(**UnprocessableEntity.schema()) for key, response in responses.items(): - # Verify that the response is a class and that class is a subclass of `pydantic.BaseModel` - if inspect.isclass(response) and issubclass(response, BaseModel): + if response is None: + # Verify that if the response is None, because http status code "204" means return "No Content" + _responses[key] = Response(description=HTTP_STATUS.get(key, "")) + continue + if isinstance(response, dict): + _responses[key] = response # type: ignore + else: schema = response.schema(ref_template=OPENAPI3_REF_TEMPLATE) _responses[key] = Response( description=HTTP_STATUS.get(key, ""), @@ -308,19 +313,15 @@ def get_responses( _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 + if model_config.openapi_extra.get("content"): + _responses[key].content.update(model_config.openapi_extra.get("content")) _schemas[response.__name__] = Schema(**schema) definitions = schema.get("definitions") if definitions: for name, value in definitions.items(): _schemas[name] = Schema(**value) - # Verify that if the response is None, because http status code "204" means return "No Content" - elif response is None: - _responses[key] = Response( - description=HTTP_STATUS.get(key, ""), - ) - else: - raise TypeError(f"{response} is invalid `pydantic.BaseModel`.") + # handle extra_responses for key, value in extra_responses.items(): # key "200" value {"content":{"text/csv":{"schema":{"type": "string"}}}} diff --git a/flask_openapi3/view.py b/flask_openapi3/view.py index 3c468351..ed309943 100644 --- a/flask_openapi3/view.py +++ b/flask_openapi3/view.py @@ -9,7 +9,7 @@ from .openapi import OpenAPI from copy import deepcopy -from typing import Optional, List, Dict, Type, Any, Callable +from typing import Optional, List, Dict, Type, Any, Callable, Union from pydantic import BaseModel @@ -29,7 +29,7 @@ def __init__( url_prefix: Optional[str] = None, view_tags: Optional[List[Tag]] = None, view_security: Optional[List[Dict[str, List[str]]]] = None, - view_responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + view_responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, doc_ui: bool = True, operation_id_callback: Callable = get_operation_id_for_path, ): @@ -40,7 +40,7 @@ def __init__( url_prefix: A path to prepend to all the APIView's urls view_tags: APIView tags for every api view_security: APIView security for every api - view_responses: APIView response models + view_responses: API responses, should be BaseModel, dict or None. doc_ui: Add openapi document UI(swagger, rapidoc and redoc). Defaults to True. operation_id_callback: Callback function for custom operation_id generation. Receives name (str), path (str) and method (str) parameters. @@ -109,7 +109,7 @@ def doc( operation_id: Optional[str] = None, extra_form: Optional[ExtraRequestBody] = None, extra_body: Optional[ExtraRequestBody] = None, - responses: Optional[Dict[str, Optional[Type[BaseModel]]]] = None, + responses: Optional[Dict[str, Union[Type[BaseModel], Dict[Any, Any], None]]] = None, extra_responses: Optional[Dict[str, dict]] = None, deprecated: Optional[bool] = None, security: Optional[List[Dict[str, List[Any]]]] = None, @@ -129,7 +129,7 @@ def doc( operation_id: Unique string used to identify the operation. extra_form: Extra information describing the request body(application/form). extra_body: Extra information describing the request body(application/json). - responses: response's model must be pydantic BaseModel. + responses: API responses, should be BaseModel, dict or None. extra_responses: Extra information for responses. deprecated: Declares this operation to be deprecated. security: A declaration of which security mechanisms can be used for this operation. @@ -146,6 +146,10 @@ def doc( warnings.warn( """`extra_body` will be deprecated in v3.x, please use `openapi_extra` instead.""", DeprecationWarning) + if extra_responses is not None: + warnings.warn( + """`extra_responses` will be deprecated in v3.x, please use `responses` instead.""", + DeprecationWarning) if responses is None: responses = {} diff --git a/tests/test_openapi.py b/tests/test_openapi.py index fa62e113..1d711455 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -5,7 +5,7 @@ from flask_openapi3 import OpenAPI -def test_responses_and_extra_responses_are_replicated_in_open_api(request): +def test_responses_are_replicated_in_open_api(request): test_app = OpenAPI(request.node.name) test_app.config["TESTING"] = True @@ -13,11 +13,8 @@ class BaseResponse(BaseModel): """Base description""" test: int - @test_app.get( - "/test", - responses={"201": BaseResponse}, - extra_responses={ - "201": { + class Config: + openapi_extra = { "description": "Custom description", "headers": { "location": { @@ -36,8 +33,8 @@ class BaseResponse(BaseModel): } } } - } - ) + + @test_app.get("/test", responses={"201": BaseResponse}) def endpoint_test(): return b'', 201 @@ -57,7 +54,7 @@ def endpoint_test(): "application/json": { "schema": {"$ref": "#/components/schemas/BaseResponse"} }, - # While this one comes from extra_responses + # While this one comes from responses "text/plain": { "schema": {"type": "string"} } @@ -70,14 +67,13 @@ def endpoint_test(): } -def test_none_responses_and_extra_responses_are_replicated_in_open_api(request): +def test_none_responses_are_replicated_in_open_api(request): test_app = OpenAPI(request.node.name) test_app.config["TESTING"] = True @test_app.get( "/test", - responses={"204": None}, - extra_responses={ + responses={ "204": { "description": "Custom description", "headers": { @@ -126,13 +122,13 @@ def endpoint_test(): } -def test_extra_responses_are_replicated_in_open_api(request): +def test_responses_are_replicated_in_open_api2(request): test_app = OpenAPI(request.node.name) test_app.config["TESTING"] = True @test_app.get( "/test", - extra_responses={ + responses={ "201": { "description": "Custom description", "headers": { @@ -181,13 +177,13 @@ def endpoint_test(): } -def test_extra_responses_without_content_are_replicated_in_open_api(request): +def test_responses_without_content_are_replicated_in_open_api(request): test_app = OpenAPI(request.node.name) test_app.config["TESTING"] = True @test_app.get( "/test", - extra_responses={ + responses={ "201": { "description": "Custom description", "headers": { diff --git a/tests/test_restapi.py b/tests/test_restapi.py index 3f4280fb..fa7132b5 100644 --- a/tests/test_restapi.py +++ b/tests/test_restapi.py @@ -106,7 +106,6 @@ def client(): url="https://www.openapis.org/", description="Something great got better, get excited!"), responses={"200": BookResponse}, - extra_responses={"200": {"content": {"text/csv": {"schema": {"type": "string"}}}}}, security=security ) def get_book(path: BookPath): diff --git a/tests/test_restapi_with_doc_prefix.py b/tests/test_restapi_with_doc_prefix.py index d356ad51..8806f149 100644 --- a/tests/test_restapi_with_doc_prefix.py +++ b/tests/test_restapi_with_doc_prefix.py @@ -70,6 +70,13 @@ class BookResponse(BaseModel): message: str = Field("ok", description="Exception Information") data: BookBodyWithID + class Config: + openapi_extra = { + "content": { + "text/csv": {"schema": {"type": "string"}} + } + } + @pytest.fixture def client(): @@ -82,7 +89,6 @@ def client(): '/book/', tags=[book_tag], responses={"200": BookResponse}, - extra_responses={"200": {"content": {"text/csv": {"schema": {"type": "string"}}}}}, security=security ) def get_book(path: BookPath):