Skip to content

Commit cd568ea

Browse files
committed
Add tests
1 parent 6d95399 commit cd568ea

File tree

8 files changed

+226
-37
lines changed

8 files changed

+226
-37
lines changed

examples/multi_content_type.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
# @Author : llc
33
# @Time : 2024/12/27 15:30
4+
from flask import Request
45
from pydantic import BaseModel
56

67
from flask_openapi3 import OpenAPI
@@ -30,6 +31,17 @@ class CatBody(BaseModel):
3031
}
3132

3233

34+
class BsonModel(BaseModel):
35+
e: int = None
36+
f: str = None
37+
38+
model_config = {
39+
"openapi_extra": {
40+
"content_type": "application/bson"
41+
}
42+
}
43+
44+
3345
class ContentTypeModel(BaseModel):
3446
model_config = {
3547
"openapi_extra": {
@@ -38,9 +50,28 @@ class ContentTypeModel(BaseModel):
3850
}
3951

4052

41-
@app.post("/a", responses={200: DogBody | CatBody | ContentTypeModel})
42-
def index_a(body: DogBody | CatBody | ContentTypeModel):
53+
@app.post("/a", responses={200: DogBody | CatBody | ContentTypeModel | BsonModel})
54+
def index_a(body: DogBody | CatBody | ContentTypeModel | BsonModel):
55+
"""
56+
This may be confusing, if the content-type is application/json, the type of body will be auto parsed to
57+
DogBody or CatBody, otherwise it cannot be parsed to ContentTypeModel or BsonModel.
58+
The body is equivalent to the request variable in Flask, and you can use body.data, body.text, etc ...
59+
"""
4360
print(body)
61+
if isinstance(body, Request):
62+
if body.mimetype == "text/csv":
63+
# processing csv data
64+
...
65+
elif body.mimetype == "application/bson":
66+
# processing bson data
67+
from bson import BSON
68+
69+
obj = BSON(body.data).decode()
70+
new_body = body.model_validate(obj=obj)
71+
print(new_body)
72+
else:
73+
# DogBody or CatBody
74+
...
4475
return {"hello": "world"}
4576

4677

flask_openapi3/request.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from json import JSONDecodeError
77
from types import UnionType
88
from typing import Any, Optional, Type, Union, get_args, get_origin
9-
9+
from functools import wraps
1010
from flask import abort, current_app, request
1111
from pydantic import BaseModel, RootModel, ValidationError
1212
from pydantic.fields import FieldInfo
@@ -46,10 +46,8 @@ def _get_value(model: Type[BaseModel], args: MultiDict, model_field_key: str, mo
4646
def _validate_header(header: Type[BaseModel], func_kwargs: dict):
4747
request_headers = dict(request.headers)
4848
header_dict = {}
49-
model_properties = header.model_json_schema().get("properties", {})
5049
for model_field_key, model_field_value in header.model_fields.items():
5150
key_title = model_field_key.replace("_", "-").title()
52-
model_field_schema = model_properties.get(model_field_value.alias or model_field_key)
5351
if model_field_value.alias and header.model_config.get("populate_by_name"):
5452
key = model_field_value.alias
5553
key_alias_title = model_field_value.alias.replace("_", "-").title()
@@ -60,11 +58,9 @@ def _validate_header(header: Type[BaseModel], func_kwargs: dict):
6058
value = request_headers.get(key_alias_title)
6159
else:
6260
key = model_field_key
63-
value = request_headers[key_title]
61+
value = request_headers.get(key_title)
6462
if value is not None:
6563
header_dict[key] = value
66-
if model_field_schema.get("type") == "null":
67-
header_dict[key] = value # type:ignore
6864
# extra keys
6965
for key, value in request_headers.items():
7066
if key not in header_dict.keys():
@@ -147,7 +143,7 @@ def _validate_form(form: Type[BaseModel], func_kwargs: dict):
147143

148144
def _validate_body(body: Type[BaseModel], func_kwargs: dict):
149145
if is_application_json(request.mimetype):
150-
if get_origin(body) == UnionType:
146+
if get_origin(body) in (Union, UnionType):
151147
root_model_list = [model for model in get_args(body)]
152148
Body = RootModel[Union[tuple(root_model_list)]] # type: ignore
153149
else:

flask_openapi3/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import sys
88
from enum import Enum
99
from http import HTTPStatus
10-
from typing import Any, Callable, DefaultDict, Optional, Type, get_type_hints
10+
from typing import Any, Callable, DefaultDict, Optional, Type, get_type_hints, Union
1111
from types import UnionType
1212
from flask import current_app, make_response
1313
from flask.wrappers import Response as FlaskResponse
@@ -349,7 +349,7 @@ def _parse_body(_model):
349349
for name, value in definitions.items():
350350
components_schemas[name] = Schema(**value)
351351

352-
if get_origin(body) == UnionType:
352+
if get_origin(body) in (Union, UnionType):
353353
for model in get_args(body):
354354
_parse_body(model)
355355
else:
@@ -431,7 +431,7 @@ def _parse_response(_key, _model):
431431
elif isinstance(response_model, dict):
432432
response_model["description"] = response_model.get("description", HTTP_STATUS.get(key, ""))
433433
_responses[key] = Response(**response_model)
434-
elif get_origin(response_model) == UnionType:
434+
elif get_origin(response_model) in [UnionType, Union]:
435435
for model in get_args(response_model):
436436
_parse_response(key, model)
437437
else:

tests/test_api_blueprint.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from pydantic import BaseModel, Field
99

1010
from flask_openapi3 import APIBlueprint, Info, OpenAPI, Tag
11+
from flask_openapi3 import APIBlueprint, OpenAPI, Server, ExternalDocumentation
12+
from flask_openapi3 import Tag, Info
1113

1214
info = Info(title="book API", version="1.0.0")
1315

@@ -84,7 +86,17 @@ def update_book1(path: BookPath, body: BookBody):
8486
return {"code": 0, "message": "ok"}
8587

8688

87-
@api.patch("/v2/book/<int:bid>")
89+
@api.patch(
90+
'/v2/book/<int:bid>',
91+
servers=[Server(
92+
url="http://127.0.0.1:5000",
93+
variables=None
94+
)],
95+
external_docs=ExternalDocumentation(
96+
url="https://www.openapis.org/",
97+
description="Something great got better, get excited!"),
98+
deprecated=True
99+
)
88100
def update_book1_v2(path: BookPath, body: BookBody):
89101
assert path.bid == 1
90102
assert body.age == 3

tests/test_api_view.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import pytest
88
from pydantic import BaseModel, Field
99

10-
from flask_openapi3 import APIView, Info, OpenAPI, Tag
10+
from flask_openapi3 import APIView, Server, ExternalDocumentation
11+
from flask_openapi3 import OpenAPI, Tag, Info
1112

1213
info = Info(title="book API", version="1.0.0")
1314
jwt = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
@@ -63,7 +64,17 @@ def put(self, path: BookPath):
6364
print(path)
6465
return "put"
6566

66-
@api_view.doc(summary="delete book", deprecated=True)
67+
@api_view.doc(
68+
summary="delete book",
69+
servers=[Server(
70+
url="http://127.0.0.1:5000",
71+
variables=None
72+
)],
73+
external_docs=ExternalDocumentation(
74+
url="https://www.openapis.org/",
75+
description="Something great got better, get excited!"),
76+
deprecated=True
77+
)
6778
def delete(self, path: BookPath):
6879
print(path)
6980
return "delete"

tests/test_multi_content_type.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# -*- coding: utf-8 -*-
2+
# @Author : llc
3+
# @Time : 2025/1/6 16:37
4+
from typing import Union
5+
6+
import pytest
7+
from flask import Request
8+
from pydantic import BaseModel
9+
10+
from flask_openapi3 import OpenAPI
11+
12+
app = OpenAPI(__name__)
13+
app.config["TESTING"] = True
14+
15+
16+
class DogBody(BaseModel):
17+
a: int = None
18+
b: str = None
19+
20+
model_config = {
21+
"openapi_extra": {
22+
"content_type": "application/vnd.dog+json"
23+
}
24+
}
25+
26+
27+
class CatBody(BaseModel):
28+
c: int = None
29+
d: str = None
30+
31+
model_config = {
32+
"openapi_extra": {
33+
"content_type": "application/vnd.cat+json"
34+
}
35+
}
36+
37+
38+
class BsonModel(BaseModel):
39+
e: int = None
40+
f: str = None
41+
42+
model_config = {
43+
"openapi_extra": {
44+
"content_type": "application/bson"
45+
}
46+
}
47+
48+
49+
class ContentTypeModel(BaseModel):
50+
model_config = {
51+
"openapi_extra": {
52+
"content_type": "text/csv"
53+
}
54+
}
55+
56+
57+
@app.post("/a", responses={200: Union[DogBody, CatBody, ContentTypeModel, BsonModel]})
58+
def index_a(body: Union[DogBody, CatBody, ContentTypeModel, BsonModel]):
59+
"""
60+
This may be confusing, if the content-type is application/json, the type of body will be auto parsed to
61+
DogBody or CatBody, otherwise it cannot be parsed to ContentTypeModel or BsonModel.
62+
The body is equivalent to the request variable in Flask, and you can use body.data, body.text, etc ...
63+
"""
64+
print(body)
65+
if isinstance(body, Request):
66+
if body.mimetype == "text/csv":
67+
# processing csv data
68+
...
69+
elif body.mimetype == "application/bson":
70+
# processing bson data
71+
...
72+
else:
73+
# DogBody or CatBody
74+
...
75+
return {"hello": "world"}
76+
77+
78+
@app.post("/b", responses={200: Union[ContentTypeModel, BsonModel]})
79+
def index_b(body: Union[ContentTypeModel, BsonModel]):
80+
"""
81+
This may be confusing, if the content-type is application/json, the type of body will be auto parsed to
82+
DogBody or CatBody, otherwise it cannot be parsed to ContentTypeModel or BsonModel.
83+
The body is equivalent to the request variable in Flask, and you can use body.data, body.text, etc ...
84+
"""
85+
print(body)
86+
if isinstance(body, Request):
87+
if body.mimetype == "text/csv":
88+
# processing csv data
89+
...
90+
elif body.mimetype == "application/bson":
91+
# processing bson data
92+
...
93+
else:
94+
# DogBody or CatBody
95+
...
96+
return {"hello": "world"}
97+
98+
99+
@pytest.fixture
100+
def client():
101+
client = app.test_client()
102+
103+
return client
104+
105+
106+
def test_openapi(client):
107+
resp = client.get("/openapi/openapi.json")
108+
assert resp.status_code == 200
109+
110+
resp = client.post("/a", json={"a": 1, "b": "2"})
111+
assert resp.status_code == 200
112+
113+
resp = client.post("/a", data="a,b,c\n1,2,3", headers={"Content-Type": "text/csv"})
114+
assert resp.status_code == 200

tests/test_restapi.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99

1010
import pytest
1111
from flask import Response
12-
from pydantic import BaseModel, Field, RootModel
12+
from pydantic import BaseModel, RootModel, Field
1313

14-
from flask_openapi3 import ExternalDocumentation, Info, OpenAPI, Tag
14+
from flask_openapi3 import ExternalDocumentation, Server
15+
from flask_openapi3 import Info, Tag
16+
from flask_openapi3 import OpenAPI
1517

1618
info = Info(title="book API", version="1.0.0")
1719

@@ -44,6 +46,8 @@ def get_operation_id_for_path_callback(*, name: str, path: str, method: str) ->
4446

4547
class BookQuery(BaseModel):
4648
age: Optional[int] = Field(None, description="Age")
49+
author: str
50+
none: Optional[None] = None
4751

4852

4953
class BookBody(BaseModel):
@@ -96,10 +100,15 @@ def client():
96100
tags=[book_tag],
97101
operation_id="get_book_id",
98102
external_docs=ExternalDocumentation(
99-
url="https://www.openapis.org/", description="Something great got better, get excited!"
100-
),
103+
url="https://www.openapis.org/",
104+
description="Something great got better, get excited!"),
105+
servers=[Server(
106+
url="http://127.0.0.1:5000",
107+
variables=None
108+
)],
101109
responses={"200": BookResponse},
102110
security=security,
111+
deprecated=True,
103112
)
104113
def get_book(path: BookPath):
105114
"""Get a book
@@ -110,8 +119,8 @@ def get_book(path: BookPath):
110119
return NotFoundResponse().model_dump(), 404
111120

112121

113-
@app.get("/book", tags=[book_tag], responses={"200": BookListResponseV1})
114-
def get_books(query: BookBody):
122+
@app.get('/book', tags=[book_tag], responses={"200": BookListResponseV1})
123+
def get_books(query: BookQuery):
115124
"""get books
116125
to get all books
117126
"""

0 commit comments

Comments
 (0)