APIFlask 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apiflask-2.2.0 → apiflask-2.3.0}/CHANGES.md +19 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/PKG-INFO +1 -1
- {apiflask-2.2.0 → apiflask-2.3.0}/pyproject.toml +1 -1
- {apiflask-2.2.0 → apiflask-2.3.0}/requirements/dev.txt +6 -6
- {apiflask-2.2.0 → apiflask-2.3.0}/requirements/docs.txt +17 -17
- {apiflask-2.2.0 → apiflask-2.3.0}/requirements/examples.txt +7 -7
- {apiflask-2.2.0 → apiflask-2.3.0}/requirements/min-versions.txt +4 -4
- {apiflask-2.2.0 → apiflask-2.3.0}/requirements/tests.txt +6 -6
- {apiflask-2.2.0 → apiflask-2.3.0}/requirements/typing.txt +1 -1
- {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/PKG-INFO +1 -1
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/app.py +13 -13
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/scaffold.py +74 -2
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorator_input.py +173 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tox.ini +1 -1
- {apiflask-2.2.0 → apiflask-2.3.0}/LICENSE +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/MANIFEST.in +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/NOTICE +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/README.md +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/README.md +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/auth/basic_auth/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/auth/token_auth/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/base_response/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/basic/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/blueprint_tags/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/cbv/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/dataclass/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/file_upload/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/file_upload/upload/.gitkeep +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/basic/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/custom_decorators/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/static_docs/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/static_docs/generate_docs.sh +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/static_docs/index.html +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/static_docs/openapi.json +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/orm/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/pagination/app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/requirements.txt +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/examples/test_examples.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/setup.cfg +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/SOURCES.txt +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/dependency_links.txt +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/entry_points.txt +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/requires.txt +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/top_level.txt +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/__init__.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/blueprint.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/commands.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/exceptions.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/fields.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/helpers.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/openapi.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/py.typed +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/route.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/schemas.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/security.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/settings.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/types.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/ui_templates.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/validators.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/views.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/__init__.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/conftest.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/schemas.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_app.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_apps/__init__.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_async.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_base_response.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_blueprint.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_commands.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorator_auth_required.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorator_doc.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorator_output.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorators.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_exceptions.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_fields.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_helpers.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_basic.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_blueprint.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_extensions.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_headers.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_info.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_paths.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_security.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_tags.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_route.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_schemas.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_security.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_api_docs.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_auto_behaviour.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_openapi_fields.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_openapi_spec.py +0 -0
- {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_response_customization.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## Version 2.3.0
|
|
2
|
+
|
|
3
|
+
Released: 2024/11/25
|
|
4
|
+
|
|
5
|
+
- Support skipping the validation for the request body with `@input(validation=False)` ([issue #573][issue_573]).
|
|
6
|
+
- Enable CI test for Python 3.13.
|
|
7
|
+
|
|
8
|
+
[issue_573]: https://github.com/apiflask/apiflask/issues/573
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Version 2.2.1
|
|
12
|
+
|
|
13
|
+
Released: 2024/9/8
|
|
14
|
+
|
|
15
|
+
- Fix OpenAPI spec for multiple content types with `json_or_form` location ([issue #600][issue_600]).
|
|
16
|
+
|
|
17
|
+
[issue_600]: https://github.com/apiflask/apiflask/issues/600
|
|
18
|
+
|
|
19
|
+
|
|
1
20
|
## Version 2.2.0
|
|
2
21
|
|
|
3
22
|
Released: 2024/8/3
|
|
@@ -4,7 +4,7 @@ description = "A lightweight web API framework based on Flask and marshmallow-co
|
|
|
4
4
|
readme = "README.md"
|
|
5
5
|
license = { text = "MIT" }
|
|
6
6
|
authors = [{ name = "Grey Li", email = "withlihui@gmail.com" }]
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
classifiers = [
|
|
9
9
|
"Development Status :: 3 - Alpha",
|
|
10
10
|
"Environment :: Web Environment",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
-r examples.txt
|
|
10
10
|
-r tests.txt
|
|
11
11
|
-r typing.txt
|
|
12
|
-
build==1.2.
|
|
12
|
+
build==1.2.2
|
|
13
13
|
# via
|
|
14
14
|
# -r requirements/dev.in
|
|
15
15
|
# pip-tools
|
|
16
|
-
cachetools==5.
|
|
16
|
+
cachetools==5.5.0
|
|
17
17
|
# via tox
|
|
18
18
|
cfgv==3.4.0
|
|
19
19
|
# via pre-commit
|
|
@@ -21,7 +21,7 @@ chardet==5.2.0
|
|
|
21
21
|
# via tox
|
|
22
22
|
distlib==0.3.8
|
|
23
23
|
# via virtualenv
|
|
24
|
-
filelock==3.
|
|
24
|
+
filelock==3.16.0
|
|
25
25
|
# via
|
|
26
26
|
# tox
|
|
27
27
|
# virtualenv
|
|
@@ -43,13 +43,13 @@ pyproject-hooks==1.1.0
|
|
|
43
43
|
# pip-tools
|
|
44
44
|
toposort==1.10
|
|
45
45
|
# via pip-compile-multi
|
|
46
|
-
tox==4.
|
|
46
|
+
tox==4.18.1
|
|
47
47
|
# via -r requirements/dev.in
|
|
48
|
-
virtualenv==20.26.
|
|
48
|
+
virtualenv==20.26.4
|
|
49
49
|
# via
|
|
50
50
|
# pre-commit
|
|
51
51
|
# tox
|
|
52
|
-
wheel==0.
|
|
52
|
+
wheel==0.44.0
|
|
53
53
|
# via pip-tools
|
|
54
54
|
|
|
55
55
|
# The following packages are considered to be unsafe in a requirements file:
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
#
|
|
6
6
|
# pip-compile-multi
|
|
7
7
|
#
|
|
8
|
-
babel==2.
|
|
8
|
+
babel==2.16.0
|
|
9
9
|
# via mkdocs-material
|
|
10
|
-
|
|
11
|
-
# via griffe
|
|
12
|
-
certifi==2024.7.4
|
|
10
|
+
certifi==2024.8.30
|
|
13
11
|
# via requests
|
|
14
12
|
charset-normalizer==3.3.2
|
|
15
13
|
# via requests
|
|
@@ -23,16 +21,16 @@ colorama==0.4.6
|
|
|
23
21
|
# mkdocs-material
|
|
24
22
|
ghp-import==2.1.0
|
|
25
23
|
# via mkdocs
|
|
26
|
-
griffe==
|
|
24
|
+
griffe==1.2.0
|
|
27
25
|
# via mkdocstrings-python
|
|
28
|
-
idna==3.
|
|
26
|
+
idna==3.8
|
|
29
27
|
# via requests
|
|
30
28
|
jinja2==3.1.4
|
|
31
29
|
# via
|
|
32
30
|
# mkdocs
|
|
33
31
|
# mkdocs-material
|
|
34
32
|
# mkdocstrings
|
|
35
|
-
markdown==3.
|
|
33
|
+
markdown==3.7
|
|
36
34
|
# via
|
|
37
35
|
# mkdocs
|
|
38
36
|
# mkdocs-autorefs
|
|
@@ -49,32 +47,34 @@ mergedeep==1.3.4
|
|
|
49
47
|
# via
|
|
50
48
|
# mkdocs
|
|
51
49
|
# mkdocs-get-deps
|
|
52
|
-
mkdocs==1.6.
|
|
50
|
+
mkdocs==1.6.1
|
|
53
51
|
# via
|
|
54
52
|
# mkdocs-autorefs
|
|
55
53
|
# mkdocs-material
|
|
56
54
|
# mkdocstrings
|
|
57
|
-
mkdocs-autorefs==1.0
|
|
58
|
-
# via
|
|
55
|
+
mkdocs-autorefs==1.2.0
|
|
56
|
+
# via
|
|
57
|
+
# mkdocstrings
|
|
58
|
+
# mkdocstrings-python
|
|
59
59
|
mkdocs-get-deps==0.2.0
|
|
60
60
|
# via mkdocs
|
|
61
|
-
mkdocs-material==9.5.
|
|
61
|
+
mkdocs-material==9.5.34
|
|
62
62
|
# via -r requirements/docs.in
|
|
63
63
|
mkdocs-material-extensions==1.3.1
|
|
64
64
|
# via mkdocs-material
|
|
65
|
-
mkdocstrings==0.
|
|
65
|
+
mkdocstrings==0.26.1
|
|
66
66
|
# via
|
|
67
67
|
# -r requirements/docs.in
|
|
68
68
|
# mkdocstrings-python
|
|
69
|
-
mkdocstrings-python==1.
|
|
69
|
+
mkdocstrings-python==1.11.1
|
|
70
70
|
# via -r requirements/docs.in
|
|
71
71
|
packaging==24.1
|
|
72
72
|
# via mkdocs
|
|
73
|
-
paginate==0.5.
|
|
73
|
+
paginate==0.5.7
|
|
74
74
|
# via mkdocs-material
|
|
75
75
|
pathspec==0.12.1
|
|
76
76
|
# via mkdocs
|
|
77
|
-
platformdirs==4.
|
|
77
|
+
platformdirs==4.3.1
|
|
78
78
|
# via
|
|
79
79
|
# mkdocs-get-deps
|
|
80
80
|
# mkdocstrings
|
|
@@ -86,7 +86,7 @@ pymdown-extensions==10.9
|
|
|
86
86
|
# mkdocstrings
|
|
87
87
|
python-dateutil==2.9.0.post0
|
|
88
88
|
# via ghp-import
|
|
89
|
-
pyyaml==6.0.
|
|
89
|
+
pyyaml==6.0.2
|
|
90
90
|
# via
|
|
91
91
|
# mkdocs
|
|
92
92
|
# mkdocs-get-deps
|
|
@@ -102,5 +102,5 @@ six==1.16.0
|
|
|
102
102
|
# via python-dateutil
|
|
103
103
|
urllib3==2.2.2
|
|
104
104
|
# via requests
|
|
105
|
-
watchdog==4.0.
|
|
105
|
+
watchdog==4.0.2
|
|
106
106
|
# via mkdocs
|
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
#
|
|
6
6
|
# pip-compile-multi
|
|
7
7
|
#
|
|
8
|
-
authlib==1.3.
|
|
8
|
+
authlib==1.3.2
|
|
9
9
|
# via -r requirements/examples.in
|
|
10
10
|
blinker==1.8.2
|
|
11
11
|
# via flask
|
|
12
|
-
cffi==1.
|
|
12
|
+
cffi==1.17.1
|
|
13
13
|
# via cryptography
|
|
14
14
|
click==8.1.7
|
|
15
15
|
# via flask
|
|
16
|
-
cryptography==43.0.
|
|
16
|
+
cryptography==43.0.1
|
|
17
17
|
# via authlib
|
|
18
18
|
flask==3.0.3
|
|
19
19
|
# via flask-sqlalchemy
|
|
20
20
|
flask-sqlalchemy==3.1.1
|
|
21
21
|
# via -r requirements/examples.in
|
|
22
|
-
greenlet==3.0
|
|
22
|
+
greenlet==3.1.0
|
|
23
23
|
# via sqlalchemy
|
|
24
24
|
itsdangerous==2.2.0
|
|
25
25
|
# via flask
|
|
@@ -29,7 +29,7 @@ markupsafe==2.1.5
|
|
|
29
29
|
# via
|
|
30
30
|
# jinja2
|
|
31
31
|
# werkzeug
|
|
32
|
-
marshmallow==3.
|
|
32
|
+
marshmallow==3.22.0
|
|
33
33
|
# via marshmallow-dataclass
|
|
34
34
|
marshmallow-dataclass==8.7.0
|
|
35
35
|
# via -r requirements/examples.in
|
|
@@ -39,7 +39,7 @@ packaging==24.1
|
|
|
39
39
|
# via marshmallow
|
|
40
40
|
pycparser==2.22
|
|
41
41
|
# via cffi
|
|
42
|
-
sqlalchemy==2.0.
|
|
42
|
+
sqlalchemy==2.0.34
|
|
43
43
|
# via flask-sqlalchemy
|
|
44
44
|
typeguard==4.0.1
|
|
45
45
|
# via marshmallow-dataclass
|
|
@@ -51,5 +51,5 @@ typing-extensions==4.12.2
|
|
|
51
51
|
# typing-inspect
|
|
52
52
|
typing-inspect==0.9.0
|
|
53
53
|
# via marshmallow-dataclass
|
|
54
|
-
werkzeug==3.0.
|
|
54
|
+
werkzeug==3.0.4
|
|
55
55
|
# via flask
|
|
@@ -9,11 +9,11 @@ apispec==6.0.0
|
|
|
9
9
|
# via -r requirements/min-versions.in
|
|
10
10
|
authlib==1.2.1
|
|
11
11
|
# via -r requirements/min-versions.in
|
|
12
|
-
cffi==1.
|
|
12
|
+
cffi==1.17.1
|
|
13
13
|
# via cryptography
|
|
14
14
|
click==8.1.7
|
|
15
15
|
# via flask
|
|
16
|
-
cryptography==43.0.
|
|
16
|
+
cryptography==43.0.1
|
|
17
17
|
# via authlib
|
|
18
18
|
flask==2.0.0
|
|
19
19
|
# via
|
|
@@ -27,7 +27,7 @@ flask-marshmallow==1.0.0
|
|
|
27
27
|
# via -r requirements/min-versions.in
|
|
28
28
|
flask-sqlalchemy==2.5.1
|
|
29
29
|
# via -r requirements/min-versions.in
|
|
30
|
-
greenlet==3.0
|
|
30
|
+
greenlet==3.1.0
|
|
31
31
|
# via sqlalchemy
|
|
32
32
|
itsdangerous==2.2.0
|
|
33
33
|
# via flask
|
|
@@ -35,7 +35,7 @@ jinja2==3.1.4
|
|
|
35
35
|
# via flask
|
|
36
36
|
markupsafe==2.1.5
|
|
37
37
|
# via jinja2
|
|
38
|
-
marshmallow==3.
|
|
38
|
+
marshmallow==3.22.0
|
|
39
39
|
# via
|
|
40
40
|
# flask-marshmallow
|
|
41
41
|
# marshmallow-dataclass
|
|
@@ -7,19 +7,19 @@
|
|
|
7
7
|
#
|
|
8
8
|
asgiref==3.8.1
|
|
9
9
|
# via -r requirements/tests.in
|
|
10
|
-
attrs==
|
|
10
|
+
attrs==24.2.0
|
|
11
11
|
# via
|
|
12
12
|
# jsonschema
|
|
13
13
|
# referencing
|
|
14
|
-
certifi==2024.
|
|
14
|
+
certifi==2024.8.30
|
|
15
15
|
# via requests
|
|
16
16
|
charset-normalizer==3.3.2
|
|
17
17
|
# via requests
|
|
18
|
-
coverage[toml]==7.6.
|
|
18
|
+
coverage[toml]==7.6.1
|
|
19
19
|
# via pytest-cov
|
|
20
20
|
exceptiongroup==1.2.2
|
|
21
21
|
# via pytest
|
|
22
|
-
idna==3.
|
|
22
|
+
idna==3.8
|
|
23
23
|
# via requests
|
|
24
24
|
iniconfig==2.0.0
|
|
25
25
|
# via pytest
|
|
@@ -51,7 +51,7 @@ pytest==8.3.2
|
|
|
51
51
|
# pytest-cov
|
|
52
52
|
pytest-cov==5.0.0
|
|
53
53
|
# via -r requirements/tests.in
|
|
54
|
-
pyyaml==6.0.
|
|
54
|
+
pyyaml==6.0.2
|
|
55
55
|
# via jsonschema-path
|
|
56
56
|
referencing==0.35.1
|
|
57
57
|
# via
|
|
@@ -62,7 +62,7 @@ requests==2.32.3
|
|
|
62
62
|
# via jsonschema-path
|
|
63
63
|
rfc3339-validator==0.1.4
|
|
64
64
|
# via openapi-schema-validator
|
|
65
|
-
rpds-py==0.
|
|
65
|
+
rpds-py==0.20.0
|
|
66
66
|
# via
|
|
67
67
|
# jsonschema
|
|
68
68
|
# referencing
|
|
@@ -1093,20 +1093,20 @@ class APIFlask(APIScaffold, Flask):
|
|
|
1093
1093
|
|
|
1094
1094
|
# requestBody
|
|
1095
1095
|
if view_func._spec.get('body'):
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1096
|
+
content_types = view_func._spec.get('content_type')
|
|
1097
|
+
if not isinstance(content_types, list):
|
|
1098
|
+
content_types = [content_types]
|
|
1099
|
+
operation['requestBody'] = {'content': {}}
|
|
1100
|
+
for content_type in content_types:
|
|
1101
|
+
operation['requestBody']['content'][content_type] = {
|
|
1102
|
+
'schema': view_func._spec['body'],
|
|
1102
1103
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
operation['requestBody']['content'][content_type]['examples'] = examples
|
|
1104
|
+
if view_func._spec.get('body_example'):
|
|
1105
|
+
example = view_func._spec.get('body_example')
|
|
1106
|
+
operation['requestBody']['content'][content_type]['example'] = example
|
|
1107
|
+
if view_func._spec.get('body_examples'):
|
|
1108
|
+
examples = view_func._spec.get('body_examples')
|
|
1109
|
+
operation['requestBody']['content'][content_type]['examples'] = examples
|
|
1110
1110
|
|
|
1111
1111
|
# security
|
|
1112
1112
|
if custom_security: # custom security
|
|
@@ -6,6 +6,7 @@ from functools import wraps
|
|
|
6
6
|
|
|
7
7
|
from flask import current_app
|
|
8
8
|
from flask import jsonify
|
|
9
|
+
from flask import request
|
|
9
10
|
from flask import Response
|
|
10
11
|
from marshmallow import ValidationError as MarshmallowValidationError
|
|
11
12
|
from webargs.flaskparser import FlaskParser as BaseFlaskParser
|
|
@@ -100,6 +101,46 @@ def _generate_schema_from_mapping(schema: DictSchemaType, schema_name: str | Non
|
|
|
100
101
|
return Schema.from_dict(schema, name=schema_name)() # type: ignore
|
|
101
102
|
|
|
102
103
|
|
|
104
|
+
def _load_raw_data(location: str = 'json') -> dict[t.Any, t.Any]:
|
|
105
|
+
"""
|
|
106
|
+
Load raw data from the specified location in the request.
|
|
107
|
+
|
|
108
|
+
Arguments:
|
|
109
|
+
location (str): Specifies the source of the data to load, which can be one of the following:
|
|
110
|
+
- `json`: Load data from the request's JSON body.
|
|
111
|
+
- `form`: Load data from the request's form.
|
|
112
|
+
- `files` or `form_and_files`: Load data from both the request's form and files.
|
|
113
|
+
- `json_or_form`: Preferably load data from the request's JSON body or request's form.
|
|
114
|
+
- `query`: Load data from the request's query parameters.
|
|
115
|
+
- `path`: Load data from the request's view arguments.
|
|
116
|
+
|
|
117
|
+
`headers` and `cookies` are not supported by this function.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
dict: A dictionary containing the data loaded from the specified location.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValueError: If the provided location argument is not one of the expected values.
|
|
124
|
+
"""
|
|
125
|
+
if location == 'json':
|
|
126
|
+
if request.is_json:
|
|
127
|
+
return request.get_json() or {}
|
|
128
|
+
return {}
|
|
129
|
+
if location == 'form':
|
|
130
|
+
return request.form.to_dict()
|
|
131
|
+
if location in ['files', 'form_and_files']:
|
|
132
|
+
return {**request.files.to_dict(), **request.form.to_dict()}
|
|
133
|
+
if location == 'json_or_form':
|
|
134
|
+
if request.is_json:
|
|
135
|
+
return request.get_json() or {}
|
|
136
|
+
return request.form.to_dict()
|
|
137
|
+
if location in ['query', 'querystring']:
|
|
138
|
+
return request.args.to_dict()
|
|
139
|
+
if location == 'path':
|
|
140
|
+
return request.view_args or {}
|
|
141
|
+
raise ValueError(f'Invalid location argument: {location}')
|
|
142
|
+
|
|
143
|
+
|
|
103
144
|
class APIScaffold:
|
|
104
145
|
"""A base class for [`APIFlask`][apiflask.app.APIFlask] and
|
|
105
146
|
[`APIBlueprint`][apiflask.blueprint.APIBlueprint].
|
|
@@ -209,6 +250,7 @@ class APIScaffold:
|
|
|
209
250
|
schema_name: str | None = None,
|
|
210
251
|
example: t.Any | None = None,
|
|
211
252
|
examples: dict[str, t.Any] | None = None,
|
|
253
|
+
validation: bool = True,
|
|
212
254
|
**kwargs: t.Any,
|
|
213
255
|
) -> t.Callable[[DecoratedType], DecoratedType]:
|
|
214
256
|
"""Add input settings for view functions.
|
|
@@ -262,6 +304,11 @@ class APIScaffold:
|
|
|
262
304
|
},
|
|
263
305
|
}
|
|
264
306
|
```
|
|
307
|
+
validation: Flag to allow disabling of validation on input. Default to `True`.
|
|
308
|
+
|
|
309
|
+
*Version changed: 2.2.2
|
|
310
|
+
|
|
311
|
+
- Add parameter `validation` to allow disabling of validation on input.
|
|
265
312
|
|
|
266
313
|
*Version changed: 2.0.0*
|
|
267
314
|
|
|
@@ -290,6 +337,17 @@ class APIScaffold:
|
|
|
290
337
|
|
|
291
338
|
def decorator(f):
|
|
292
339
|
f = _ensure_sync(f)
|
|
340
|
+
|
|
341
|
+
if not validation:
|
|
342
|
+
|
|
343
|
+
@wraps(f)
|
|
344
|
+
def wrapper(*args: t.Any, **kwargs: t.Any):
|
|
345
|
+
raw_data = _load_raw_data(location)
|
|
346
|
+
kwargs[f'{location}_data'] = raw_data
|
|
347
|
+
return f(*args, **kwargs)
|
|
348
|
+
|
|
349
|
+
return wrapper
|
|
350
|
+
|
|
293
351
|
is_body_location = location in BODY_LOCATIONS
|
|
294
352
|
if is_body_location and hasattr(f, '_spec') and 'body' in f._spec:
|
|
295
353
|
raise RuntimeError(
|
|
@@ -298,7 +356,13 @@ class APIScaffold:
|
|
|
298
356
|
'and "json_or_form").'
|
|
299
357
|
)
|
|
300
358
|
if location == 'json':
|
|
301
|
-
_annotate(
|
|
359
|
+
_annotate(
|
|
360
|
+
f,
|
|
361
|
+
body=schema,
|
|
362
|
+
body_example=example,
|
|
363
|
+
body_examples=examples,
|
|
364
|
+
content_type='application/json',
|
|
365
|
+
)
|
|
302
366
|
elif location == 'form':
|
|
303
367
|
_annotate(
|
|
304
368
|
f,
|
|
@@ -315,6 +379,14 @@ class APIScaffold:
|
|
|
315
379
|
body_examples=examples,
|
|
316
380
|
content_type='multipart/form-data',
|
|
317
381
|
)
|
|
382
|
+
elif location == 'json_or_form':
|
|
383
|
+
_annotate(
|
|
384
|
+
f,
|
|
385
|
+
body=schema,
|
|
386
|
+
body_example=example,
|
|
387
|
+
body_examples=examples,
|
|
388
|
+
content_type=['application/x-www-form-urlencoded', 'application/json'],
|
|
389
|
+
)
|
|
318
390
|
else:
|
|
319
391
|
if not hasattr(f, '_spec') or f._spec.get('args') is None:
|
|
320
392
|
_annotate(f, args=[])
|
|
@@ -406,7 +478,7 @@ class APIScaffold:
|
|
|
406
478
|
See the [docs](https://apiflask.com/openapi/#response-links) for more details
|
|
407
479
|
about setting response links.
|
|
408
480
|
|
|
409
|
-
content_type: The content/media type of the response. It
|
|
481
|
+
content_type: The content/media type of the response. It defaults to `application/json`.
|
|
410
482
|
headers: The schemas of the headers.
|
|
411
483
|
|
|
412
484
|
*Version changed: 2.1.0*
|
|
@@ -11,7 +11,10 @@ from .schemas import Files
|
|
|
11
11
|
from .schemas import Foo
|
|
12
12
|
from .schemas import Form
|
|
13
13
|
from .schemas import FormAndFiles
|
|
14
|
+
from apiflask import Schema
|
|
14
15
|
from apiflask.fields import String
|
|
16
|
+
from apiflask.validators import Length
|
|
17
|
+
from apiflask.validators import OneOf
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def test_input(app, client):
|
|
@@ -190,6 +193,43 @@ def test_input_with_form_and_files_location(app, client):
|
|
|
190
193
|
assert rv.json == {'name': True, 'image': True}
|
|
191
194
|
|
|
192
195
|
|
|
196
|
+
def test_input_with_json_or_form_location(app, client):
|
|
197
|
+
@app.post('/')
|
|
198
|
+
@app.input(Form, location='json_or_form')
|
|
199
|
+
def index(json_or_form_data):
|
|
200
|
+
return json_or_form_data
|
|
201
|
+
|
|
202
|
+
rv = client.get('/openapi.json')
|
|
203
|
+
assert rv.status_code == 200
|
|
204
|
+
osv.validate(rv.json)
|
|
205
|
+
assert (
|
|
206
|
+
'application/x-www-form-urlencoded'
|
|
207
|
+
in rv.json['paths']['/']['post']['requestBody']['content']
|
|
208
|
+
)
|
|
209
|
+
assert 'application/json' in rv.json['paths']['/']['post']['requestBody']['content']
|
|
210
|
+
assert (
|
|
211
|
+
rv.json['paths']['/']['post']['requestBody']['content']['application/json']['schema'][
|
|
212
|
+
'$ref'
|
|
213
|
+
]
|
|
214
|
+
== '#/components/schemas/Form'
|
|
215
|
+
)
|
|
216
|
+
assert (
|
|
217
|
+
rv.json['paths']['/']['post']['requestBody']['content'][
|
|
218
|
+
'application/x-www-form-urlencoded'
|
|
219
|
+
]['schema']['$ref']
|
|
220
|
+
== '#/components/schemas/Form'
|
|
221
|
+
)
|
|
222
|
+
assert 'Form' in rv.json['components']['schemas']
|
|
223
|
+
|
|
224
|
+
rv = client.post('/', data={'name': 'foo'})
|
|
225
|
+
assert rv.status_code == 200
|
|
226
|
+
assert rv.json == {'name': 'foo'}
|
|
227
|
+
|
|
228
|
+
rv = client.post('/', json={'name': 'foo'})
|
|
229
|
+
assert rv.status_code == 200
|
|
230
|
+
assert rv.json == {'name': 'foo'}
|
|
231
|
+
|
|
232
|
+
|
|
193
233
|
def test_input_with_path_location(app, client):
|
|
194
234
|
@app.get('/<image_type>')
|
|
195
235
|
@app.input(EnumPathParameter, location='path')
|
|
@@ -376,3 +416,136 @@ def test_input_body_example(app, client):
|
|
|
376
416
|
rv.json['paths']['/baz']['post']['requestBody']['content']['application/json']['examples']
|
|
377
417
|
== examples
|
|
378
418
|
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_skip_validation(app, client):
|
|
422
|
+
incorrect_json = {'name': 'Kitty', 'category': 'unknown'}
|
|
423
|
+
|
|
424
|
+
class PetIn(Schema):
|
|
425
|
+
name = String(required=True, validate=Length(0, 10))
|
|
426
|
+
category = String(required=True, validate=OneOf(['dog', 'cat']))
|
|
427
|
+
|
|
428
|
+
@app.patch('/pets_without_validation/<int:pet_id>')
|
|
429
|
+
@app.input(PetIn, validation=False)
|
|
430
|
+
def pets_without_validation(pet_id, json_data):
|
|
431
|
+
return {'pet_id': pet_id, 'json_data': json_data}
|
|
432
|
+
|
|
433
|
+
no_validated_rv = client.patch('/pets_without_validation/1', json=incorrect_json)
|
|
434
|
+
assert no_validated_rv.status_code == 200
|
|
435
|
+
assert no_validated_rv.json['json_data']['name'] == 'Kitty'
|
|
436
|
+
assert no_validated_rv.json['json_data']['category'] == 'unknown'
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_location_raw_data(app):
|
|
440
|
+
from apiflask.scaffold import _load_raw_data
|
|
441
|
+
|
|
442
|
+
# will raises ValueError
|
|
443
|
+
location = 'unknown'
|
|
444
|
+
with pytest.raises(ValueError, match=f'Invalid location argument: {location}'):
|
|
445
|
+
_load_raw_data('unknown')
|
|
446
|
+
|
|
447
|
+
# `json` location
|
|
448
|
+
with app.test_request_context('/load_raw_data', method='POST', json={'name': 'Kitty'}):
|
|
449
|
+
json_data = _load_raw_data()
|
|
450
|
+
assert json_data['name'] == 'Kitty'
|
|
451
|
+
|
|
452
|
+
with app.test_request_context('/load_raw_data', method='POST', data={'name': 'Kitty'}):
|
|
453
|
+
json_data = _load_raw_data()
|
|
454
|
+
assert json_data == {}
|
|
455
|
+
|
|
456
|
+
# `form` location
|
|
457
|
+
with app.test_request_context(
|
|
458
|
+
'/load_raw_data',
|
|
459
|
+
method='POST',
|
|
460
|
+
data={'username': 'Kitty', 'password': '<PASSWORD>'},
|
|
461
|
+
content_type='application/x-www-form-urlencoded',
|
|
462
|
+
):
|
|
463
|
+
form_data = _load_raw_data('form')
|
|
464
|
+
assert form_data['username'] == 'Kitty'
|
|
465
|
+
assert form_data['password'] == '<PASSWORD>'
|
|
466
|
+
|
|
467
|
+
with app.test_request_context(
|
|
468
|
+
'/load_raw_data',
|
|
469
|
+
method='POST',
|
|
470
|
+
json={'username': 'Kitty', 'password': '<PASSWORD>'},
|
|
471
|
+
):
|
|
472
|
+
form_data = _load_raw_data('form')
|
|
473
|
+
assert form_data == {}
|
|
474
|
+
|
|
475
|
+
# `files` or `form_and_files` location
|
|
476
|
+
with app.test_request_context(
|
|
477
|
+
'/load_raw_data',
|
|
478
|
+
method='POST',
|
|
479
|
+
data={'file': (io.BytesIO(b'Hello World!'), 'demo.txt')},
|
|
480
|
+
content_type='multipart/form-data',
|
|
481
|
+
):
|
|
482
|
+
files_data = _load_raw_data('files')
|
|
483
|
+
assert files_data['file'].filename == 'demo.txt'
|
|
484
|
+
assert files_data['file'].stream.read() == b'Hello World!'
|
|
485
|
+
|
|
486
|
+
with app.test_request_context(
|
|
487
|
+
'/load_raw_data',
|
|
488
|
+
method='POST',
|
|
489
|
+
data={'name': 'Kitty', 'file': (io.BytesIO(b'Hello World2!'), 'demo2.txt')},
|
|
490
|
+
content_type='multipart/form-data',
|
|
491
|
+
):
|
|
492
|
+
files_and_form_rv = _load_raw_data('form_and_files')
|
|
493
|
+
assert files_and_form_rv['file'].filename == 'demo2.txt'
|
|
494
|
+
assert files_and_form_rv['file'].stream.read() == b'Hello World2!'
|
|
495
|
+
assert files_and_form_rv['name'] == 'Kitty'
|
|
496
|
+
|
|
497
|
+
with app.test_request_context(
|
|
498
|
+
'/load_raw_data',
|
|
499
|
+
method='POST',
|
|
500
|
+
json={'username': 'Kitty', 'password': '<PASSWORD>'},
|
|
501
|
+
):
|
|
502
|
+
form_and_files_data = _load_raw_data('form_and_files')
|
|
503
|
+
assert form_and_files_data == {}
|
|
504
|
+
|
|
505
|
+
# `query` location
|
|
506
|
+
with app.test_request_context(
|
|
507
|
+
'/load_raw_data?name=Kitty',
|
|
508
|
+
method='GET',
|
|
509
|
+
):
|
|
510
|
+
query_data = _load_raw_data('query')
|
|
511
|
+
assert query_data == {'name': 'Kitty'}
|
|
512
|
+
|
|
513
|
+
with app.test_request_context(
|
|
514
|
+
'/load_raw_data',
|
|
515
|
+
method='POST',
|
|
516
|
+
):
|
|
517
|
+
query_data = _load_raw_data('query')
|
|
518
|
+
assert query_data == {}
|
|
519
|
+
|
|
520
|
+
# `json_or_form`
|
|
521
|
+
with app.test_request_context(
|
|
522
|
+
'/load_raw_data',
|
|
523
|
+
data={'username': 'Kitty', 'password': '<PASSWORD>'},
|
|
524
|
+
content_type='application/x-www-form-urlencoded',
|
|
525
|
+
):
|
|
526
|
+
json_or_form_rv = _load_raw_data('json_or_form')
|
|
527
|
+
assert json_or_form_rv['username'] == 'Kitty'
|
|
528
|
+
assert json_or_form_rv['password'] == '<PASSWORD>'
|
|
529
|
+
|
|
530
|
+
with app.test_request_context(
|
|
531
|
+
'/load_raw_data',
|
|
532
|
+
json={'currentId': '<current_id>'},
|
|
533
|
+
):
|
|
534
|
+
json_or_form_data = _load_raw_data('json_or_form')
|
|
535
|
+
json_or_form_data['currentId'] = '<current_id>'
|
|
536
|
+
|
|
537
|
+
with app.test_request_context(
|
|
538
|
+
'/load_raw_data',
|
|
539
|
+
method='POST',
|
|
540
|
+
):
|
|
541
|
+
json_or_form_data = _load_raw_data('json_or_form')
|
|
542
|
+
assert json_or_form_data == {}
|
|
543
|
+
|
|
544
|
+
# `path` location
|
|
545
|
+
@app.get('/load_raw_data/<int:pet_id>')
|
|
546
|
+
def load_raw_data(pet_id):
|
|
547
|
+
return _load_raw_data('path')
|
|
548
|
+
|
|
549
|
+
with app.test_request_context('/load_raw_data/1', method='GET'):
|
|
550
|
+
path_data = app.dispatch_request()
|
|
551
|
+
assert path_data == {'pet_id': 1}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|