-
-
Notifications
You must be signed in to change notification settings - Fork 779
Fix for aiohttp and multipart/form-data uploads #1222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
b131e21
2af3c48
6ba7927
c277980
41e03cd
5b69e90
e214f45
1628d36
13488a5
c6c5009
d4da76c
b0bb367
41ebb71
cbaff89
83c8b4d
40a65f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -268,36 +268,79 @@ def body_definition(self): | |
| return {} | ||
|
|
||
| def _get_body_argument(self, body, arguments, has_kwargs, sanitize): | ||
| x_body_name = sanitize(self.body_schema.get('x-body-name', 'body')) | ||
| if len(arguments) <= 0 and not has_kwargs: | ||
| return {} | ||
|
|
||
| x_body_name_default = 'body' | ||
| x_body_name_explicit = True | ||
| x_body_name = sanitize(self.body_schema.get('x-body-name', '')) | ||
| if not x_body_name: | ||
| x_body_name_explicit = False | ||
| x_body_name = x_body_name_default | ||
|
|
||
| # if the body came in null, and the schema says it can be null, we decide | ||
| # to include no value for the body argument, rather than the default body | ||
| if is_nullable(self.body_schema) and is_null(body): | ||
| return {x_body_name: None} | ||
| if x_body_name in arguments or has_kwargs: | ||
| return {x_body_name: None} | ||
| return {} | ||
|
|
||
| # now determine the actual value for the body (whether it came in or is default) | ||
| default_body = self.body_schema.get('default', {}) | ||
| body_props = {k: {"schema": v} for k, v | ||
| body_props = {sanitize(k): {"schema": v} for k, v | ||
| in self.body_schema.get("properties", {}).items()} | ||
|
|
||
| # by OpenAPI specification `additionalProperties` defaults to `true` | ||
| # see: https://github.com/OAI/OpenAPI-Specification/blame/3.0.2/versions/3.0.2.md#L2305 | ||
| additional_props = self.body_schema.get("additionalProperties", True) | ||
|
|
||
| if body is None: | ||
| body = deepcopy(default_body) | ||
|
|
||
| # if the body isn't even an object, then none of the concerns below matter | ||
| if self.body_schema.get("type") != "object": | ||
| if x_body_name in arguments or has_kwargs: | ||
| return {x_body_name: body} | ||
| return {} | ||
|
|
||
| body_arg = deepcopy(default_body) | ||
| body_arg.update(body or {}) | ||
| # by OpenAPI specification `additionalProperties` defaults to `true` | ||
| # see: https://github.com/OAI/OpenAPI-Specification/blame/3.0.2/versions/3.0.2.md#L2305 | ||
| additional_props = self.body_schema.get("additionalProperties", True) | ||
|
|
||
| res = {} | ||
| if body_props or additional_props: | ||
| res = self._get_typed_body_values(body_arg, body_props, additional_props) | ||
| # supply the initial defaults and convert all values to the proper types by schema | ||
| x = deepcopy(default_body) | ||
| x.update(body or {}) | ||
| converted_body = self._get_typed_body_values(x, body_props, additional_props) | ||
|
|
||
| # NOTE: | ||
| # Handlers could have a single argument to receive the whole body or they | ||
| # could have individual arguments to receive all the body's parameters, or | ||
| # they may have a **kwargs, arguments to receive anything. So, the question | ||
| # arises that if kwargs is given, do we pass to the handler a single body | ||
| # argument, or the broken out arguments, or both? | ||
| # | ||
| # #1 If 'x-body-arg' is explicitly given and it exists in [arguments], then the | ||
| # body, as a whole, will be passed to the handler with that name. STOP. | ||
| # | ||
| # #2 If kwargs is given, then we don't know what the handler cares about, so we | ||
| # pass the body as a whole as an argument named, 'body', along with the | ||
| # individual body properties. STOP. | ||
| # | ||
| # #3 Else, we pass the body's individual properties which exist in [arguments]. | ||
|
||
| # | ||
| # #4 Finally, if that resulting argument list is empty, then we include an argument | ||
| # named 'body' to the handler, but only if 'body' exists in [arguments] | ||
|
|
||
| if x_body_name_explicit and x_body_name in arguments: #1 | ||
| return {x_body_name: converted_body} | ||
|
|
||
| if has_kwargs: #2 | ||
| converted_body[x_body_name_default] = copy(converted_body) # copy just to avoid circular ref | ||
| return converted_body | ||
|
|
||
| r = {k: converted_body[k] for k in converted_body if k in arguments} #3 | ||
|
|
||
| if len(r) <= 0 and x_body_name_default in arguments: #4 | ||
| r[x_body_name_default] = converted_body | ||
|
|
||
| return r | ||
|
|
||
| if x_body_name in arguments or has_kwargs: | ||
| return {x_body_name: res} | ||
| return {} | ||
|
|
||
| def _get_typed_body_values(self, body_arg, body_props, additional_props): | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import os | ||
|
|
||
| import aiohttp | ||
| import pytest | ||
| from pathlib import Path | ||
|
|
||
| from connexion import AioHttpApp | ||
|
|
||
| try: | ||
| import ujson as json | ||
| except ImportError: | ||
| import json | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def aiohttp_app(aiohttp_api_spec_dir): | ||
| app = AioHttpApp(__name__, port=5001, | ||
| specification_dir=aiohttp_api_spec_dir, | ||
| debug=True) | ||
| app.add_api( | ||
| 'openapi_multipart.yaml', | ||
| validate_responses=True, | ||
| strict_validation=True, | ||
| pythonic_params=True, | ||
| pass_context_arg_name='request_ctx', | ||
| ) | ||
| return app | ||
|
|
||
|
|
||
| async def test_single_file_upload(aiohttp_app, aiohttp_client): | ||
| app_client = await aiohttp_client(aiohttp_app.app) | ||
|
|
||
| resp = await app_client.post( | ||
| '/v1.0/upload_file', | ||
| data=aiohttp.FormData(fields=[('funky_funky', open(__file__, 'rb'))])(), | ||
| ) | ||
|
|
||
| data = await resp.json() | ||
| assert resp.status == 200 | ||
| assert data['fileName'] == f'{__name__}.py' | ||
ddurham2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| assert data['content'] == Path(__file__).read_bytes().decode('utf8') | ||
|
|
||
|
|
||
| async def test_many_files_upload(aiohttp_app, aiohttp_client): | ||
| app_client = await aiohttp_client(aiohttp_app.app) | ||
|
|
||
| dir_name = os.path.dirname(__file__) | ||
| files_field = [ | ||
| ('files', open(f'{dir_name}/{file_name}', 'rb')) \ | ||
| for file_name in sorted(os.listdir(dir_name)) if file_name.endswith('.py') | ||
| ] | ||
|
|
||
| form_data = aiohttp.FormData(fields=files_field) | ||
|
|
||
| resp = await app_client.post( | ||
| '/v1.0/upload_files', | ||
| data=form_data(), | ||
| ) | ||
|
|
||
| data = await resp.json() | ||
|
|
||
| assert resp.status == 200 | ||
| assert data['filesCount'] == len(files_field) | ||
ddurham2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| assert data['contents'] == [ | ||
| Path(f'{dir_name}/{file_name}').read_bytes().decode('utf8') \ | ||
| for file_name in sorted(os.listdir(dir_name)) if file_name.endswith('.py') | ||
| ] | ||
|
|
||
|
|
||
| async def test_mixed_multipart_single_file(aiohttp_app, aiohttp_client): | ||
| app_client = await aiohttp_client(aiohttp_app.app) | ||
|
|
||
| form_data = aiohttp.FormData() | ||
| form_data.add_field('dirName', os.path.dirname(__file__)) | ||
| form_data.add_field('funky_funky', open(__file__, 'rb')) | ||
|
|
||
| resp = await app_client.post( | ||
| '/v1.0/mixed_single_file', | ||
| data=form_data(), | ||
| ) | ||
|
|
||
| data = await resp.json() | ||
|
|
||
| assert resp.status == 200 | ||
| assert data['dirName'] == os.path.dirname(__file__) | ||
| assert data['fileName'] == f'{__name__}.py' | ||
| assert data['content'] == Path(__file__).read_bytes().decode('utf8') | ||
|
|
||
|
|
||
|
|
||
| async def test_mixed_multipart_many_files(aiohttp_app, aiohttp_client): | ||
| app_client = await aiohttp_client(aiohttp_app.app) | ||
|
|
||
| dir_name = os.path.dirname(__file__) | ||
| files_field = [ | ||
| ('files', open(f'{dir_name}/{file_name}', 'rb')) \ | ||
| for file_name in sorted(os.listdir(dir_name)) if file_name.endswith('.py') | ||
| ] | ||
|
|
||
| form_data = aiohttp.FormData(fields=files_field) | ||
| form_data.add_field('dirName', os.path.dirname(__file__)) | ||
| form_data.add_field('testCount', str(len(files_field))) | ||
|
|
||
| resp = await app_client.post( | ||
| '/v1.0/mixed_many_files', | ||
| data=form_data(), | ||
| ) | ||
|
|
||
| data = await resp.json() | ||
|
|
||
| assert resp.status == 200 | ||
| assert data['dirName'] == os.path.dirname(__file__) | ||
| assert data['testCount'] == len(files_field) | ||
| assert data['filesCount'] == len(files_field) | ||
| assert data['contents'] == [ | ||
| Path(f'{dir_name}/{file_name}').read_bytes().decode('utf8') \ | ||
| for file_name in sorted(os.listdir(dir_name)) if file_name.endswith('.py') | ||
| ] | ||
Uh oh!
There was an error while loading. Please reload this page.