From 16790be9d86dce9ba556da329ce58910fce03048 Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Fri, 12 Jan 2024 18:32:43 +0100 Subject: [PATCH 1/3] fix: support multiple files for multipart arrays --- .../templates/model.py.jinja | 73 ++++++++++++++----- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index f8864b343..3fe0db1cf 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -77,26 +77,26 @@ class {{ class_name }}: additional_properties: Dict[str, {{ additional_property_type }}] = _attrs_field(init=False, factory=dict) {% endif %} -{% macro _to_dict(multipart=False) %} -{% for property in model.required_properties + model.optional_properties %} +{% macro _transform_property(property, content, multipart=False) %} {% import "property_templates/" + property.template as prop_template %} -{% if prop_template.transform %} -{{ prop_template.transform(property, "self." + property.python_name, property.python_name, multipart=multipart) }} -{% elif multipart %} -{{ property.python_name }} = self.{{ property.python_name }} if isinstance(self.{{ property.python_name }}, Unset) else (None, str(self.{{ property.python_name }}).encode(), "text/plain") -{% else %} -{{ property.python_name }} = self.{{ property.python_name }} -{% endif %} +{%- if prop_template.transform -%} +{{ prop_template.transform(property, content, property.python_name, multipart=multipart) }} +{%- elif multipart -%} +{{ property.python_name }} = {{ content }} if isinstance({{ content }}, Unset) else (None, str({{ content }}).encode(), "text/plain") +{%- else -%} +{{ property.python_name }} = {{ content }} +{%- endif -%} -{% endfor %} +{%- endmacro -%} +{% macro _prepare_field_dict(multipart=False) %} field_dict: Dict[str, Any] = {} {% if model.additional_properties %} -{% if model.additional_properties.template %}{# Can be a bool instead of an object #} - {% import "property_templates/" + model.additional_properties.template as prop_template %} -{% else %} - {% set prop_template = None %} -{% endif %} +{%- if model.additional_properties.template -%}{# Can be a bool instead of an object #} + {%- import "property_templates/" + model.additional_properties.template as prop_template -%} +{%- else -%} + {%- set prop_template = None -%} +{%- endif -%} {% if prop_template and prop_template.transform %} for prop_name, prop in self.additional_properties.items(): {{ prop_template.transform(model.additional_properties, "prop", "field_dict[prop_name]", multipart=multipart, declare_type=false) | indent(4) }} @@ -107,8 +107,16 @@ field_dict.update({ }) {% else %} field_dict.update(self.additional_properties) -{% endif %} -{% endif %} +{%- endif -%} +{%- endif -%} +{% endmacro %} + +{% macro _to_dict() %} +{% for property in model.required_properties + model.optional_properties -%} +{{ _transform_property(property, "self." + property.python_name) }} +{% endfor %} + +{{ _prepare_field_dict() }} field_dict.update({ {% for property in model.required_properties + model.optional_properties %} {% if property.required %} @@ -133,8 +141,35 @@ return field_dict {{ _to_dict() | indent(8) }} {% if model.is_multipart_body %} - def to_multipart(self) -> Dict[str, Any]: - {{ _to_dict(multipart=True) | indent(8) }} + def to_multipart(self) -> List[Tuple[str, Any]]: + field_list: List[Tuple[str, Any]] = [] + {% for property in model.required_properties + model.optional_properties %} + {% if property.__class__.__name__ == 'ListProperty' %} + {% if not property.required %} + for cont in self.{{ property.python_name }} or []: + {% else %} + for cont in self.{{ property.python_name }}: + {% endif %} + {{ _transform_property(property.inner_property, "cont", True) | indent(12) }} + field_list.append(("{{ property.python_name }}", {{property.inner_property.python_name}})) + + {% else %} + {{ _transform_property(property, "self." + property.python_name, True) | indent(8) }} + {% if not property.required %} + if {{ property.python_name }} is not UNSET: + field_list.append(("{{ property.python_name }}", {{property.python_name}})) + {% else %} + field_list.append(("{{ property.python_name }}", {{property.python_name}})) + {% endif %} + {% endif %} + {% endfor %} + + {{ _prepare_field_dict(True) | indent(8) }} + + field_list += list(field_dict.items()) + + return field_list + {% endif %} @classmethod From fe841d7038ff645948e9c0b4e9ccf758a018b03e Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Sat, 13 Jan 2024 10:42:33 +0100 Subject: [PATCH 2/3] test: Add end2end test for multipart objects with arrays of files --- end_to_end_tests/baseline_openapi_3.0.json | 49 +++++ end_to_end_tests/baseline_openapi_3.1.yaml | 49 +++++ .../my_test_api_client/api/tests/__init__.py | 8 + ...ay_of_files_in_object_tests_upload_post.py | 172 ++++++++++++++++++ .../my_test_api_client/models/__init__.py | 2 + .../my_test_api_client/models/a_model.py | 1 + .../body_upload_file_tests_upload_post.py | 51 +++--- .../models/http_validation_error.py | 1 + ...odel_with_additional_properties_inlined.py | 1 + .../model_with_additional_properties_refed.py | 1 + .../models/model_with_any_json_properties.py | 1 + ...circular_ref_in_additional_properties_a.py | 1 + ...circular_ref_in_additional_properties_b.py | 1 + ...ive_additional_properties_a_date_holder.py | 1 + ..._recursive_ref_in_additional_properties.py | 1 + .../models/model_with_union_property.py | 1 + .../model_with_union_property_inlined.py | 1 + .../models/post_bodies_multiple_files_body.py | 15 +- .../models/test_inline_objects_body.py | 1 + .../test_inline_objects_response_200.py | 1 + ..._files_in_object_tests_upload_post_body.py | 56 ++++++ .../models/validation_error.py | 1 + .../models/post_body_multipart_body.py | 22 +-- .../templates/model.py.jinja | 20 +- 24 files changed, 406 insertions(+), 52 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index 6753bb2a4..797ed2dba 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -450,6 +450,55 @@ } } }, + "/tests/upload/multiple-files-in-object": { + "post": { + "tags": [ + "tests" + ], + "summary": "Array of files in object", + "description": "Upload an array of files as part of an object", + "operationId": "upload_array_of_files_in_object_tests_upload_post", + "parameters": [], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type" : "object", + "files" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "attachments content", + "format" : "binary" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/tests/json_body": { "post": { "tags": [ diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index b630ce674..24078afb1 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -446,6 +446,55 @@ info: } } }, + "/tests/upload/multiple-files-in-object": { + "post": { + "tags": [ + "tests" + ], + "summary": "Array of files in object", + "description": "Upload an array of files as part of an object", + "operationId": "upload_array_of_files_in_object_tests_upload_post", + "parameters": [ ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "files": { + "type": "array", + "items": { + "type": "string", + "description": "attachments content", + "format": "binary" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/tests/json_body": { "post": { "tags": [ diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py index 9af9c4626..aa58fe241 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py @@ -20,6 +20,7 @@ test_inline_objects, token_with_cookie_auth_token_with_cookie_get, unsupported_content_tests_unsupported_content_get, + upload_array_of_files_in_object_tests_upload_post, upload_file_tests_upload_post, upload_multiple_files_tests_upload_post, ) @@ -89,6 +90,13 @@ def upload_multiple_files_tests_upload_post(cls) -> types.ModuleType: """ return upload_multiple_files_tests_upload_post + @classmethod + def upload_array_of_files_in_object_tests_upload_post(cls) -> types.ModuleType: + """ + Upload an array of files as part of an object + """ + return upload_array_of_files_in_object_tests_upload_post + @classmethod def json_body_tests_json_body_post(cls) -> types.ModuleType: """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py new file mode 100644 index 000000000..93dea2d2f --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py @@ -0,0 +1,172 @@ +from http import HTTPStatus +from typing import Any, Dict, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.upload_array_of_files_in_object_tests_upload_post_body import ( + UploadArrayOfFilesInObjectTestsUploadPostBody, +) +from ...types import Response + + +def _get_kwargs( + *, + body: UploadArrayOfFilesInObjectTestsUploadPostBody, +) -> Dict[str, Any]: + headers: Dict[str, Any] = {} + + _kwargs: Dict[str, Any] = { + "method": "post", + "url": "/tests/upload/multiple-files-in-object", + } + + _body = body.to_multipart() + + _kwargs["files"] = _body + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Any, HTTPValidationError]]: + if response.status_code == HTTPStatus.OK: + response_200 = response.json() + return response_200 + if response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[Any, HTTPValidationError]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: Union[AuthenticatedClient, Client], + body: UploadArrayOfFilesInObjectTestsUploadPostBody, +) -> Response[Union[Any, HTTPValidationError]]: + """Array of files in object + + Upload an array of files as part of an object + + Args: + body (UploadArrayOfFilesInObjectTestsUploadPostBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Any, HTTPValidationError]] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: Union[AuthenticatedClient, Client], + body: UploadArrayOfFilesInObjectTestsUploadPostBody, +) -> Optional[Union[Any, HTTPValidationError]]: + """Array of files in object + + Upload an array of files as part of an object + + Args: + body (UploadArrayOfFilesInObjectTestsUploadPostBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Any, HTTPValidationError] + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: Union[AuthenticatedClient, Client], + body: UploadArrayOfFilesInObjectTestsUploadPostBody, +) -> Response[Union[Any, HTTPValidationError]]: + """Array of files in object + + Upload an array of files as part of an object + + Args: + body (UploadArrayOfFilesInObjectTestsUploadPostBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Any, HTTPValidationError]] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: Union[AuthenticatedClient, Client], + body: UploadArrayOfFilesInObjectTestsUploadPostBody, +) -> Optional[Union[Any, HTTPValidationError]]: + """Array of files in object + + Upload an array of files as part of an object + + Args: + body (UploadArrayOfFilesInObjectTestsUploadPostBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Any, HTTPValidationError] + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index 5166f321b..6f55e3361 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py @@ -76,6 +76,7 @@ ) from .test_inline_objects_body import TestInlineObjectsBody from .test_inline_objects_response_200 import TestInlineObjectsResponse200 +from .upload_array_of_files_in_object_tests_upload_post_body import UploadArrayOfFilesInObjectTestsUploadPostBody from .validation_error import ValidationError __all__ = ( @@ -145,5 +146,6 @@ "PostResponsesUnionsSimpleBeforeComplexResponse200AType1", "TestInlineObjectsBody", "TestInlineObjectsResponse200", + "UploadArrayOfFilesInObjectTestsUploadPostBody", "ValidationError", ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index d14160bf8..c967e4537 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -186,6 +186,7 @@ def to_dict(self) -> Dict[str, Any]: not_required_nullable_model = self.not_required_nullable_model field_dict: Dict[str, Any] = {} + field_dict.update( { "an_enum_value": an_enum_value, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py index 20d27d4a6..9d180bc46 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -113,6 +113,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() + field_dict.update( { "some_file": some_file, @@ -139,41 +140,55 @@ def to_dict(self) -> Dict[str, Any]: return field_dict - def to_multipart(self) -> Dict[str, Any]: + def to_multipart(self) -> List[Tuple[str, Any]]: + field_list: List[Tuple[str, Any]] = [] some_file = self.some_file.to_tuple() + field_list.append(("some_file", some_file)) some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json") + field_list.append(("some_object", some_object)) some_nullable_object: Union[None, Tuple[None, bytes, str]] if isinstance(self.some_nullable_object, BodyUploadFileTestsUploadPostSomeNullableObject): some_nullable_object = (None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json") else: some_nullable_object = self.some_nullable_object + field_list.append(("some_nullable_object", some_nullable_object)) some_optional_file: Union[Unset, FileJsonType] = UNSET if not isinstance(self.some_optional_file, Unset): some_optional_file = self.some_optional_file.to_tuple() + if some_optional_file is not UNSET: + field_list.append(("some_optional_file", some_optional_file)) some_string = ( self.some_string if isinstance(self.some_string, Unset) else (None, str(self.some_string).encode(), "text/plain") ) + if some_string is not UNSET: + field_list.append(("some_string", some_string)) a_datetime: Union[Unset, bytes] = UNSET if not isinstance(self.a_datetime, Unset): a_datetime = self.a_datetime.isoformat().encode() + if a_datetime is not UNSET: + field_list.append(("a_datetime", a_datetime)) a_date: Union[Unset, bytes] = UNSET if not isinstance(self.a_date, Unset): a_date = self.a_date.isoformat().encode() + if a_date is not UNSET: + field_list.append(("a_date", a_date)) some_number = ( self.some_number if isinstance(self.some_number, Unset) else (None, str(self.some_number).encode(), "text/plain") ) + if some_number is not UNSET: + field_list.append(("some_number", some_number)) some_array: Union[None, Tuple[None, bytes, str], Unset] if isinstance(self.some_array, Unset): some_array = UNSET @@ -187,42 +202,28 @@ def to_multipart(self) -> Dict[str, Any]: else: some_array = self.some_array + if some_array is not UNSET: + field_list.append(("some_array", some_array)) some_optional_object: Union[Unset, Tuple[None, bytes, str]] = UNSET if not isinstance(self.some_optional_object, Unset): some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()).encode(), "application/json") + if some_optional_object is not UNSET: + field_list.append(("some_optional_object", some_optional_object)) some_enum: Union[Unset, Tuple[None, bytes, str]] = UNSET if not isinstance(self.some_enum, Unset): some_enum = (None, str(self.some_enum.value).encode(), "text/plain") + if some_enum is not UNSET: + field_list.append(("some_enum", some_enum)) + field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = (None, json.dumps(prop.to_dict()).encode(), "application/json") - field_dict.update( - { - "some_file": some_file, - "some_object": some_object, - "some_nullable_object": some_nullable_object, - } - ) - if some_optional_file is not UNSET: - field_dict["some_optional_file"] = some_optional_file - if some_string is not UNSET: - field_dict["some_string"] = some_string - if a_datetime is not UNSET: - field_dict["a_datetime"] = a_datetime - if a_date is not UNSET: - field_dict["a_date"] = a_date - if some_number is not UNSET: - field_dict["some_number"] = some_number - if some_array is not UNSET: - field_dict["some_array"] = some_array - if some_optional_object is not UNSET: - field_dict["some_optional_object"] = some_optional_object - if some_enum is not UNSET: - field_dict["some_enum"] = some_enum - return field_dict + field_list += list(field_dict.items()) + + return field_list @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index 1f04c29d0..30e206653 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -29,6 +29,7 @@ def to_dict(self) -> Dict[str, Any]: detail.append(detail_item) field_dict: Dict[str, Any] = {} + field_dict.update({}) if detail is not UNSET: field_dict["detail"] = detail diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py index 761a43e54..048856548 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py @@ -32,6 +32,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() + field_dict.update({}) if a_number is not UNSET: field_dict["a_number"] = a_number diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py index 4605b8801..181829a2c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py @@ -18,6 +18,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.value + field_dict.update({}) return field_dict diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py index 49a66f7a5..a34c1bd7f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py @@ -34,6 +34,7 @@ def to_dict(self) -> Dict[str, Any]: else: field_dict[prop_name] = prop + field_dict.update({}) return field_dict diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py index 9e3f6c12c..4ab44178f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py @@ -22,6 +22,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() + field_dict.update({}) return field_dict diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py index 5b4f0c268..d324cbe2b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py @@ -22,6 +22,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() + field_dict.update({}) return field_dict diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py index 4693be0a1..20afb41d6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py @@ -18,6 +18,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.isoformat() + field_dict.update({}) return field_dict diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py index ea5b1211f..ebaca3f31 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py @@ -18,6 +18,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() + field_dict.update({}) return field_dict diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py index 890010b78..926bdb573 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py @@ -28,6 +28,7 @@ def to_dict(self) -> Dict[str, Any]: a_property = self.a_property.value field_dict: Dict[str, Any] = {} + field_dict.update({}) if a_property is not UNSET: field_dict["a_property"] = a_property diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py index 2a832e21a..6ac0f4a4c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py @@ -33,6 +33,7 @@ def to_dict(self) -> Dict[str, Any]: fruit = self.fruit.to_dict() field_dict: Dict[str, Any] = {} + field_dict.update({}) if fruit is not UNSET: field_dict["fruit"] = fruit diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py index 1c61d3385..c05d0a46b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Type, TypeVar, Union +from typing import Any, Dict, List, Tuple, Type, TypeVar, Union from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -29,18 +29,21 @@ def to_dict(self) -> Dict[str, Any]: return field_dict - def to_multipart(self) -> Dict[str, Any]: + def to_multipart(self) -> List[Tuple[str, Any]]: + field_list: List[Tuple[str, Any]] = [] a = self.a if isinstance(self.a, Unset) else (None, str(self.a).encode(), "text/plain") + if a is not UNSET: + field_list.append(("a", a)) + field_dict: Dict[str, Any] = {} field_dict.update( {key: (None, str(value).encode(), "text/plain") for key, value in self.additional_properties.items()} ) - field_dict.update({}) - if a is not UNSET: - field_dict["a"] = a - return field_dict + field_list += list(field_dict.items()) + + return field_list @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py index 8c1843b41..e3e956359 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py @@ -20,6 +20,7 @@ def to_dict(self) -> Dict[str, Any]: a_property = self.a_property field_dict: Dict[str, Any] = {} + field_dict.update({}) if a_property is not UNSET: field_dict["a_property"] = a_property diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py index 6a0ade77f..8d3c3195b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py @@ -20,6 +20,7 @@ def to_dict(self) -> Dict[str, Any]: a_property = self.a_property field_dict: Dict[str, Any] = {} + field_dict.update({}) if a_property is not UNSET: field_dict["a_property"] = a_property diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py new file mode 100644 index 000000000..27bb357cb --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py @@ -0,0 +1,56 @@ +from typing import Any, Dict, List, Tuple, Type, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UploadArrayOfFilesInObjectTestsUploadPostBody") + + +@_attrs_define +class UploadArrayOfFilesInObjectTestsUploadPostBody: + """ """ + + additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + + return field_dict + + def to_multipart(self) -> List[Tuple[str, Any]]: + field_list: List[Tuple[str, Any]] = [] + + field_dict: Dict[str, Any] = {} + field_dict.update( + {key: (None, str(value).encode(), "text/plain") for key, value in self.additional_properties.items()} + ) + + field_list += list(field_dict.items()) + + return field_list + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + upload_array_of_files_in_object_tests_upload_post_body = cls() + + upload_array_of_files_in_object_tests_upload_post_body.additional_properties = d + return upload_array_of_files_in_object_tests_upload_post_body + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index 6ff5d4790..33ec486f2 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py @@ -26,6 +26,7 @@ def to_dict(self) -> Dict[str, Any]: type = self.type field_dict: Dict[str, Any] = {} + field_dict.update( { "loc": loc, diff --git a/integration-tests/integration_tests/models/post_body_multipart_body.py b/integration-tests/integration_tests/models/post_body_multipart_body.py index de9992232..bcb45afc6 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -1,5 +1,5 @@ from io import BytesIO -from typing import Any, Dict, List, Type, TypeVar, Union +from typing import Any, Dict, List, Tuple, Type, TypeVar, Union from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -44,33 +44,33 @@ def to_dict(self) -> Dict[str, Any]: return field_dict - def to_multipart(self) -> Dict[str, Any]: + def to_multipart(self) -> List[Tuple[str, Any]]: + field_list: List[Tuple[str, Any]] = [] a_string = ( self.a_string if isinstance(self.a_string, Unset) else (None, str(self.a_string).encode(), "text/plain") ) + field_list.append(("a_string", a_string)) file = self.file.to_tuple() + field_list.append(("file", file)) description = ( self.description if isinstance(self.description, Unset) else (None, str(self.description).encode(), "text/plain") ) + if description is not UNSET: + field_list.append(("description", description)) + field_dict: Dict[str, Any] = {} field_dict.update( {key: (None, str(value).encode(), "text/plain") for key, value in self.additional_properties.items()} ) - field_dict.update( - { - "a_string": a_string, - "file": file, - } - ) - if description is not UNSET: - field_dict["description"] = description - return field_dict + field_list += list(field_dict.items()) + + return field_list @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 3fe0db1cf..7963ab0e7 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -79,24 +79,24 @@ class {{ class_name }}: {% macro _transform_property(property, content, multipart=False) %} {% import "property_templates/" + property.template as prop_template %} -{%- if prop_template.transform -%} +{% if prop_template.transform %} {{ prop_template.transform(property, content, property.python_name, multipart=multipart) }} -{%- elif multipart -%} +{% elif multipart %} {{ property.python_name }} = {{ content }} if isinstance({{ content }}, Unset) else (None, str({{ content }}).encode(), "text/plain") -{%- else -%} +{% else %} {{ property.python_name }} = {{ content }} -{%- endif -%} +{% endif %} -{%- endmacro -%} +{% endmacro %} {% macro _prepare_field_dict(multipart=False) %} field_dict: Dict[str, Any] = {} {% if model.additional_properties %} -{%- if model.additional_properties.template -%}{# Can be a bool instead of an object #} - {%- import "property_templates/" + model.additional_properties.template as prop_template -%} -{%- else -%} - {%- set prop_template = None -%} -{%- endif -%} +{% if model.additional_properties.template %}{# Can be a bool instead of an object #} + {% import "property_templates/" + model.additional_properties.template as prop_template %} +{% else %} + {% set prop_template = None %} +{% endif %} {% if prop_template and prop_template.transform %} for prop_name, prop in self.additional_properties.items(): {{ prop_template.transform(model.additional_properties, "prop", "field_dict[prop_name]", multipart=multipart, declare_type=false) | indent(4) }} From 9d8dffc33a59a125928c552728d2dadcf617f75c Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Thu, 22 Feb 2024 11:06:29 -0700 Subject: [PATCH 3/3] test: Fix schema of new e2e test --- end_to_end_tests/baseline_openapi_3.0.json | 100 +++++++++--------- end_to_end_tests/baseline_openapi_3.1.yaml | 100 +++++++++--------- .../my_test_api_client/api/tests/__init__.py | 14 +-- ...ay_of_files_in_object_tests_upload_post.py | 2 +- ..._files_in_object_tests_upload_post_body.py | 37 ++++++- 5 files changed, 144 insertions(+), 109 deletions(-) diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index 07fc5ae0a..199565c1e 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -87,6 +87,57 @@ } } }, + "/bodies/multipart/multiple-files-in-object": { + "post": { + "tags": [ + "tests" + ], + "summary": "Array of files in object", + "description": "Upload an array of files as part of an object", + "operationId": "upload_array_of_files_in_object_tests_upload_post", + "parameters": [], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type" : "object", + "properties" : { + "files": { + "type": "array", + "items": { + "type": "string", + "description": "attachments content", + "format": "binary" + } + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/tests/": { "get": { "tags": [ @@ -450,55 +501,6 @@ } } }, - "/tests/upload/multiple-files-in-object": { - "post": { - "tags": [ - "tests" - ], - "summary": "Array of files in object", - "description": "Upload an array of files as part of an object", - "operationId": "upload_array_of_files_in_object_tests_upload_post", - "parameters": [], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type" : "object", - "files" : { - "type" : "array", - "items" : { - "type" : "string", - "description" : "attachments content", - "format" : "binary" - } - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, "/tests/json_body": { "post": { "tags": [ diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index 43654d0a7..62d21c5e2 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -83,6 +83,57 @@ info: } } }, + "/bodies/multipart/multiple-files-in-object": { + "post": { + "tags": [ + "tests" + ], + "summary": "Array of files in object", + "description": "Upload an array of files as part of an object", + "operationId": "upload_array_of_files_in_object_tests_upload_post", + "parameters": [ ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "string", + "description": "attachments content", + "format": "binary" + } + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/tests/": { "get": { "tags": [ @@ -446,55 +497,6 @@ info: } } }, - "/tests/upload/multiple-files-in-object": { - "post": { - "tags": [ - "tests" - ], - "summary": "Array of files in object", - "description": "Upload an array of files as part of an object", - "operationId": "upload_array_of_files_in_object_tests_upload_post", - "parameters": [ ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "files": { - "type": "array", - "items": { - "type": "string", - "description": "attachments content", - "format": "binary" - } - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, "/tests/json_body": { "post": { "tags": [ diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py index aa58fe241..9bee64236 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py @@ -27,6 +27,13 @@ class TestsEndpoints: + @classmethod + def upload_array_of_files_in_object_tests_upload_post(cls) -> types.ModuleType: + """ + Upload an array of files as part of an object + """ + return upload_array_of_files_in_object_tests_upload_post + @classmethod def get_user_list(cls) -> types.ModuleType: """ @@ -90,13 +97,6 @@ def upload_multiple_files_tests_upload_post(cls) -> types.ModuleType: """ return upload_multiple_files_tests_upload_post - @classmethod - def upload_array_of_files_in_object_tests_upload_post(cls) -> types.ModuleType: - """ - Upload an array of files as part of an object - """ - return upload_array_of_files_in_object_tests_upload_post - @classmethod def json_body_tests_json_body_post(cls) -> types.ModuleType: """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py index 93dea2d2f..33af2d211 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py @@ -20,7 +20,7 @@ def _get_kwargs( _kwargs: Dict[str, Any] = { "method": "post", - "url": "/tests/upload/multiple-files-in-object", + "url": "/bodies/multipart/multiple-files-in-object", } _body = body.to_multipart() diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py index f27a4aa5b..11050491b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py @@ -1,25 +1,47 @@ -from typing import Any, Dict, List, Tuple, Type, TypeVar +from io import BytesIO +from typing import Any, Dict, List, Tuple, Type, TypeVar, Union from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..types import UNSET, File, FileJsonType, Unset + T = TypeVar("T", bound="UploadArrayOfFilesInObjectTestsUploadPostBody") @_attrs_define class UploadArrayOfFilesInObjectTestsUploadPostBody: - """ """ + """ + Attributes: + files (Union[Unset, List[File]]): + """ + files: Union[Unset, List[File]] = UNSET additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: + files: Union[Unset, List[FileJsonType]] = UNSET + if not isinstance(self.files, Unset): + files = [] + for files_item_data in self.files: + files_item = files_item_data.to_tuple() + + files.append(files_item) + field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) + field_dict.update({}) + if files is not UNSET: + field_dict["files"] = files return field_dict def to_multipart(self) -> List[Tuple[str, Any]]: field_list: List[Tuple[str, Any]] = [] + for cont in self.files or []: + files_item = cont.to_tuple() + + field_list.append(("files", files_item)) field_dict: Dict[str, Any] = {} field_dict.update( @@ -33,7 +55,16 @@ def to_multipart(self) -> List[Tuple[str, Any]]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: d = src_dict.copy() - upload_array_of_files_in_object_tests_upload_post_body = cls() + files = [] + _files = d.pop("files", UNSET) + for files_item_data in _files or []: + files_item = File(payload=BytesIO(files_item_data)) + + files.append(files_item) + + upload_array_of_files_in_object_tests_upload_post_body = cls( + files=files, + ) upload_array_of_files_in_object_tests_upload_post_body.additional_properties = d return upload_array_of_files_in_object_tests_upload_post_body