APIFlask 2.3.0__tar.gz → 2.3.2__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.3.0 → apiflask-2.3.2}/CHANGES.md +21 -1
- {apiflask-2.3.0 → apiflask-2.3.2}/PKG-INFO +2 -1
- {apiflask-2.3.0 → apiflask-2.3.2}/pyproject.toml +2 -1
- {apiflask-2.3.0 → apiflask-2.3.2}/src/APIFlask.egg-info/PKG-INFO +2 -1
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/app.py +6 -4
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/scaffold.py +21 -55
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_decorator_input.py +46 -113
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_openapi_headers.py +17 -12
- {apiflask-2.3.0 → apiflask-2.3.2}/LICENSE +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/MANIFEST.in +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/NOTICE +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/README.md +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/README.md +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/auth/basic_auth/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/auth/token_auth/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/base_response/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/basic/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/blueprint_tags/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/cbv/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/dataclass/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/file_upload/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/file_upload/upload/.gitkeep +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/openapi/basic/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/openapi/custom_decorators/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/openapi/static_docs/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/openapi/static_docs/generate_docs.sh +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/openapi/static_docs/index.html +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/openapi/static_docs/openapi.json +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/orm/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/pagination/app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/requirements.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/examples/test_examples.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/requirements/dev.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/requirements/docs.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/requirements/examples.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/requirements/min-versions.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/requirements/tests.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/requirements/typing.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/setup.cfg +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/APIFlask.egg-info/SOURCES.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/APIFlask.egg-info/dependency_links.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/APIFlask.egg-info/entry_points.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/APIFlask.egg-info/requires.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/APIFlask.egg-info/top_level.txt +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/__init__.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/blueprint.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/commands.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/exceptions.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/fields.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/helpers.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/openapi.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/py.typed +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/route.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/schemas.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/security.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/settings.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/types.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/ui_templates.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/validators.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/src/apiflask/views.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/__init__.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/conftest.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/schemas.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_app.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_apps/__init__.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_async.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_base_response.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_blueprint.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_commands.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_decorator_auth_required.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_decorator_doc.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_decorator_output.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_decorators.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_exceptions.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_fields.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_helpers.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_openapi_basic.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_openapi_blueprint.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_openapi_extensions.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_openapi_info.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_openapi_paths.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_openapi_security.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_openapi_tags.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_route.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_schemas.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_security.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_settings_api_docs.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_settings_auto_behaviour.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_settings_openapi_fields.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_settings_openapi_spec.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tests/test_settings_response_customization.py +0 -0
- {apiflask-2.3.0 → apiflask-2.3.2}/tox.ini +0 -0
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## Version 2.3.2
|
|
2
|
+
|
|
3
|
+
Released: 2024/12/15
|
|
4
|
+
|
|
5
|
+
- Fix response headers to be compliant with the OpenAPI specification for versions 3.0.0+ ([issue #631][issue_631]).
|
|
6
|
+
- Fix input data loading implementation when input validation is skipped ([issue #629][issue_629]).
|
|
7
|
+
|
|
8
|
+
[issue_629]: https://github.com/apiflask/apiflask/issues/629
|
|
9
|
+
[issue_631]: https://github.com/apiflask/apiflask/issues/631
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Version 2.3.1
|
|
13
|
+
|
|
14
|
+
Released: 2024/12/7
|
|
15
|
+
|
|
16
|
+
- Include input documentation in API spec when specifying `validation=False` on `@input` decorator ([issue #626][issue_626]).
|
|
17
|
+
|
|
18
|
+
[issue_626]: https://github.com/apiflask/apiflask/issues/626
|
|
19
|
+
|
|
20
|
+
|
|
1
21
|
## Version 2.3.0
|
|
2
22
|
|
|
3
23
|
Released: 2024/11/25
|
|
@@ -70,7 +90,7 @@ Released: 2023/12/16
|
|
|
70
90
|
- Allow adding multiple media types for a response ([issue #494][issue_494]).
|
|
71
91
|
- Support adding headers to the response schema ([issue #507][issue_507]).
|
|
72
92
|
- Add support for adding decorators to the OpenAPI spec and documentation UI endpoints ([issue #483][issue_483]).
|
|
73
|
-
- Fix the base
|
|
93
|
+
- Fix the base response schema inconsistency issue.
|
|
74
94
|
|
|
75
95
|
[issue_494]: https://github.com/apiflask/apiflask/issues/494
|
|
76
96
|
[issue_507]: https://github.com/apiflask/apiflask/issues/507
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: APIFlask
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
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
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
23
|
Classifier: License :: OSI Approved :: MIT License
|
|
23
24
|
Classifier: Operating System :: OS Independent
|
|
24
25
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
@@ -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.3.
|
|
7
|
+
version = "2.3.2"
|
|
8
8
|
classifiers = [
|
|
9
9
|
"Development Status :: 3 - Alpha",
|
|
10
10
|
"Environment :: Web Environment",
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3.10",
|
|
17
17
|
"Programming Language :: Python :: 3.11",
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
19
20
|
"License :: OSI Approved :: MIT License",
|
|
20
21
|
"Operating System :: OS Independent",
|
|
21
22
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: APIFlask
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
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
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
23
|
Classifier: License :: OSI Approved :: MIT License
|
|
23
24
|
Classifier: Operating System :: OS Independent
|
|
24
25
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
@@ -1253,12 +1253,14 @@ class APIFlask(APIScaffold, Flask):
|
|
|
1253
1253
|
if links is not None:
|
|
1254
1254
|
operation['responses'][status_code]['links'] = links
|
|
1255
1255
|
if headers_schema is not None:
|
|
1256
|
-
|
|
1256
|
+
header_params = self._ma_plugin.converter.schema2parameters( # type: ignore
|
|
1257
1257
|
headers_schema, location='headers'
|
|
1258
1258
|
)
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1259
|
+
headers = {header['name']: header for header in header_params}
|
|
1260
|
+
for header in headers.values():
|
|
1261
|
+
header.pop('in', None)
|
|
1262
|
+
header.pop('name', None)
|
|
1263
|
+
operation['responses'][status_code]['headers'] = headers
|
|
1262
1264
|
|
|
1263
1265
|
def _add_response_with_schema(
|
|
1264
1266
|
self,
|
|
@@ -6,7 +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
|
+
from flask import request as flask_request
|
|
10
10
|
from flask import Response
|
|
11
11
|
from marshmallow import ValidationError as MarshmallowValidationError
|
|
12
12
|
from webargs.flaskparser import FlaskParser as BaseFlaskParser
|
|
@@ -55,9 +55,14 @@ class FlaskParser(BaseFlaskParser):
|
|
|
55
55
|
error_headers,
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
+
def load_location_data(self, schema: Schema, location: str) -> t.Any:
|
|
59
|
+
"""
|
|
60
|
+
Expose the internal `_load_location_data` method to support loading data without validation
|
|
61
|
+
"""
|
|
62
|
+
return self._load_location_data(schema=schema, req=flask_request, location=location)
|
|
63
|
+
|
|
58
64
|
|
|
59
65
|
parser: FlaskParser = FlaskParser()
|
|
60
|
-
use_args: t.Callable = parser.use_args
|
|
61
66
|
|
|
62
67
|
|
|
63
68
|
def _get_files_and_form(request, schema):
|
|
@@ -101,46 +106,6 @@ def _generate_schema_from_mapping(schema: DictSchemaType, schema_name: str | Non
|
|
|
101
106
|
return Schema.from_dict(schema, name=schema_name)() # type: ignore
|
|
102
107
|
|
|
103
108
|
|
|
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
|
-
|
|
144
109
|
class APIScaffold:
|
|
145
110
|
"""A base class for [`APIFlask`][apiflask.app.APIFlask] and
|
|
146
111
|
[`APIBlueprint`][apiflask.blueprint.APIBlueprint].
|
|
@@ -338,16 +303,6 @@ class APIScaffold:
|
|
|
338
303
|
def decorator(f):
|
|
339
304
|
f = _ensure_sync(f)
|
|
340
305
|
|
|
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
|
-
|
|
351
306
|
is_body_location = location in BODY_LOCATIONS
|
|
352
307
|
if is_body_location and hasattr(f, '_spec') and 'body' in f._spec:
|
|
353
308
|
raise RuntimeError(
|
|
@@ -394,9 +349,20 @@ class APIScaffold:
|
|
|
394
349
|
_annotate(f, omit_default_path_parameters=True)
|
|
395
350
|
# TODO: Support set example for request parameters
|
|
396
351
|
f._spec['args'].append((schema, location))
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
352
|
+
|
|
353
|
+
arg_name_val = arg_name or f'{location}_data'
|
|
354
|
+
|
|
355
|
+
if not validation:
|
|
356
|
+
|
|
357
|
+
@wraps(f)
|
|
358
|
+
def wrapper(*args: t.Any, **kwargs: t.Any):
|
|
359
|
+
location_data = parser.load_location_data(schema=schema, location=location)
|
|
360
|
+
kwargs[arg_name_val] = location_data
|
|
361
|
+
return f(*args, **kwargs)
|
|
362
|
+
|
|
363
|
+
return wrapper
|
|
364
|
+
|
|
365
|
+
return parser.use_args(schema, location=location, arg_name=arg_name_val, **kwargs)(f)
|
|
400
366
|
|
|
401
367
|
return decorator
|
|
402
368
|
|
|
@@ -435,117 +435,50 @@ def test_skip_validation(app, client):
|
|
|
435
435
|
assert no_validated_rv.json['json_data']['name'] == 'Kitty'
|
|
436
436
|
assert no_validated_rv.json['json_data']['category'] == 'unknown'
|
|
437
437
|
|
|
438
|
+
rv = client.get('/openapi.json')
|
|
439
|
+
assert rv.status_code == 200
|
|
440
|
+
assert (
|
|
441
|
+
rv.json['paths']['/pets_without_validation/{pet_id}']['patch']['requestBody']['content'][
|
|
442
|
+
'application/json'
|
|
443
|
+
]['schema']['$ref']
|
|
444
|
+
== '#/components/schemas/PetIn'
|
|
445
|
+
)
|
|
446
|
+
assert 'PetIn' in rv.json['components']['schemas']
|
|
438
447
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
json_data
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
):
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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}
|
|
448
|
+
|
|
449
|
+
@pytest.mark.parametrize('validation', [True, False])
|
|
450
|
+
@pytest.mark.parametrize('payload', [[], [{'bar': 'baz'}], [{'qux': 'baz'}]])
|
|
451
|
+
def test_skip_validation_list_input(app, client, validation, payload):
|
|
452
|
+
class FooIn(Schema):
|
|
453
|
+
bar = String(required=True)
|
|
454
|
+
|
|
455
|
+
@app.put('/foo/bulk')
|
|
456
|
+
@app.input(FooIn(many=True), validation=validation)
|
|
457
|
+
def bulk_put_foo(json_data):
|
|
458
|
+
return json_data
|
|
459
|
+
|
|
460
|
+
rv = client.put('/foo/bulk', json=payload)
|
|
461
|
+
if validation and payload and 'bar' not in payload[0]:
|
|
462
|
+
assert rv.status_code == 422
|
|
463
|
+
else:
|
|
464
|
+
assert rv.status_code == 200
|
|
465
|
+
assert rv.json == payload
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@pytest.mark.parametrize('validation', [True, False])
|
|
469
|
+
@pytest.mark.parametrize('payload', [{}, {'bar': 'qux'}])
|
|
470
|
+
def test_skip_validation_arg_name(app, client, validation, payload):
|
|
471
|
+
class FooIn(Schema):
|
|
472
|
+
bar = String(required=True)
|
|
473
|
+
|
|
474
|
+
@app.post('/foo')
|
|
475
|
+
@app.input(FooIn, arg_name='baz', validation=validation)
|
|
476
|
+
def post_foo(baz):
|
|
477
|
+
return baz
|
|
478
|
+
|
|
479
|
+
rv = client.post('/foo', json=payload)
|
|
480
|
+
if validation and not payload:
|
|
481
|
+
assert rv.status_code == 422
|
|
482
|
+
else:
|
|
483
|
+
assert rv.status_code == 200
|
|
484
|
+
assert rv.json == payload
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import openapi_spec_validator as osv
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
1
4
|
from .schemas import Foo
|
|
2
5
|
from .schemas import ResponseHeader
|
|
3
6
|
from apiflask.fields import Boolean
|
|
@@ -25,36 +28,26 @@ def test_spec_with_dict_headers(app, client):
|
|
|
25
28
|
rv = client.get('/openapi.json')
|
|
26
29
|
assert rv.json['paths']['/foo']['get']['responses']['200']['headers'] == {
|
|
27
30
|
'X-boolean': {
|
|
28
|
-
'name': 'X-boolean',
|
|
29
|
-
'in': 'header',
|
|
30
31
|
'description': 'A boolean header',
|
|
31
32
|
'required': False,
|
|
32
33
|
'schema': {'type': 'boolean'},
|
|
33
34
|
},
|
|
34
35
|
'X-integer': {
|
|
35
|
-
'name': 'X-integer',
|
|
36
|
-
'in': 'header',
|
|
37
36
|
'description': 'An integer header',
|
|
38
37
|
'required': False,
|
|
39
38
|
'schema': {'type': 'integer'},
|
|
40
39
|
},
|
|
41
40
|
'X-number': {
|
|
42
|
-
'name': 'X-number',
|
|
43
|
-
'in': 'header',
|
|
44
41
|
'description': 'A number header',
|
|
45
42
|
'required': False,
|
|
46
43
|
'schema': {'type': 'number'},
|
|
47
44
|
},
|
|
48
45
|
'X-string': {
|
|
49
|
-
'name': 'X-string',
|
|
50
|
-
'in': 'header',
|
|
51
46
|
'description': 'A string header',
|
|
52
47
|
'required': False,
|
|
53
48
|
'schema': {'type': 'string'},
|
|
54
49
|
},
|
|
55
50
|
'X-array': {
|
|
56
|
-
'name': 'X-array',
|
|
57
|
-
'in': 'header',
|
|
58
51
|
'description': 'An array header',
|
|
59
52
|
'required': False,
|
|
60
53
|
'schema': {'items': {'type': 'string'}, 'type': 'array'},
|
|
@@ -83,10 +76,22 @@ def test_spec_with_schema_headers(app, client):
|
|
|
83
76
|
rv = client.get('/openapi.json')
|
|
84
77
|
assert rv.json['paths']['/foo']['get']['responses']['200']['headers'] == {
|
|
85
78
|
'X-Token': {
|
|
86
|
-
'name': 'X-Token',
|
|
87
|
-
'in': 'header',
|
|
88
79
|
'description': 'A custom token header',
|
|
89
80
|
'required': True,
|
|
90
81
|
'schema': {'type': 'string'},
|
|
91
82
|
},
|
|
92
83
|
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.parametrize('openapi_version', ['3.0.0', '3.1.0'])
|
|
87
|
+
def test_spec_validity_with_headers(app, client, openapi_version):
|
|
88
|
+
app.config['OPENAPI_VERSION'] = openapi_version
|
|
89
|
+
|
|
90
|
+
@app.route('/foo')
|
|
91
|
+
@app.output(Foo, headers=ResponseHeader)
|
|
92
|
+
def foo():
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
rv = client.get('/openapi.json')
|
|
96
|
+
assert rv.status_code == 200
|
|
97
|
+
osv.validate(rv.json)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|