Skip to content

Commit 40fb407

Browse files
committed
Merge branch '2.x'
2 parents 117b3a8 + 6a81cac commit 40fb407

23 files changed

+530
-89
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
run: |
2929
python -m pip install --upgrade pip
3030
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
31-
pip install pytest flake8
31+
pip install pytest flake8 openapi-python-client
3232
pip install -e .
3333
- name: Test with pytest
3434
run: |

CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
## 2.0.0 2022-??-??
2+
3+
- [#17](https://github.com/luolingchun/flask-openapi3/issues/17) Support for Nested APIBlueprint enhancement. Thanks @dvaerum
4+
- [#23](https://github.com/luolingchun/flask-openapi3/pull/23) Fixed externalDocs support. Thanks @dvaerum
5+
- Add `flask openapi` command
6+
- Remove export markdown to `flask openapi` command
7+
- Upgrade flask to v2.0
8+
19
## v1.1.4 2022-05-05
210

311
- fix: Trailing slash in APIBlueprint
412

513
## v1.1.3 2022-05-01
614

715
- fix: Find globalns for the unwrapped func
8-
- [#19](https://github.com/luolingchun/flask-openapi3/issues/19) fix: Trailing slash in APIBlueprint. Thinks @ev-agelos
16+
- [#19](https://github.com/luolingchun/flask-openapi3/issues/19) fix: Trailing slash in APIBlueprint. Thanks @ev-agelos
917
- add description for UnprocessableEntity
1018
- remove printouts in `__init__.py`
1119

docs/OpenAPI/Specification.md

+42
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,47 @@
1+
## Specification
2+
13
If you need the complete Specification(json) , go to http://127.0.0.1:5000/openapi/openapi.json
24

5+
**command: `flask openapi`**
6+
7+
*New in v2.0.0*
8+
9+
The `flask openapi` command will export the OpenAPI Specification to console when you execute the command:
10+
11+
```
12+
flask openapi
13+
```
14+
15+
Execute `flask openapi --help` for more information:
16+
17+
```
18+
flask openapi --help
19+
20+
Usage: flask openapi [OPTIONS]
21+
22+
Export the OpenAPI Specification to console or a file
23+
24+
Options:
25+
-o, --output PATH The output file path.
26+
-f, --format [json|yaml|markdown]
27+
The output file format.
28+
-i, --indent INTEGER The indentation for JSON dumps.
29+
-a, --ensure_ascii Ensure ASCII characters or not. Defaults to
30+
False.
31+
--help Show this message and exit.
32+
33+
```
34+
35+
!!! info
36+
37+
You need to manually install `pyyaml` using pip:
38+
```bash
39+
pip install flask-openapi3[yaml]
40+
41+
# or
42+
pip install pyyaml
43+
```
44+
345
## doc_ui
446

547
You can pass `doc_ui=False` to disable the `OpenAPI spec` when init `OpenAPI `.

docs/Quickstart.md

+40-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ from flask_openapi3 import OpenAPI
7272

7373
app = OpenAPI(__name__)
7474

75-
api = APIBlueprint('/book', __name__, url_prefix='/api')
75+
api = APIBlueprint('book', __name__, url_prefix='/api')
7676

7777

7878
@api.post('/book')
@@ -86,3 +86,42 @@ app.register_api(api)
8686
if __name__ == '__main__':
8787
app.run()
8888
```
89+
90+
## Nested APIBlueprint
91+
92+
*New in v2.0.0*
93+
94+
Allow an **API Blueprint** to be registered on another **API Blueprint**.
95+
96+
For more information, please see [Flask Nesting Blueprints](https://flask.palletsprojects.com/en/latest/blueprints/#nesting-blueprints).
97+
98+
```python hl_lines="21 22"
99+
from flask_openapi3 import OpenAPI, APIBlueprint
100+
101+
app = OpenAPI(__name__)
102+
103+
api = APIBlueprint('book', __name__, url_prefix='/api/book')
104+
api_english = APIBlueprint('english', __name__)
105+
api_chinese = APIBlueprint('chinese', __name__)
106+
107+
108+
@api_english.post('/english')
109+
def create_english_book():
110+
return {"message": "english"}
111+
112+
113+
@api_chinese.post('/chinese')
114+
def create_chinese_book():
115+
return {"message": "chinese"}
116+
117+
118+
# register nested api
119+
api.register_api(api_english)
120+
api.register_api(api_chinese)
121+
# register api
122+
app.register_api(api)
123+
124+
if __name__ == '__main__':
125+
app.run(debug=True)
126+
```
127+

docs/Quickstart.zh.md

+39
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,42 @@ app.register_api(api)
8787
if __name__ == '__main__':
8888
app.run()
8989
```
90+
91+
## 嵌套 APIBlueprint
92+
93+
*v2.0.0 新增*
94+
95+
允许一个 **API Blueprint** 被另一个 **API Blueprint** 注册。
96+
97+
更多信息请查看 [Flask Nesting Blueprints](https://flask.palletsprojects.com/en/latest/blueprints/#nesting-blueprints)
98+
99+
```python hl_lines="21 22"
100+
from flask_openapi3 import OpenAPI, APIBlueprint
101+
102+
app = OpenAPI(__name__)
103+
104+
api = APIBlueprint('book', __name__, url_prefix='/api/book')
105+
api_english = APIBlueprint('english', __name__)
106+
api_chinese = APIBlueprint('chinese', __name__)
107+
108+
109+
@api_english.post('/english')
110+
def create_english_book():
111+
return {"message": "english"}
112+
113+
114+
@api_chinese.post('/chinese')
115+
def create_chinese_book():
116+
return {"message": "chinese"}
117+
118+
119+
# register nested api
120+
api.register_api(api_english)
121+
api.register_api(api_chinese)
122+
# register api
123+
app.register_api(api)
124+
125+
if __name__ == '__main__':
126+
app.run(debug=True)
127+
```
128+

examples/nested_apiblueprint_demo.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from flask_openapi3 import OpenAPI, APIBlueprint
2+
3+
app = OpenAPI(__name__)
4+
5+
api = APIBlueprint('book', __name__, url_prefix='/api/book')
6+
api_english = APIBlueprint('english', __name__)
7+
api_chinese = APIBlueprint('chinese', __name__)
8+
9+
10+
@api_english.post('/english')
11+
def create_english_book():
12+
return {"message": "english"}
13+
14+
15+
@api_chinese.post('/chinese')
16+
def create_chinese_book():
17+
return {"message": "chinese"}
18+
19+
20+
# register nested api
21+
api.register_api(api_english)
22+
api.register_api(api_chinese)
23+
# register api
24+
app.register_api(api)
25+
26+
if __name__ == '__main__':
27+
app.run(debug=True)

examples/response_demo.py

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ def hello(path: HelloPath):
3434
return response
3535

3636

37+
@bp.get("/hello_no_response/<string:name>", responses={"204": None})
38+
def hello_no_response(path: HelloPath):
39+
message = {"message": f"""Hello {path.name}!"""}
40+
41+
# This message will never be returned because the http code (NO_CONTENT) doesn't return anything
42+
response = make_response(message, HTTPStatus.NO_CONTENT)
43+
response.mimetype = "application/json"
44+
return response
45+
46+
3747
app.register_api(bp)
3848

3949
if __name__ == "__main__":

flask_openapi3/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# @Author : llc
33
# @Time : 2022/4/30 9:20
44

5-
__version__ = '1.1.4'
5+
__version__ = '2.0.0rc1'

flask_openapi3/api_blueprint.py

+44-16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .do_wrapper import _do_wrapper
1313
from .http import HTTPMethod
1414
from .models import Tag, Components
15+
from .types import OpenAPIResponsesType
1516
from .utils import get_openapi_path, get_operation, get_responses, parse_and_store_tags, parse_parameters, \
1617
validate_responses_type, parse_method, get_operation_id_for_path
1718

@@ -24,7 +25,7 @@ def __init__(
2425
*,
2526
abp_tags: Optional[List[Tag]] = None,
2627
abp_security: Optional[List[Dict[str, List[str]]]] = None,
27-
abp_responses: Optional[Dict[str, Type[BaseModel]]] = None,
28+
abp_responses: OpenAPIResponsesType = None,
2829
doc_ui: bool = True,
2930
**kwargs: Any
3031
) -> None:
@@ -53,6 +54,28 @@ def __init__(
5354
self.abp_responses = abp_responses or {}
5455
self.doc_ui = doc_ui
5556

57+
def register_api(self, api: "APIBlueprint") -> None:
58+
"""Register a nested APIBlueprint"""
59+
if api is self:
60+
raise ValueError("Cannot register a api blueprint on itself")
61+
62+
for tag in api.tags:
63+
if tag.name not in self.tag_names:
64+
self.tags.append(tag)
65+
66+
for path_url, path_item in api.paths.items():
67+
trail_slash = path_url.endswith('/')
68+
# merge url_prefix and new api blueprint path url
69+
uri = self.url_prefix.rstrip("/") + "/" + path_url.lstrip("/") if self.url_prefix else path_url
70+
# strip the right slash
71+
if not trail_slash:
72+
uri = uri.rstrip('/')
73+
self.paths[uri] = path_item
74+
75+
self.components_schemas.update(**api.components_schemas)
76+
77+
self.register_blueprint(api)
78+
5679
def _do_decorator(
5780
self,
5881
rule: str,
@@ -145,12 +168,13 @@ def get(
145168
tags: Optional[List[Tag]] = None,
146169
summary: Optional[str] = None,
147170
description: Optional[str] = None,
148-
responses: Optional[Dict[str, Type[BaseModel]]] = None,
171+
responses: OpenAPIResponsesType = None,
149172
extra_responses: Optional[Dict[str, dict]] = None,
150173
security: Optional[List[Dict[str, List[Any]]]] = None,
151174
deprecated: Optional[bool] = None,
152175
operation_id: Optional[str] = None,
153-
doc_ui: bool = True
176+
doc_ui: bool = True,
177+
**options: Any
154178
) -> Callable:
155179
"""Decorator for rest api, like: app.route(methods=['GET'])"""
156180

@@ -186,7 +210,7 @@ def wrapper(**kwargs) -> Response:
186210
)
187211
return resp
188212

189-
options = {"methods": [HTTPMethod.GET]}
213+
options.update({"methods": [HTTPMethod.GET]})
190214
self.add_url_rule(rule, view_func=wrapper, **options)
191215

192216
return wrapper
@@ -200,12 +224,13 @@ def post(
200224
tags: Optional[List[Tag]] = None,
201225
summary: Optional[str] = None,
202226
description: Optional[str] = None,
203-
responses: Optional[Dict[str, Type[BaseModel]]] = None,
227+
responses: OpenAPIResponsesType = None,
204228
extra_responses: Optional[Dict[str, dict]] = None,
205229
security: Optional[List[Dict[str, List[Any]]]] = None,
206230
deprecated: Optional[bool] = None,
207231
operation_id: Optional[str] = None,
208-
doc_ui: bool = True
232+
doc_ui: bool = True,
233+
**options: Any
209234
) -> Callable:
210235
"""Decorator for rest api, like: app.route(methods=['POST'])"""
211236

@@ -241,7 +266,7 @@ def wrapper(**kwargs) -> Response:
241266
)
242267
return resp
243268

244-
options = {"methods": [HTTPMethod.POST]}
269+
options.update({"methods": [HTTPMethod.POST]})
245270
self.add_url_rule(rule, view_func=wrapper, **options)
246271

247272
return wrapper
@@ -255,12 +280,13 @@ def put(
255280
tags: Optional[List[Tag]] = None,
256281
summary: Optional[str] = None,
257282
description: Optional[str] = None,
258-
responses: Optional[Dict[str, Type[BaseModel]]] = None,
283+
responses: OpenAPIResponsesType = None,
259284
extra_responses: Optional[Dict[str, dict]] = None,
260285
security: Optional[List[Dict[str, List[Any]]]] = None,
261286
deprecated: Optional[bool] = None,
262287
operation_id: Optional[str] = None,
263-
doc_ui: bool = True
288+
doc_ui: bool = True,
289+
**options: Any
264290
) -> Callable:
265291
"""Decorator for rest api, like: app.route(methods=['PUT'])"""
266292

@@ -296,7 +322,7 @@ def wrapper(**kwargs) -> Response:
296322
)
297323
return resp
298324

299-
options = {"methods": [HTTPMethod.PUT]}
325+
options.update({"methods": [HTTPMethod.PUT]})
300326
self.add_url_rule(rule, view_func=wrapper, **options)
301327

302328
return wrapper
@@ -310,12 +336,13 @@ def delete(
310336
tags: Optional[List[Tag]] = None,
311337
summary: Optional[str] = None,
312338
description: Optional[str] = None,
313-
responses: Optional[Dict[str, Type[BaseModel]]] = None,
339+
responses: OpenAPIResponsesType = None,
314340
extra_responses: Optional[Dict[str, dict]] = None,
315341
security: Optional[List[Dict[str, List[Any]]]] = None,
316342
deprecated: Optional[bool] = None,
317343
operation_id: Optional[str] = None,
318-
doc_ui: bool = True
344+
doc_ui: bool = True,
345+
**options: Any
319346
) -> Callable:
320347
"""Decorator for rest api, like: app.route(methods=['DELETE'])"""
321348

@@ -351,7 +378,7 @@ def wrapper(**kwargs) -> Response:
351378
)
352379
return resp
353380

354-
options = {"methods": [HTTPMethod.DELETE]}
381+
options.update({"methods": [HTTPMethod.DELETE]})
355382
self.add_url_rule(rule, view_func=wrapper, **options)
356383

357384
return wrapper
@@ -365,12 +392,13 @@ def patch(
365392
tags: Optional[List[Tag]] = None,
366393
summary: Optional[str] = None,
367394
description: Optional[str] = None,
368-
responses: Optional[Dict[str, Type[BaseModel]]] = None,
395+
responses: OpenAPIResponsesType = None,
369396
extra_responses: Optional[Dict[str, dict]] = None,
370397
security: Optional[List[Dict[str, List[Any]]]] = None,
371398
deprecated: Optional[bool] = None,
372399
operation_id: Optional[str] = None,
373-
doc_ui: bool = True
400+
doc_ui: bool = True,
401+
**options: Any
374402
) -> Callable:
375403
"""Decorator for rest api, like: app.route(methods=['PATCH'])"""
376404

@@ -406,7 +434,7 @@ def wrapper(**kwargs) -> Response:
406434
)
407435
return resp
408436

409-
options = {"methods": [HTTPMethod.PATCH]}
437+
options.update({"methods": [HTTPMethod.PATCH]})
410438
self.add_url_rule(rule, view_func=wrapper, **options)
411439

412440
return wrapper

0 commit comments

Comments
 (0)