From 2e45a3353e4546d908c33c8595a4cc76758f9287 Mon Sep 17 00:00:00 2001 From: luolingchun Date: Tue, 27 Jun 2023 09:33:58 +0800 Subject: [PATCH 1/3] [#79] Support `by_alias` in Model Config --- flask_openapi3/utils.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/flask_openapi3/utils.py b/flask_openapi3/utils.py index e9bbbd70..ad451b46 100644 --- a/flask_openapi3/utils.py +++ b/flask_openapi3/utils.py @@ -83,18 +83,21 @@ def get_operation_id_for_path(*, name: str, path: str, method: str) -> str: return operation_id -def get_schema(obj: Type[BaseModel]) -> dict: +def get_model_schema(model: Type[BaseModel]) -> dict: """Converts a Pydantic model to an OpenAPI schema.""" - assert inspect.isclass(obj) and issubclass(obj, BaseModel), \ - f"{obj} is invalid `pydantic.BaseModel`" + assert inspect.isclass(model) and issubclass(model, BaseModel), \ + f"{model} is invalid `pydantic.BaseModel`" - return obj.schema(ref_template=OPENAPI3_REF_TEMPLATE) + model_config = model.Config + by_alias = getattr(model_config, "by_alias", True) + + return model.schema(by_alias=by_alias, ref_template=OPENAPI3_REF_TEMPLATE) def parse_header(header: Type[BaseModel]) -> Tuple[List[Parameter], dict]: """Parses a header model and returns a list of parameters and component schemas.""" - schema = get_schema(header) + schema = get_model_schema(header) parameters = [] components_schemas: Dict = dict() properties = schema.get("properties", {}) @@ -121,7 +124,7 @@ def parse_header(header: Type[BaseModel]) -> Tuple[List[Parameter], dict]: def parse_cookie(cookie: Type[BaseModel]) -> Tuple[List[Parameter], dict]: """Parses a cookie model and returns a list of parameters and component schemas.""" - schema = get_schema(cookie) + schema = get_model_schema(cookie) parameters = [] components_schemas: Dict = dict() properties = schema.get("properties", {}) @@ -148,7 +151,7 @@ def parse_cookie(cookie: Type[BaseModel]) -> Tuple[List[Parameter], dict]: def parse_path(path: Type[BaseModel]) -> Tuple[List[Parameter], dict]: """Parses a path model and returns a list of parameters and component schemas.""" - schema = get_schema(path) + schema = get_model_schema(path) parameters = [] components_schemas: Dict = dict() properties = schema.get("properties", {}) @@ -175,7 +178,7 @@ def parse_path(path: Type[BaseModel]) -> Tuple[List[Parameter], dict]: def parse_query(query: Type[BaseModel]) -> Tuple[List[Parameter], dict]: """Parses a query model and returns a list of parameters and component schemas.""" - schema = get_schema(query) + schema = get_model_schema(query) parameters = [] components_schemas: Dict = dict() properties = schema.get("properties", {}) @@ -205,7 +208,7 @@ def parse_form( extra_form: Optional[ExtraRequestBody] = None, ) -> Tuple[Dict[str, MediaType], dict]: """Parses a form model and returns a list of parameters and component schemas.""" - schema = get_schema(form) + schema = get_model_schema(form) components_schemas = dict() properties = schema.get("properties", {}) @@ -250,7 +253,7 @@ def parse_body( extra_body: Optional[ExtraRequestBody] = None, ) -> Tuple[Dict[str, MediaType], dict]: """Parses a body model and returns a list of parameters and component schemas.""" - schema = get_schema(body) + schema = get_model_schema(body) components_schemas = dict() title = schema.get("title") @@ -315,7 +318,7 @@ def get_responses( if isinstance(response, dict): _responses[key] = response # type: ignore else: - schema = response.schema(ref_template=OPENAPI3_REF_TEMPLATE) + schema = get_model_schema(response) _responses[key] = Response( description=HTTP_STATUS.get(key, ""), content={ From 262b288b6ba82f90bf99dd3f29af86780bc0086e Mon Sep 17 00:00:00 2001 From: luolingchun Date: Fri, 30 Jun 2023 10:32:44 +0800 Subject: [PATCH 2/3] Add unit test for `by_alias` --- tests/test_model_config.py | 151 +++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tests/test_model_config.py diff --git a/tests/test_model_config.py b/tests/test_model_config.py new file mode 100644 index 00000000..5f1d5a77 --- /dev/null +++ b/tests/test_model_config.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# @Author : llc +# @Time : 2023/6/30 10:12 +from typing import List, Dict + +import pytest +from pydantic import BaseModel, Field + +from flask_openapi3 import OpenAPI, FileStorage + +app = OpenAPI(__name__) +app.config["TESTING"] = True + + +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" + } + } + + }} + + +class MessageResponse(BaseModel): + message: str = Field(..., description="The message") + metadata: Dict[str, str] = Field(alias="metadata_") + + class Config: + by_alias = False + openapi_extra = { + # "example": {"message": "aaa"}, + "examples": { + "example1": { + "summary": "example1 summary", + "value": { + "message": "bbb" + } + }, + "example2": { + "summary": "example2 summary", + "value": { + "message": "ccc" + } + } + } + } + + +@app.post("/form") +def api_form(form: UploadFilesForm): + print(form) + return {"code": 0, "message": "ok"} + + +@app.post("/body", responses={"200": MessageResponse}) +def api_error_json(body: BookBody): + print(body) + return {"code": 0, "message": "ok"} + + +@pytest.fixture +def client(): + client = app.test_client() + + return client + + +def test_openapi(client): + resp = client.get("/openapi/openapi.json") + _json = resp.json + assert resp.status_code == 200 + assert _json["paths"]["/form"]["post"]["requestBody"]["content"]["multipart/form-data"]["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"] + } + } + } + assert _json["paths"]["/body"]["post"]["requestBody"]["description"] == "This is post RequestBody" + assert _json["paths"]["/body"]["post"]["requestBody"]["content"]["application/json"]["example"] == \ + {"age": 12, "author": "author1"} + assert _json["paths"]["/body"]["post"]["responses"]["200"]["content"]["application/json"]["examples"] == \ + { + "example1": { + "summary": "example1 summary", + "value": { + "message": "bbb" + } + }, + "example2": { + "summary": "example2 summary", + "value": { + "message": "ccc" + } + } + } + assert _json["components"]["schemas"]["MessageResponse"]["properties"].get("metadata") is not None From d614c439630b7f0b6acdc006bc44e7693029504d Mon Sep 17 00:00:00 2001 From: luolingchun Date: Fri, 30 Jun 2023 10:41:26 +0800 Subject: [PATCH 3/3] Add document for model config `by_alias` --- docs/Usage/Model_Config.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/Usage/Model_Config.md b/docs/Usage/Model_Config.md index 70d5becb..eb0613e2 100644 --- a/docs/Usage/Model_Config.md +++ b/docs/Usage/Model_Config.md @@ -105,4 +105,18 @@ class Message(BaseModel): Effect in swagger: -![](../assets/Snipaste_2023-06-02_11-08-40.png) \ No newline at end of file +![](../assets/Snipaste_2023-06-02_11-08-40.png) + + +## by_alias + +Sometimes you may not want to use aliases (such as in the responses model). In that case, `by_alias` will be convenient: + +```python +class MessageResponse(BaseModel): + message: str = Field(..., description="The message") + metadata: Dict[str, str] = Field(alias="metadata_") + + class Config: + by_alias = False +``` \ No newline at end of file