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.
Files changed (92) hide show
  1. {apiflask-2.2.0 → apiflask-2.3.0}/CHANGES.md +19 -0
  2. {apiflask-2.2.0 → apiflask-2.3.0}/PKG-INFO +1 -1
  3. {apiflask-2.2.0 → apiflask-2.3.0}/pyproject.toml +1 -1
  4. {apiflask-2.2.0 → apiflask-2.3.0}/requirements/dev.txt +6 -6
  5. {apiflask-2.2.0 → apiflask-2.3.0}/requirements/docs.txt +17 -17
  6. {apiflask-2.2.0 → apiflask-2.3.0}/requirements/examples.txt +7 -7
  7. {apiflask-2.2.0 → apiflask-2.3.0}/requirements/min-versions.txt +4 -4
  8. {apiflask-2.2.0 → apiflask-2.3.0}/requirements/tests.txt +6 -6
  9. {apiflask-2.2.0 → apiflask-2.3.0}/requirements/typing.txt +1 -1
  10. {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/PKG-INFO +1 -1
  11. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/app.py +13 -13
  12. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/scaffold.py +74 -2
  13. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorator_input.py +173 -0
  14. {apiflask-2.2.0 → apiflask-2.3.0}/tox.ini +1 -1
  15. {apiflask-2.2.0 → apiflask-2.3.0}/LICENSE +0 -0
  16. {apiflask-2.2.0 → apiflask-2.3.0}/MANIFEST.in +0 -0
  17. {apiflask-2.2.0 → apiflask-2.3.0}/NOTICE +0 -0
  18. {apiflask-2.2.0 → apiflask-2.3.0}/README.md +0 -0
  19. {apiflask-2.2.0 → apiflask-2.3.0}/examples/README.md +0 -0
  20. {apiflask-2.2.0 → apiflask-2.3.0}/examples/auth/basic_auth/app.py +0 -0
  21. {apiflask-2.2.0 → apiflask-2.3.0}/examples/auth/token_auth/app.py +0 -0
  22. {apiflask-2.2.0 → apiflask-2.3.0}/examples/base_response/app.py +0 -0
  23. {apiflask-2.2.0 → apiflask-2.3.0}/examples/basic/app.py +0 -0
  24. {apiflask-2.2.0 → apiflask-2.3.0}/examples/blueprint_tags/app.py +0 -0
  25. {apiflask-2.2.0 → apiflask-2.3.0}/examples/cbv/app.py +0 -0
  26. {apiflask-2.2.0 → apiflask-2.3.0}/examples/dataclass/app.py +0 -0
  27. {apiflask-2.2.0 → apiflask-2.3.0}/examples/file_upload/app.py +0 -0
  28. {apiflask-2.2.0 → apiflask-2.3.0}/examples/file_upload/upload/.gitkeep +0 -0
  29. {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/basic/app.py +0 -0
  30. {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/custom_decorators/app.py +0 -0
  31. {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/static_docs/app.py +0 -0
  32. {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/static_docs/generate_docs.sh +0 -0
  33. {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/static_docs/index.html +0 -0
  34. {apiflask-2.2.0 → apiflask-2.3.0}/examples/openapi/static_docs/openapi.json +0 -0
  35. {apiflask-2.2.0 → apiflask-2.3.0}/examples/orm/app.py +0 -0
  36. {apiflask-2.2.0 → apiflask-2.3.0}/examples/pagination/app.py +0 -0
  37. {apiflask-2.2.0 → apiflask-2.3.0}/examples/requirements.txt +0 -0
  38. {apiflask-2.2.0 → apiflask-2.3.0}/examples/test_examples.py +0 -0
  39. {apiflask-2.2.0 → apiflask-2.3.0}/setup.cfg +0 -0
  40. {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/SOURCES.txt +0 -0
  41. {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/dependency_links.txt +0 -0
  42. {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/entry_points.txt +0 -0
  43. {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/requires.txt +0 -0
  44. {apiflask-2.2.0 → apiflask-2.3.0}/src/APIFlask.egg-info/top_level.txt +0 -0
  45. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/__init__.py +0 -0
  46. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/blueprint.py +0 -0
  47. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/commands.py +0 -0
  48. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/exceptions.py +0 -0
  49. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/fields.py +0 -0
  50. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/helpers.py +0 -0
  51. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/openapi.py +0 -0
  52. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/py.typed +0 -0
  53. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/route.py +0 -0
  54. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/schemas.py +0 -0
  55. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/security.py +0 -0
  56. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/settings.py +0 -0
  57. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/types.py +0 -0
  58. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/ui_templates.py +0 -0
  59. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/validators.py +0 -0
  60. {apiflask-2.2.0 → apiflask-2.3.0}/src/apiflask/views.py +0 -0
  61. {apiflask-2.2.0 → apiflask-2.3.0}/tests/__init__.py +0 -0
  62. {apiflask-2.2.0 → apiflask-2.3.0}/tests/conftest.py +0 -0
  63. {apiflask-2.2.0 → apiflask-2.3.0}/tests/schemas.py +0 -0
  64. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_app.py +0 -0
  65. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_apps/__init__.py +0 -0
  66. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_async.py +0 -0
  67. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_base_response.py +0 -0
  68. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_blueprint.py +0 -0
  69. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_commands.py +0 -0
  70. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorator_auth_required.py +0 -0
  71. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorator_doc.py +0 -0
  72. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorator_output.py +0 -0
  73. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_decorators.py +0 -0
  74. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_exceptions.py +0 -0
  75. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_fields.py +0 -0
  76. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_helpers.py +0 -0
  77. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_basic.py +0 -0
  78. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_blueprint.py +0 -0
  79. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_extensions.py +0 -0
  80. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_headers.py +0 -0
  81. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_info.py +0 -0
  82. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_paths.py +0 -0
  83. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_security.py +0 -0
  84. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_openapi_tags.py +0 -0
  85. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_route.py +0 -0
  86. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_schemas.py +0 -0
  87. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_security.py +0 -0
  88. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_api_docs.py +0 -0
  89. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_auto_behaviour.py +0 -0
  90. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_openapi_fields.py +0 -0
  91. {apiflask-2.2.0 → apiflask-2.3.0}/tests/test_settings_openapi_spec.py +0 -0
  92. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: APIFlask
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A lightweight web API framework based on Flask and marshmallow-code projects.
5
5
  Author-email: Grey Li <withlihui@gmail.com>
6
6
  License: MIT
@@ -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.2.0"
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.1
12
+ build==1.2.2
13
13
  # via
14
14
  # -r requirements/dev.in
15
15
  # pip-tools
16
- cachetools==5.4.0
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.15.4
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.16.0
46
+ tox==4.18.1
47
47
  # via -r requirements/dev.in
48
- virtualenv==20.26.3
48
+ virtualenv==20.26.4
49
49
  # via
50
50
  # pre-commit
51
51
  # tox
52
- wheel==0.43.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.15.0
8
+ babel==2.16.0
9
9
  # via mkdocs-material
10
- backports-strenum==1.3.1
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==0.48.0
24
+ griffe==1.2.0
27
25
  # via mkdocstrings-python
28
- idna==3.7
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.6
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.0
50
+ mkdocs==1.6.1
53
51
  # via
54
52
  # mkdocs-autorefs
55
53
  # mkdocs-material
56
54
  # mkdocstrings
57
- mkdocs-autorefs==1.0.1
58
- # via mkdocstrings
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.31
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.25.2
65
+ mkdocstrings==0.26.1
66
66
  # via
67
67
  # -r requirements/docs.in
68
68
  # mkdocstrings-python
69
- mkdocstrings-python==1.10.7
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.6
73
+ paginate==0.5.7
74
74
  # via mkdocs-material
75
75
  pathspec==0.12.1
76
76
  # via mkdocs
77
- platformdirs==4.2.2
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.1
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.1
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.1
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.16.0
12
+ cffi==1.17.1
13
13
  # via cryptography
14
14
  click==8.1.7
15
15
  # via flask
16
- cryptography==43.0.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.3
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.21.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.31
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.3
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.16.0
12
+ cffi==1.17.1
13
13
  # via cryptography
14
14
  click==8.1.7
15
15
  # via flask
16
- cryptography==43.0.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.3
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.21.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==23.2.0
10
+ attrs==24.2.0
11
11
  # via
12
12
  # jsonschema
13
13
  # referencing
14
- certifi==2024.7.4
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.0
18
+ coverage[toml]==7.6.1
19
19
  # via pytest-cov
20
20
  exceptiongroup==1.2.2
21
21
  # via pytest
22
- idna==3.7
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.1
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.19.1
65
+ rpds-py==0.20.0
66
66
  # via
67
67
  # jsonschema
68
68
  # referencing
@@ -5,7 +5,7 @@
5
5
  #
6
6
  # pip-compile-multi
7
7
  #
8
- mypy==1.11.1
8
+ mypy==1.11.2
9
9
  # via -r requirements/typing.in
10
10
  mypy-extensions==1.0.0
11
11
  # via mypy
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: APIFlask
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A lightweight web API framework based on Flask and marshmallow-code projects.
5
5
  Author-email: Grey Li <withlihui@gmail.com>
6
6
  License: MIT
@@ -1093,20 +1093,20 @@ class APIFlask(APIScaffold, Flask):
1093
1093
 
1094
1094
  # requestBody
1095
1095
  if view_func._spec.get('body'):
1096
- content_type = view_func._spec.get('content_type', 'application/json')
1097
- operation['requestBody'] = {
1098
- 'content': {
1099
- content_type: {
1100
- 'schema': view_func._spec['body'],
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
- 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
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(f, body=schema, body_example=example, body_examples=examples)
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 defautls to `application/json`.
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}
@@ -1,6 +1,6 @@
1
1
  [tox]
2
2
  envlist =
3
- py3{12,11,10,9,8}
3
+ py3{13,12,11,10,9,8}
4
4
  pypy310
5
5
  minimal
6
6
  style
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