From 5e8ad7a0d425de0df7c00c9a864f00dd992856bb Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Tue, 13 May 2025 09:39:38 +0200 Subject: [PATCH 01/16] chore: Reimplement functionality based on the latest main --- 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 ++ .../my_test_api_client/models/__init__.py | 2 + .../my_test_api_client/models/a_model.py | 1 + .../body_upload_file_tests_upload_post.py | 76 ++++++++--------- .../models/http_validation_error.py | 1 + ...odel_with_additional_properties_inlined.py | 1 + .../models/model_with_union_property.py | 1 + .../model_with_union_property_inlined.py | 1 + .../models/post_bodies_multiple_files_body.py | 12 +-- .../models/test_inline_objects_body.py | 1 + .../test_inline_objects_response_200.py | 1 + .../models/validation_error.py | 1 + .../my_enum_api_client/models/a_model.py | 1 + .../models/post_user_list_body.py | 83 +++++++------------ .../models/post_body_multipart_body.py | 19 ++--- integration-tests/pyproject.toml | 14 +--- .../templates/model.py.jinja | 55 +++++++++--- .../union_property.py.jinja | 12 ++- 20 files changed, 260 insertions(+), 128 deletions(-) diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index eec3e39ac..40a91f628 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -476,6 +476,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 caddda8eb..ff68931bf 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -463,6 +463,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 1b91acc98..821489ab4 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/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index f354c31c7..cae3ca303 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 @@ -87,6 +87,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__ = ( @@ -167,5 +168,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 5a9ba85ae..db3c56629 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 @@ -206,6 +206,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 f5db27451..91fe12617 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 @@ -136,6 +136,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, @@ -167,13 +168,17 @@ 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_required_number = (None, str(self.some_required_number).encode(), "text/plain") + field_list.append(("some_required_number", some_required_number)) some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json") + field_list.append(("some_object", some_object)) some_nullable_object: tuple[None, bytes, str] if isinstance(self.some_nullable_object, BodyUploadFileTestsUploadPostSomeNullableObject): @@ -181,30 +186,41 @@ def to_multipart(self) -> dict[str, Any]: else: some_nullable_object = (None, str(self.some_nullable_object).encode(), "text/plain") + 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_nullable_number: Union[Unset, tuple[None, bytes, str]] if isinstance(self.some_nullable_number, Unset): @@ -214,14 +230,17 @@ def to_multipart(self) -> dict[str, Any]: else: some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") - some_int_array: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.some_int_array, Unset): - _temp_some_int_array = [] - for some_int_array_item_data in self.some_int_array: - some_int_array_item: Union[None, int] - some_int_array_item = some_int_array_item_data - _temp_some_int_array.append(some_int_array_item) - some_int_array = (None, json.dumps(_temp_some_int_array).encode(), "application/json") + if some_nullable_number is not UNSET: + field_list.append(("some_nullable_number", some_nullable_number)) + for some_int_array_element in self.some_int_array or []: + some_int_array_item: tuple[None, bytes, str] + + if isinstance(some_int_array_element, int): + some_int_array_item = (None, str(some_int_array_element).encode(), "text/plain") + else: + some_int_array_item = (None, str(some_int_array_element).encode(), "text/plain") + + field_list.append(("some_int_array", some_int_array_item)) some_array: Union[Unset, tuple[None, bytes, str]] @@ -236,47 +255,28 @@ def to_multipart(self) -> dict[str, Any]: else: some_array = (None, str(self.some_array).encode(), "text/plain") + 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_required_number": some_required_number, - "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_nullable_number is not UNSET: - field_dict["some_nullable_number"] = some_nullable_number - if some_int_array is not UNSET: - field_dict["some_int_array"] = some_int_array - 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: Mapping[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 cfe0a9a0c..43009994a 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 @@ -30,6 +30,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 bcdbf868c..bb70f94a8 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 @@ -33,6 +33,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_union_property.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py index d704baada..aa3019f97 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 @@ -29,6 +29,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 8011b7707..e2ebb7acd 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 @@ -34,6 +34,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 5f4aefccc..756eec0f1 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 @@ -30,18 +30,20 @@ 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] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = (None, str(prop).encode(), "text/plain") - field_dict.update({}) - if a is not UNSET: - field_dict["a"] = a + field_list += list(field_dict.items()) - return field_dict + return field_list @classmethod def from_dict(cls: type[T], src_dict: Mapping[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 e03ada2ef..66b4b3dcc 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 @@ -21,6 +21,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 02d7888ea..37c3005c2 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 @@ -21,6 +21,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/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index 3c8fc9d04..613e44d4e 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 @@ -27,6 +27,7 @@ def to_dict(self) -> dict[str, Any]: type_ = self.type_ field_dict: dict[str, Any] = {} + field_dict.update( { "loc": loc, diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py index 881286e74..5c3508cf5 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py @@ -52,6 +52,7 @@ def to_dict(self) -> dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) field_dict: dict[str, Any] = {} + field_dict.update( { "an_enum_value": an_enum_value, diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py index bfddb8c02..8b0788c5e 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py @@ -94,35 +94,27 @@ def to_dict(self) -> dict[str, Any]: return field_dict - def to_multipart(self) -> dict[str, Any]: - an_enum_value: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.an_enum_value, Unset): - _temp_an_enum_value = [] - for an_enum_value_item_data in self.an_enum_value: - an_enum_value_item: str = an_enum_value_item_data - _temp_an_enum_value.append(an_enum_value_item) - an_enum_value = (None, json.dumps(_temp_an_enum_value).encode(), "application/json") + def to_multipart(self) -> list[tuple[str, Any]]: + field_list: list[tuple[str, Any]] = [] + for an_enum_value_element in self.an_enum_value or []: + an_enum_value_item = (None, str(an_enum_value_element).encode(), "text/plain") - an_enum_value_with_null: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.an_enum_value_with_null, Unset): - _temp_an_enum_value_with_null = [] - for an_enum_value_with_null_item_data in self.an_enum_value_with_null: - an_enum_value_with_null_item: Union[None, str] - if isinstance(an_enum_value_with_null_item_data, str): - an_enum_value_with_null_item = an_enum_value_with_null_item_data - else: - an_enum_value_with_null_item = an_enum_value_with_null_item_data - _temp_an_enum_value_with_null.append(an_enum_value_with_null_item) - an_enum_value_with_null = (None, json.dumps(_temp_an_enum_value_with_null).encode(), "application/json") + field_list.append(("an_enum_value", an_enum_value_item)) - an_enum_value_with_only_null: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.an_enum_value_with_only_null, Unset): - _temp_an_enum_value_with_only_null = self.an_enum_value_with_only_null - an_enum_value_with_only_null = ( - None, - json.dumps(_temp_an_enum_value_with_only_null).encode(), - "application/json", - ) + for an_enum_value_with_null_element in self.an_enum_value_with_null or []: + an_enum_value_with_null_item: tuple[None, bytes, str] + + if an_enum_value_with_null_element is None: + an_enum_value_with_null_item = (None, str(an_enum_value_with_null_element).encode(), "text/plain") + else: + an_enum_value_with_null_item = (None, str(an_enum_value_with_null_element).encode(), "text/plain") + + field_list.append(("an_enum_value_with_null", an_enum_value_with_null_item)) + + for an_enum_value_with_only_null_element in self.an_enum_value_with_only_null or []: + an_enum_value_with_only_null_item = (None, str(an_enum_value_with_only_null_element).encode(), "text/plain") + + field_list.append(("an_enum_value_with_only_null", an_enum_value_with_only_null_item)) an_allof_enum_with_overridden_default: Union[Unset, tuple[None, bytes, str]] = UNSET if not isinstance(self.an_allof_enum_with_overridden_default, Unset): @@ -132,41 +124,30 @@ def to_multipart(self) -> dict[str, Any]: "text/plain", ) + if an_allof_enum_with_overridden_default is not UNSET: + field_list.append(("an_allof_enum_with_overridden_default", an_allof_enum_with_overridden_default)) an_optional_allof_enum: Union[Unset, tuple[None, bytes, str]] = UNSET if not isinstance(self.an_optional_allof_enum, Unset): an_optional_allof_enum = (None, str(self.an_optional_allof_enum).encode(), "text/plain") - nested_list_of_enums: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.nested_list_of_enums, Unset): - _temp_nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item: str = nested_list_of_enums_item_item_data - nested_list_of_enums_item.append(nested_list_of_enums_item_item) + if an_optional_allof_enum is not UNSET: + field_list.append(("an_optional_allof_enum", an_optional_allof_enum)) + for nested_list_of_enums_element in self.nested_list_of_enums or []: + _temp_nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_element: + nested_list_of_enums_item_item: str = nested_list_of_enums_item_item_data + _temp_nested_list_of_enums_item.append(nested_list_of_enums_item_item) + nested_list_of_enums_item = (None, json.dumps(_temp_nested_list_of_enums_item).encode(), "application/json") - _temp_nested_list_of_enums.append(nested_list_of_enums_item) - nested_list_of_enums = (None, json.dumps(_temp_nested_list_of_enums).encode(), "application/json") + field_list.append(("nested_list_of_enums", nested_list_of_enums_item)) field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = (None, str(prop).encode(), "text/plain") - field_dict.update({}) - if an_enum_value is not UNSET: - field_dict["an_enum_value"] = an_enum_value - if an_enum_value_with_null is not UNSET: - field_dict["an_enum_value_with_null"] = an_enum_value_with_null - if an_enum_value_with_only_null is not UNSET: - field_dict["an_enum_value_with_only_null"] = an_enum_value_with_only_null - if an_allof_enum_with_overridden_default is not UNSET: - field_dict["an_allof_enum_with_overridden_default"] = an_allof_enum_with_overridden_default - if an_optional_allof_enum is not UNSET: - field_dict["an_optional_allof_enum"] = an_optional_allof_enum - if nested_list_of_enums is not UNSET: - field_dict["nested_list_of_enums"] = nested_list_of_enums + field_list += list(field_dict.items()) - return field_dict + return field_list @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 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 f2100a10e..912137295 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -45,31 +45,30 @@ 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 = (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] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = (None, str(prop).encode(), "text/plain") - field_dict.update( - { - "a_string": a_string, - "file": file, - } - ) - if description is not UNSET: - field_dict["description"] = description + field_list += list(field_dict.items()) - return field_dict + return field_list @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 536a55b32..6052a3d70 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -14,22 +14,12 @@ dependencies = [ [tool.pdm] distribution = true -[tool.pdm.dev-dependencies] -dev = [ - "pytest", - "mypy", - "pytest-asyncio>=0.23.5", -] - [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" - + [tool.ruff] line-length = 120 [tool.ruff.lint] -select = ["F", "I"] - -[tool.mypy] -# Just to get mypy to _not_ look at the parent directory's config \ No newline at end of file +select = ["F", "I", "UP"] diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index eed2ea3a0..b2e082b6a 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -82,19 +82,19 @@ 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 multipart %} -{{ prop_template.transform_multipart(property, "self." + property.python_name, property.python_name) }} +{{ prop_template.transform_multipart(property, content, property.python_name) }} {% elif prop_template.transform %} -{{ prop_template.transform(property=property, source="self." + property.python_name, destination=property.python_name) }} +{{ prop_template.transform(property=property, source=content, destination=property.python_name) }} {% else %} -{{ property.python_name }} = self.{{ property.python_name }} +{{ property.python_name }} = {{ content }} {% endif %} -{% endfor %} +{% endmacro %} +{% macro _prepare_field_dict(multipart=False) %} field_dict: dict[str, Any] = {} {% if model.additional_properties %} {% import "property_templates/" + model.additional_properties.template as prop_template %} @@ -106,8 +106,16 @@ for prop_name, prop in self.additional_properties.items(): {{ prop_template.transform(model.additional_properties, "prop", "field_dict[prop_name]", declare_type=false) | indent(4) }} {% 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() }} {% if model.required_properties | length > 0 or model.optional_properties | length > 0 %} field_dict.update({ {% for property in model.required_properties + model.optional_properties %} @@ -134,8 +142,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 {{ property.python_name }}_element in self.{{ property.python_name }} or []: + {% else %} + for {{ property.python_name }}_element in self.{{ property.python_name }}: + {% endif %} + {{ _transform_property(property.inner_property, property.python_name + "_element", 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 diff --git a/openapi_python_client/templates/property_templates/union_property.py.jinja b/openapi_python_client/templates/property_templates/union_property.py.jinja index dbf7ee9dc..1881d3eae 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -75,6 +75,14 @@ else: {% endmacro %} +{% macro instance_check(inner_property, source) %} +{% if inner_property.get_instance_type_string() == "None" %} +if {{ source }} is None: +{% else %} +if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +{% endif %} +{% endmacro %} + {% macro transform_multipart(property, source, destination) %} {% set ns = namespace(has_if = false) %} {{ destination }}: {{ property.get_type_string(json=False, multipart=True) }} @@ -86,10 +94,10 @@ if isinstance({{ source }}, Unset): {% endif %} {% for inner_property in property.inner_properties %} {% if not ns.has_if %} -if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +{{ instance_check(inner_property, source) }} {% set ns.has_if = true %} {% elif not loop.last %} -elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +el{{ instance_check(inner_property, source) }} {% else %} else: {% endif %} From 5840d9118a27f1291cb3a91c07cb4d899cfacca5 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Wed, 14 May 2025 21:02:38 -0600 Subject: [PATCH 02/16] Revert change to integration test pyproject.toml --- integration-tests/pyproject.toml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 6052a3d70..536a55b32 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -14,12 +14,22 @@ dependencies = [ [tool.pdm] distribution = true +[tool.pdm.dev-dependencies] +dev = [ + "pytest", + "mypy", + "pytest-asyncio>=0.23.5", +] + [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" - + [tool.ruff] line-length = 120 [tool.ruff.lint] -select = ["F", "I", "UP"] +select = ["F", "I"] + +[tool.mypy] +# Just to get mypy to _not_ look at the parent directory's config \ No newline at end of file From 0eba399dda0bbb28fb29aac31e2d67b2e38872cd Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Wed, 14 May 2025 21:02:45 -0600 Subject: [PATCH 03/16] Refresh snapshots --- ...ay_of_files_in_object_tests_upload_post.py | 172 ++++++++++++++++++ ..._files_in_object_tests_upload_post_body.py | 55 ++++++ 2 files changed, 227 insertions(+) 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/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..4549d1929 --- /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, 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 == 200: + response_200 = response.json() + return response_200 + if response.status_code == 422: + 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/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..7901ab355 --- /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,55 @@ +from collections.abc import Mapping +from typing import Any, 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) + + return field_dict + + def to_multipart(self) -> list[tuple[str, Any]]: + field_list: list[tuple[str, Any]] = [] + + field_dict: dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = (None, str(prop).encode(), "text/plain") + + field_list += list(field_dict.items()) + + return field_list + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + 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 From d24b3e56110e8f2fd5930d408729956168325aca Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Wed, 14 May 2025 21:56:05 -0600 Subject: [PATCH 04/16] Improve grouping of each multipart field --- .../body_upload_file_tests_upload_post.py | 30 +++++++++---------- .../models/post_bodies_multiple_files_body.py | 1 + .../models/post_user_list_body.py | 6 ++-- .../models/post_body_multipart_body.py | 4 ++- .../templates/model.py.jinja | 12 ++++---- .../property_templates/file_property.py.jinja | 4 +-- .../float_property.py.jinja | 2 +- .../union_property.py.jinja | 5 ++-- 8 files changed, 35 insertions(+), 29 deletions(-) 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 91fe12617..4f697b4c4 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 @@ -170,29 +170,29 @@ def to_dict(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() + some_file = self.some_file.to_tuple() field_list.append(("some_file", some_file)) - some_required_number = (None, str(self.some_required_number).encode(), "text/plain") + some_required_number = (None, str(self.some_required_number).encode(), "text/plain") field_list.append(("some_required_number", some_required_number)) - some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json") + some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json") field_list.append(("some_object", some_object)) - some_nullable_object: tuple[None, bytes, str] + some_nullable_object: 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 = (None, str(self.some_nullable_object).encode(), "text/plain") - 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) @@ -201,18 +201,19 @@ def to_multipart(self) -> list[tuple[str, Any]]: 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) @@ -221,31 +222,30 @@ def to_multipart(self) -> list[tuple[str, Any]]: if some_number is not UNSET: field_list.append(("some_number", some_number)) - some_nullable_number: Union[Unset, tuple[None, bytes, str]] + some_nullable_number: Union[Unset, tuple[None, bytes, str]] if isinstance(self.some_nullable_number, Unset): some_nullable_number = UNSET + elif isinstance(self.some_nullable_number, float): some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") else: some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") - if some_nullable_number is not UNSET: field_list.append(("some_nullable_number", some_nullable_number)) + for some_int_array_element in self.some_int_array or []: some_int_array_item: tuple[None, bytes, str] - if isinstance(some_int_array_element, int): some_int_array_item = (None, str(some_int_array_element).encode(), "text/plain") else: some_int_array_item = (None, str(some_int_array_element).encode(), "text/plain") - field_list.append(("some_int_array", some_int_array_item)) some_array: Union[Unset, tuple[None, bytes, str]] - if isinstance(self.some_array, Unset): some_array = UNSET + elif isinstance(self.some_array, list): _temp_some_array = [] for some_array_type_0_item_data in self.some_array: @@ -254,15 +254,15 @@ def to_multipart(self) -> list[tuple[str, Any]]: some_array = (None, json.dumps(_temp_some_array).encode(), "application/json") else: some_array = (None, str(self.some_array).encode(), "text/plain") - 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") 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 756eec0f1..e89fcfc90 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 @@ -32,6 +32,7 @@ def to_dict(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: diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py index 8b0788c5e..38f8b5c98 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py @@ -96,19 +96,17 @@ def to_dict(self) -> dict[str, Any]: def to_multipart(self) -> list[tuple[str, Any]]: field_list: list[tuple[str, Any]] = [] + for an_enum_value_element in self.an_enum_value or []: an_enum_value_item = (None, str(an_enum_value_element).encode(), "text/plain") - field_list.append(("an_enum_value", an_enum_value_item)) for an_enum_value_with_null_element in self.an_enum_value_with_null or []: an_enum_value_with_null_item: tuple[None, bytes, str] - if an_enum_value_with_null_element is None: an_enum_value_with_null_item = (None, str(an_enum_value_with_null_element).encode(), "text/plain") else: an_enum_value_with_null_item = (None, str(an_enum_value_with_null_element).encode(), "text/plain") - field_list.append(("an_enum_value_with_null", an_enum_value_with_null_item)) for an_enum_value_with_only_null_element in self.an_enum_value_with_only_null or []: @@ -126,12 +124,14 @@ def to_multipart(self) -> list[tuple[str, Any]]: if an_allof_enum_with_overridden_default is not UNSET: field_list.append(("an_allof_enum_with_overridden_default", an_allof_enum_with_overridden_default)) + an_optional_allof_enum: Union[Unset, tuple[None, bytes, str]] = UNSET if not isinstance(self.an_optional_allof_enum, Unset): an_optional_allof_enum = (None, str(self.an_optional_allof_enum).encode(), "text/plain") if an_optional_allof_enum is not UNSET: field_list.append(("an_optional_allof_enum", an_optional_allof_enum)) + for nested_list_of_enums_element in self.nested_list_of_enums or []: _temp_nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_element: 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 912137295..f82e46777 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -47,12 +47,14 @@ def to_dict(self) -> dict[str, Any]: def to_multipart(self) -> list[tuple[str, Any]]: field_list: list[tuple[str, Any]] = [] + a_string = (None, str(self.a_string).encode(), "text/plain") field_list.append(("a_string", a_string)) - file = self.file.to_tuple() + file = self.file.to_tuple() field_list.append(("file", file)) + description = ( self.description if isinstance(self.description, Unset) diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index b2e082b6a..02342af03 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -84,14 +84,13 @@ class {{ class_name }}: {% macro _transform_property(property, content, multipart=False) %} {% import "property_templates/" + property.template as prop_template %} -{% if multipart %} +{%- if multipart -%} {{ prop_template.transform_multipart(property, content, property.python_name) }} -{% elif prop_template.transform %} +{%- elif prop_template.transform -%} {{ prop_template.transform(property=property, source=content, destination=property.python_name) }} -{% else %} +{%- else -%} {{ property.python_name }} = {{ content }} -{% endif %} - +{%- endif -%} {% endmacro %} {% macro _prepare_field_dict(multipart=False) %} @@ -113,6 +112,7 @@ field_dict.update(self.additional_properties) {% macro _to_dict() %} {% for property in model.required_properties + model.optional_properties -%} {{ _transform_property(property, "self." + property.python_name) }} + {% endfor %} {{ _prepare_field_dict() }} @@ -144,6 +144,7 @@ return field_dict {% if model.is_multipart_body %} 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 %} @@ -163,6 +164,7 @@ return field_dict field_list.append(("{{ property.python_name }}", {{property.python_name}})) {% endif %} {% endif %} + {% endfor %} {{ _prepare_field_dict(True) | indent(8) }} diff --git a/openapi_python_client/templates/property_templates/file_property.py.jinja b/openapi_python_client/templates/property_templates/file_property.py.jinja index 8d27ae617..c72adcda4 100644 --- a/openapi_python_client/templates/property_templates/file_property.py.jinja +++ b/openapi_python_client/templates/property_templates/file_property.py.jinja @@ -25,9 +25,9 @@ if not isinstance({{ source }}, Unset): {% macro transform_multipart(property, source, destination) %} {% if property.required %} {{ destination }} = {{ source }}.to_tuple() -{% else %} +{%- else %} {{ destination }}: {{ property.get_type_string(json=True) }} = UNSET if not isinstance({{ source }}, Unset): {{ destination }} = {{ source }}.to_tuple() -{% endif %} +{%- endif -%} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/float_property.py.jinja b/openapi_python_client/templates/property_templates/float_property.py.jinja index 12ffb0fb4..88b8404b2 100644 --- a/openapi_python_client/templates/property_templates/float_property.py.jinja +++ b/openapi_python_client/templates/property_templates/float_property.py.jinja @@ -7,5 +7,5 @@ str({{ source }}) {{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") {% else %} {{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{% endif %} +{%- endif -%} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.py.jinja b/openapi_python_client/templates/property_templates/union_property.py.jinja index 1881d3eae..b39e2d5cc 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -86,7 +86,6 @@ if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): {% macro transform_multipart(property, source, destination) %} {% set ns = namespace(has_if = false) %} {{ destination }}: {{ property.get_type_string(json=False, multipart=True) }} - {% if not property.required %} if isinstance({{ source }}, Unset): {{ destination }} = UNSET @@ -97,11 +96,13 @@ if isinstance({{ source }}, Unset): {{ instance_check(inner_property, source) }} {% set ns.has_if = true %} {% elif not loop.last %} + el{{ instance_check(inner_property, source) }} {% else %} + else: {% endif %} {% import "property_templates/" + inner_property.template as inner_template %} {{ inner_template.transform_multipart(inner_property, source, destination) | indent(4) | trim }} -{% endfor %} +{%- endfor -%} {% endmacro %} From 2f196acb57276228809b7ec056f858212a1d7529 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Fri, 16 May 2025 20:06:01 -0600 Subject: [PATCH 05/16] Remove faulty test case. Arrays of files are being tested in other endpoints, and this one was slightly malformed. --- 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 - ..._files_in_object_tests_upload_post_body.py | 55 ------ 6 files changed, 335 deletions(-) delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py delete 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 40a91f628..eec3e39ac 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -476,55 +476,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 ff68931bf..caddda8eb 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -463,55 +463,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 821489ab4..1b91acc98 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,7 +20,6 @@ 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, ) @@ -90,13 +89,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 deleted file mode 100644 index 4549d1929..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_array_of_files_in_object_tests_upload_post.py +++ /dev/null @@ -1,172 +0,0 @@ -from http import HTTPStatus -from typing import Any, 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 == 200: - response_200 = response.json() - return response_200 - if response.status_code == 422: - 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 cae3ca303..f354c31c7 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 @@ -87,7 +87,6 @@ ) 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__ = ( @@ -168,6 +167,5 @@ "PostResponsesUnionsSimpleBeforeComplexResponse200AType1", "TestInlineObjectsBody", "TestInlineObjectsResponse200", - "UploadArrayOfFilesInObjectTestsUploadPostBody", "ValidationError", ) 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 deleted file mode 100644 index 7901ab355..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/models/upload_array_of_files_in_object_tests_upload_post_body.py +++ /dev/null @@ -1,55 +0,0 @@ -from collections.abc import Mapping -from typing import Any, 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) - - return field_dict - - def to_multipart(self) -> list[tuple[str, Any]]: - field_list: list[tuple[str, Any]] = [] - - field_dict: dict[str, Any] = {} - for prop_name, prop in self.additional_properties.items(): - field_dict[prop_name] = (None, str(prop).encode(), "text/plain") - - field_list += list(field_dict.items()) - - return field_list - - @classmethod - def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - d = dict(src_dict) - 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 From 0f8c7fe80acecebc476b2569d5240550d414a333 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Fri, 16 May 2025 21:24:28 -0600 Subject: [PATCH 06/16] Simplify property to multipart code. --- .../body_upload_file_tests_upload_post.py | 137 +++++++----------- .../models/post_bodies_multiple_files_body.py | 11 +- .../models/post_user_list_body.py | 85 ++++++----- .../models/post_body_multipart_body.py | 22 +-- .../templates/model.py.jinja | 50 +++---- .../property_templates/any_property.py.jinja | 8 +- .../boolean_property.py.jinja | 8 +- .../const_property.py.jinja | 4 + .../property_templates/date_property.py.jinja | 11 +- .../datetime_property.py.jinja | 11 +- .../property_templates/enum_property.py.jinja | 11 +- .../property_templates/file_property.py.jinja | 10 +- .../float_property.py.jinja | 8 +- .../property_templates/int_property.py.jinja | 8 +- .../property_templates/list_property.py.jinja | 35 ++--- .../literal_enum_property.py.jinja | 11 +- .../model_property.py.jinja | 11 +- .../union_property.py.jinja | 10 +- .../property_templates/uuid_property.py.jinja | 11 +- 19 files changed, 168 insertions(+), 294 deletions(-) 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 4f697b4c4..5bc69e864 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 @@ -171,110 +171,83 @@ def to_dict(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)) + field_list.append(("some_file", self.some_file.to_tuple())) - some_required_number = (None, str(self.some_required_number).encode(), "text/plain") - field_list.append(("some_required_number", some_required_number)) + field_list.append(("some_required_number", (None, str(self.some_required_number).encode(), "text/plain"))) - some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json") - field_list.append(("some_object", some_object)) + field_list.append(("some_object", (None, json.dumps(self.some_object.to_dict()).encode(), "application/json"))) - some_nullable_object: 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") + field_list.append( + ( + "some_nullable_object", + (None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json"), + ) + ) else: - some_nullable_object = (None, str(self.some_nullable_object).encode(), "text/plain") - field_list.append(("some_nullable_object", some_nullable_object)) + field_list.append(("some_nullable_object", (None, str(self.some_nullable_object).encode(), "text/plain"))) - 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)) + field_list.append(("some_optional_file", self.some_optional_file.to_tuple())) - some_string = ( - self.some_string - if isinstance(self.some_string, Unset) - else (None, str(self.some_string).encode(), "text/plain") - ) + if not isinstance(self.some_string, Unset): + field_list.append(("some_string", (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)) + field_list.append(("a_datetime", self.a_datetime.isoformat().encode())) - 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)) + field_list.append(("a_date", self.a_date.isoformat().encode())) - 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_nullable_number: Union[Unset, tuple[None, bytes, str]] - if isinstance(self.some_nullable_number, Unset): - some_nullable_number = UNSET + if not isinstance(self.some_number, Unset): + field_list.append(("some_number", (None, str(self.some_number).encode(), "text/plain"))) - elif isinstance(self.some_nullable_number, float): - some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") - else: - some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") - if some_nullable_number is not UNSET: - field_list.append(("some_nullable_number", some_nullable_number)) - - for some_int_array_element in self.some_int_array or []: - some_int_array_item: tuple[None, bytes, str] - if isinstance(some_int_array_element, int): - some_int_array_item = (None, str(some_int_array_element).encode(), "text/plain") + if not isinstance(self.some_nullable_number, Unset): + if isinstance(self.some_nullable_number, float): + field_list.append( + ("some_nullable_number", (None, str(self.some_nullable_number).encode(), "text/plain")) + ) else: - some_int_array_item = (None, str(some_int_array_element).encode(), "text/plain") - field_list.append(("some_int_array", some_int_array_item)) - - some_array: Union[Unset, tuple[None, bytes, str]] - if isinstance(self.some_array, Unset): - some_array = UNSET + field_list.append( + ("some_nullable_number", (None, str(self.some_nullable_number).encode(), "text/plain")) + ) - elif isinstance(self.some_array, list): - _temp_some_array = [] - for some_array_type_0_item_data in self.some_array: - some_array_type_0_item = some_array_type_0_item_data.to_dict() - _temp_some_array.append(some_array_type_0_item) - some_array = (None, json.dumps(_temp_some_array).encode(), "application/json") - else: - some_array = (None, str(self.some_array).encode(), "text/plain") - if some_array is not UNSET: - field_list.append(("some_array", some_array)) + if not isinstance(self.some_int_array, Unset): + for some_int_array_item_element in self.some_int_array: + if isinstance(some_int_array_item_element, int): + field_list.append( + ("some_int_array", (None, str(some_int_array_item_element).encode(), "text/plain")) + ) + else: + field_list.append( + ("some_int_array", (None, str(some_int_array_item_element).encode(), "text/plain")) + ) + + if not isinstance(self.some_array, Unset): + if isinstance(self.some_array, list): + for some_array_type_0_item_element in self.some_array: + field_list.append( + ( + "some_array", + (None, json.dumps(some_array_type_0_item_element.to_dict()).encode(), "application/json"), + ) + ) + else: + field_list.append(("some_array", (None, str(self.some_array).encode(), "text/plain"))) - 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)) + field_list.append( + ( + "some_optional_object", + (None, json.dumps(self.some_optional_object.to_dict()).encode(), "application/json"), + ) + ) - 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") + field_list.append(("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_list += list(field_dict.items()) + field_list.append((prop_name, (None, json.dumps(prop.to_dict()).encode(), "application/json"))) return field_list 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 e89fcfc90..81707d241 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 @@ -33,16 +33,11 @@ def to_dict(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 not isinstance(self.a, Unset): + field_list.append(("a", (None, str(self.a).encode(), "text/plain"))) - if a is not UNSET: - field_list.append(("a", a)) - - field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): - field_dict[prop_name] = (None, str(prop).encode(), "text/plain") - - field_list += list(field_dict.items()) + field_list.append((prop_name, (None, str(prop).encode(), "text/plain"))) return field_list diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py index 38f8b5c98..e582bd869 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py @@ -1,4 +1,3 @@ -import json from collections.abc import Mapping from typing import Any, TypeVar, Union, cast @@ -97,55 +96,61 @@ def to_dict(self) -> dict[str, Any]: def to_multipart(self) -> list[tuple[str, Any]]: field_list: list[tuple[str, Any]] = [] - for an_enum_value_element in self.an_enum_value or []: - an_enum_value_item = (None, str(an_enum_value_element).encode(), "text/plain") - field_list.append(("an_enum_value", an_enum_value_item)) - - for an_enum_value_with_null_element in self.an_enum_value_with_null or []: - an_enum_value_with_null_item: tuple[None, bytes, str] - if an_enum_value_with_null_element is None: - an_enum_value_with_null_item = (None, str(an_enum_value_with_null_element).encode(), "text/plain") - else: - an_enum_value_with_null_item = (None, str(an_enum_value_with_null_element).encode(), "text/plain") - field_list.append(("an_enum_value_with_null", an_enum_value_with_null_item)) + if not isinstance(self.an_enum_value, Unset): + for an_enum_value_item_element in self.an_enum_value: + field_list.append(("an_enum_value", (None, str(an_enum_value_item_element).encode(), "text/plain"))) - for an_enum_value_with_only_null_element in self.an_enum_value_with_only_null or []: - an_enum_value_with_only_null_item = (None, str(an_enum_value_with_only_null_element).encode(), "text/plain") + if not isinstance(self.an_enum_value_with_null, Unset): + for an_enum_value_with_null_item_element in self.an_enum_value_with_null: + if an_enum_value_with_null_item_element is None: + field_list.append( + ( + "an_enum_value_with_null", + (None, str(an_enum_value_with_null_item_element).encode(), "text/plain"), + ) + ) + else: + field_list.append( + ( + "an_enum_value_with_null", + (None, str(an_enum_value_with_null_item_element).encode(), "text/plain"), + ) + ) - field_list.append(("an_enum_value_with_only_null", an_enum_value_with_only_null_item)) + if not isinstance(self.an_enum_value_with_only_null, Unset): + for an_enum_value_with_only_null_item_element in self.an_enum_value_with_only_null: + field_list.append( + ( + "an_enum_value_with_only_null", + (None, str(an_enum_value_with_only_null_item_element).encode(), "text/plain"), + ) + ) - an_allof_enum_with_overridden_default: Union[Unset, tuple[None, bytes, str]] = UNSET if not isinstance(self.an_allof_enum_with_overridden_default, Unset): - an_allof_enum_with_overridden_default = ( - None, - str(self.an_allof_enum_with_overridden_default).encode(), - "text/plain", + field_list.append( + ( + "an_allof_enum_with_overridden_default", + (None, str(self.an_allof_enum_with_overridden_default).encode(), "text/plain"), + ) ) - if an_allof_enum_with_overridden_default is not UNSET: - field_list.append(("an_allof_enum_with_overridden_default", an_allof_enum_with_overridden_default)) - - an_optional_allof_enum: Union[Unset, tuple[None, bytes, str]] = UNSET if not isinstance(self.an_optional_allof_enum, Unset): - an_optional_allof_enum = (None, str(self.an_optional_allof_enum).encode(), "text/plain") - - if an_optional_allof_enum is not UNSET: - field_list.append(("an_optional_allof_enum", an_optional_allof_enum)) - - for nested_list_of_enums_element in self.nested_list_of_enums or []: - _temp_nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_element: - nested_list_of_enums_item_item: str = nested_list_of_enums_item_item_data - _temp_nested_list_of_enums_item.append(nested_list_of_enums_item_item) - nested_list_of_enums_item = (None, json.dumps(_temp_nested_list_of_enums_item).encode(), "application/json") + field_list.append( + ("an_optional_allof_enum", (None, str(self.an_optional_allof_enum).encode(), "text/plain")) + ) - field_list.append(("nested_list_of_enums", nested_list_of_enums_item)) + if not isinstance(self.nested_list_of_enums, Unset): + for nested_list_of_enums_item_element in self.nested_list_of_enums: + for nested_list_of_enums_item_item_element in nested_list_of_enums_item_element: + field_list.append( + ( + "nested_list_of_enums", + (None, str(nested_list_of_enums_item_item_element).encode(), "text/plain"), + ) + ) - field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): - field_dict[prop_name] = (None, str(prop).encode(), "text/plain") - - field_list += list(field_dict.items()) + field_list.append((prop_name, (None, str(prop).encode(), "text/plain"))) return field_list 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 f82e46777..114f632f8 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -48,27 +48,15 @@ def to_dict(self) -> dict[str, Any]: def to_multipart(self) -> list[tuple[str, Any]]: field_list: list[tuple[str, Any]] = [] - a_string = (None, str(self.a_string).encode(), "text/plain") + field_list.append(("a_string", (None, str(self.a_string).encode(), "text/plain"))) - field_list.append(("a_string", a_string)) + field_list.append(("file", self.file.to_tuple())) - 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 not isinstance(self.description, Unset): + field_list.append(("description", (None, str(self.description).encode(), "text/plain"))) - if description is not UNSET: - field_list.append(("description", description)) - - field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): - field_dict[prop_name] = (None, str(prop).encode(), "text/plain") - - field_list += list(field_dict.items()) + field_list.append((prop_name, (None, str(prop).encode(), "text/plain"))) return field_list diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 02342af03..2f48654ec 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -82,25 +82,30 @@ class {{ class_name }}: additional_properties: dict[str, {{ additional_property_type }}] = _attrs_field(init=False, factory=dict) {% endif %} -{% macro _transform_property(property, content, multipart=False) %} +{% macro _transform_property(property, content) %} {% import "property_templates/" + property.template as prop_template %} -{%- if multipart -%} -{{ prop_template.transform_multipart(property, content, property.python_name) }} -{%- elif prop_template.transform -%} +{%- if prop_template.transform -%} {{ prop_template.transform(property=property, source=content, destination=property.python_name) }} {%- else -%} {{ property.python_name }} = {{ content }} {%- endif -%} {% endmacro %} -{% macro _prepare_field_dict(multipart=False) %} +{% macro multipart(property, source, destination) %} +{% import "property_templates/" + property.template as prop_template %} +{% if not property.required %} +if not isinstance({{source}}, Unset): + {{ prop_template.multipart(property, source, destination) | indent(4) }} +{% else %} +{{ prop_template.multipart(property, source, destination) }} +{% endif %} +{% endmacro %} + +{% macro _prepare_field_dict() %} field_dict: dict[str, Any] = {} {% if model.additional_properties %} {% import "property_templates/" + model.additional_properties.template as prop_template %} -{% if multipart %} -for prop_name, prop in self.additional_properties.items(): - {{ prop_template.transform_multipart(model.additional_properties, "prop", "field_dict[prop_name]") | indent(4) }} -{% elif prop_template.transform %} +{% if prop_template.transform %} for prop_name, prop in self.additional_properties.items(): {{ prop_template.transform(model.additional_properties, "prop", "field_dict[prop_name]", declare_type=false) | indent(4) }} {% else %} @@ -146,30 +151,15 @@ return field_dict 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 {{ property.python_name }}_element in self.{{ property.python_name }} or []: - {% else %} - for {{ property.python_name }}_element in self.{{ property.python_name }}: - {% endif %} - {{ _transform_property(property.inner_property, property.python_name + "_element", 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 %} + {% set destination = "\"" + property.name + "\"" %} + {{ multipart(property, "self." + property.python_name, destination) | indent(8) }} {% endfor %} - {{ _prepare_field_dict(True) | indent(8) }} - - field_list += list(field_dict.items()) + {% if model.additional_properties %} + for prop_name, prop in self.additional_properties.items(): + {{ multipart(model.additional_properties, "prop", "prop_name") | indent(4) }} + {% endif %} return field_list diff --git a/openapi_python_client/templates/property_templates/any_property.py.jinja b/openapi_python_client/templates/property_templates/any_property.py.jinja index 25a16516a..0eff2d50e 100644 --- a/openapi_python_client/templates/property_templates/any_property.py.jinja +++ b/openapi_python_client/templates/property_templates/any_property.py.jinja @@ -1,7 +1,3 @@ -{% macro transform_multipart(property, source, destination) %} -{% if not property.required %} -{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") -{% else %} -{{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{% endif %} +{% macro multipart(property, source, destination) %} +field_list.append(({{ destination }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} \ No newline at end of file diff --git a/openapi_python_client/templates/property_templates/boolean_property.py.jinja b/openapi_python_client/templates/property_templates/boolean_property.py.jinja index 52fda08e1..b9a837e9c 100644 --- a/openapi_python_client/templates/property_templates/boolean_property.py.jinja +++ b/openapi_python_client/templates/property_templates/boolean_property.py.jinja @@ -2,10 +2,6 @@ "true" if {{ source }} else "false" {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if not property.required %} -{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") -{% else %} -{{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{% endif %} +{% macro multipart(property, source, destination) %} +field_list.append(({{ destination }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/const_property.py.jinja b/openapi_python_client/templates/property_templates/const_property.py.jinja index fa635b5b9..8ad5bbff9 100644 --- a/openapi_python_client/templates/property_templates/const_property.py.jinja +++ b/openapi_python_client/templates/property_templates/const_property.py.jinja @@ -3,3 +3,7 @@ if {{ property.python_name }} != {{ property.value.python_code }}{% if not property.required %}and not isinstance({{ property.python_name }}, Unset){% endif %}: raise ValueError(f"{{ property.name }} must match const {{ property.value.python_code }}, got '{{'{' + property.python_name + '}' }}'") {%- endmacro %} + +{% macro multipart(property, source, destination) %} +field_list.append(({{ destination }}, source)) +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/date_property.py.jinja b/openapi_python_client/templates/property_templates/date_property.py.jinja index b5da665f7..bf88444fe 100644 --- a/openapi_python_client/templates/property_templates/date_property.py.jinja +++ b/openapi_python_client/templates/property_templates/date_property.py.jinja @@ -26,14 +26,7 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} +{% macro multipart(property, source, destination) %} {% set transformed = source + ".isoformat().encode()" %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} -{{ destination }}: {{ type_annotation }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{%- endif %} +field_list.append(({{ destination }}, {{ transformed }})) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/datetime_property.py.jinja b/openapi_python_client/templates/property_templates/datetime_property.py.jinja index 32e3f2ee6..29c38bc97 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.py.jinja +++ b/openapi_python_client/templates/property_templates/datetime_property.py.jinja @@ -26,14 +26,7 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} +{% macro multipart(property, source, destination) %} {% set transformed = source + ".isoformat().encode()" %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} -{{ destination }}: {{ type_annotation }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{%- endif %} +field_list.append(({{ destination }}, {{ transformed }})) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/enum_property.py.jinja b/openapi_python_client/templates/property_templates/enum_property.py.jinja index c46538eac..aac89aeac 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -22,16 +22,9 @@ if not isinstance({{ source }}, Unset): {% endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} +{% macro multipart(property, source, destination) %} {% set transformed = "(None, str(" + source + ".value" + ").encode(), \"text/plain\")" %} -{% set type_string = "Union[Unset, tuple[None, bytes, str]]" %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{% endif %} +field_list.append(({{ destination }}, {{ transformed }})) {% endmacro %} {% macro transform_header(source) %} diff --git a/openapi_python_client/templates/property_templates/file_property.py.jinja b/openapi_python_client/templates/property_templates/file_property.py.jinja index c72adcda4..275650db3 100644 --- a/openapi_python_client/templates/property_templates/file_property.py.jinja +++ b/openapi_python_client/templates/property_templates/file_property.py.jinja @@ -22,12 +22,6 @@ if not isinstance({{ source }}, Unset): {% endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if property.required %} -{{ destination }} = {{ source }}.to_tuple() -{%- else %} -{{ destination }}: {{ property.get_type_string(json=True) }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ source }}.to_tuple() -{%- endif -%} +{% macro multipart(property, source, destination) %} +field_list.append(({{ destination }}, {{ source }}.to_tuple())) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/float_property.py.jinja b/openapi_python_client/templates/property_templates/float_property.py.jinja index 88b8404b2..0729c82fb 100644 --- a/openapi_python_client/templates/property_templates/float_property.py.jinja +++ b/openapi_python_client/templates/property_templates/float_property.py.jinja @@ -2,10 +2,6 @@ str({{ source }}) {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if not property.required %} -{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") -{% else %} -{{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{%- endif -%} +{% macro multipart(property, source, destination) %} +field_list.append(({{ destination }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/int_property.py.jinja b/openapi_python_client/templates/property_templates/int_property.py.jinja index 12ffb0fb4..0729c82fb 100644 --- a/openapi_python_client/templates/property_templates/int_property.py.jinja +++ b/openapi_python_client/templates/property_templates/int_property.py.jinja @@ -2,10 +2,6 @@ str({{ source }}) {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if not property.required %} -{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") -{% else %} -{{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{% endif %} +{% macro multipart(property, source, destination) %} +field_list.append(({{ destination }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/list_property.py.jinja b/openapi_python_client/templates/property_templates/list_property.py.jinja index 94a9c6d65..fc68cb2ec 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -17,12 +17,8 @@ for {{ inner_source }} in (_{{ property.python_name }} or []): {% endif %} {% endmacro %} -{% macro _transform(property, source, destination, multipart, transform_method) %} +{% macro _transform(property, source, destination, transform_method) %} {% set inner_property = property.inner_property %} -{% if multipart %} -{% set multipart_destination = destination %} -{% set destination = "_temp_" + destination %} -{% endif %} {% import "property_templates/" + inner_property.template as inner_template %} {% if inner_template.transform %} {% set inner_source = inner_property.python_name + "_data" %} @@ -33,9 +29,6 @@ for {{ inner_source }} in {{ source }}: {% else %} {{ destination }} = {{ source }} {% endif %} -{% if multipart %} -{{ multipart_destination }} = (None, json.dumps({{ destination }}).encode(), 'application/json') -{% endif %} {% endmacro %} {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, list){% endmacro %} @@ -44,34 +37,30 @@ for {{ inner_source }} in {{ source }}: {% set inner_property = property.inner_property %} {% set type_string = property.get_type_string(json=True) %} {% if property.required %} -{{ _transform(property, source, destination, False, "to_dict") }} +{{ _transform(property, source, destination, "to_dict") }} {% else %} {{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): - {{ _transform(property, source, destination, False, "to_dict") | indent(4)}} + {{ _transform(property, source, destination, "to_dict") | indent(4)}} {% endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} +{% macro transform_multipart_body(property, source, destination) %} {% set inner_property = property.inner_property %} -{% set type_string = "Union[Unset, tuple[None, bytes, str]]" %} +{% set type_string = property.get_type_string(json=True) %} {% if property.required %} -{{ _transform(property, source, destination, True, "to_dict") }} +{{ _transform(property, source, destination, "to_multipart") }} {% else %} {{ destination }}: {{ type_string }} = UNSET if not isinstance({{ source }}, Unset): - {{ _transform(property, source, destination, True, "to_dict") | indent(4)}} + {{ _transform(property, source, destination, "to_multipart") | indent(4)}} {% endif %} {% endmacro %} -{% macro transform_multipart_body(property, source, destination) %} +{% macro multipart(property, source, destination) %} {% set inner_property = property.inner_property %} -{% set type_string = property.get_type_string(json=True) %} -{% if property.required %} -{{ _transform(property, source, destination, False, "to_multipart") }} -{% else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ _transform(property, source, destination, False, "to_multipart") | indent(4)}} -{% endif %} +{% import "property_templates/" + inner_property.template as inner_template %} +{% set inner_source = inner_property.python_name + "_element" %} +for {{ inner_source }} in {{ source }}: + {{ inner_template.multipart(inner_property, inner_source, destination) | indent(4) }} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja index 1506284d7..442d102ea 100644 --- a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja @@ -21,16 +21,9 @@ if not isinstance({{ source }}, Unset): {% endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} +{% macro multipart(property, source, destination) %} {% set transformed = "(None, str(" + source + ").encode(), \"text/plain\")" %} -{% set type_string = "Union[Unset, tuple[None, bytes, str]]" %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{% endif %} +field_list.append(({{ destination }}, {{ transformed }})) {% endmacro %} {% macro transform_header(source) %} diff --git a/openapi_python_client/templates/property_templates/model_property.py.jinja b/openapi_python_client/templates/property_templates/model_property.py.jinja index 308b7478b..fbc517b43 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -34,14 +34,7 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} +{% macro multipart(property, source, destination) %} {% set transformed = "(None, json.dumps(" + source + ".to_dict()" + ").encode(), 'application/json')" %} -{% set type_string = property.get_type_string(multipart=True) %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{%- endif %} +field_list.append(({{ destination }}, {{ transformed }})) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.py.jinja b/openapi_python_client/templates/property_templates/union_property.py.jinja index b39e2d5cc..5f76702f9 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -83,14 +83,8 @@ if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): {% endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} +{% macro multipart(property, source, destination) %} {% set ns = namespace(has_if = false) %} -{{ destination }}: {{ property.get_type_string(json=False, multipart=True) }} -{% if not property.required %} -if isinstance({{ source }}, Unset): - {{ destination }} = UNSET - {% set ns.has_if = true %} -{% endif %} {% for inner_property in property.inner_properties %} {% if not ns.has_if %} {{ instance_check(inner_property, source) }} @@ -103,6 +97,6 @@ el{{ instance_check(inner_property, source) }} else: {% endif %} {% import "property_templates/" + inner_property.template as inner_template %} - {{ inner_template.transform_multipart(inner_property, source, destination) | indent(4) | trim }} + {{ inner_template.multipart(inner_property, source, destination) | indent(4) | trim }} {%- endfor -%} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/uuid_property.py.jinja b/openapi_python_client/templates/property_templates/uuid_property.py.jinja index 1f91c1e3a..922cf0f89 100644 --- a/openapi_python_client/templates/property_templates/uuid_property.py.jinja +++ b/openapi_python_client/templates/property_templates/uuid_property.py.jinja @@ -26,13 +26,6 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if property.required %} -{{ destination }} = str({{ source }}) -{%- else %} -{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} -{{ destination }}: {{ type_annotation }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = str({{ source }}) -{%- endif %} +{% macro multipart(property, source, destination) %} +field_list.append(({{ destination }},str({{ source }}))) {% endmacro %} From 14bed922b31e4af2bd4ace6a3d12100a05a6f807 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Tue, 27 May 2025 13:16:03 -0600 Subject: [PATCH 07/16] Update integration tests to check multiple files --- end_to_end_tests/test_end_to_end.py | 2 +- .../integration_tests/models/__init__.py | 2 + .../models/post_body_multipart_body.py | 41 ++++--- .../post_body_multipart_response_200.py | 42 ++++--- ..._body_multipart_response_200_files_item.py | 77 ++++++++++++ integration-tests/pyproject.toml | 2 +- .../test_body/test_post_body_multipart.py | 112 ++++++------------ pyproject.toml | 2 +- 8 files changed, 163 insertions(+), 117 deletions(-) create mode 100644 integration-tests/integration_tests/models/post_body_multipart_response_200_files_item.py diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index 5502297ff..c1e14550a 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -266,7 +266,7 @@ def test_generate_dir_already_exists(): def test_update_integration_tests(): - url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json" + url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.yaml" source_path = Path(__file__).parent.parent / "integration-tests" temp_dir = Path.cwd() / "test_update_integration_tests" shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/integration-tests/integration_tests/models/__init__.py b/integration-tests/integration_tests/models/__init__.py index 275cf6faa..3243b59b0 100644 --- a/integration-tests/integration_tests/models/__init__.py +++ b/integration-tests/integration_tests/models/__init__.py @@ -2,6 +2,7 @@ from .post_body_multipart_body import PostBodyMultipartBody from .post_body_multipart_response_200 import PostBodyMultipartResponse200 +from .post_body_multipart_response_200_files_item import PostBodyMultipartResponse200FilesItem from .post_parameters_header_response_200 import PostParametersHeaderResponse200 from .problem import Problem from .public_error import PublicError @@ -9,6 +10,7 @@ __all__ = ( "PostBodyMultipartBody", "PostBodyMultipartResponse200", + "PostBodyMultipartResponse200FilesItem", "PostParametersHeaderResponse200", "Problem", "PublicError", 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 114f632f8..528d862f1 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -1,11 +1,11 @@ from collections.abc import Mapping from io import BytesIO -from typing import Any, TypeVar, Union +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field -from ..types import UNSET, File, Unset +from ..types import File T = TypeVar("T", bound="PostBodyMultipartBody") @@ -15,20 +15,23 @@ class PostBodyMultipartBody: """ Attributes: a_string (str): - file (File): For the sake of this test, include a file name and content type. The payload should also be valid - UTF-8. - description (Union[Unset, str]): + files (list[File]): + description (str): """ a_string: str - file: File - description: Union[Unset, str] = UNSET + files: list[File] + description: str additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: a_string = self.a_string - file = self.file.to_tuple() + files = [] + for files_item_data in self.files: + files_item = files_item_data.to_tuple() + + files.append(files_item) description = self.description @@ -37,11 +40,10 @@ def to_dict(self) -> dict[str, Any]: field_dict.update( { "a_string": a_string, - "file": file, + "files": files, + "description": description, } ) - if description is not UNSET: - field_dict["description"] = description return field_dict @@ -50,10 +52,10 @@ def to_multipart(self) -> list[tuple[str, Any]]: field_list.append(("a_string", (None, str(self.a_string).encode(), "text/plain"))) - field_list.append(("file", self.file.to_tuple())) + for files_item_element in self.files: + field_list.append(("files", files_item_element.to_tuple())) - if not isinstance(self.description, Unset): - field_list.append(("description", (None, str(self.description).encode(), "text/plain"))) + field_list.append(("description", (None, str(self.description).encode(), "text/plain"))) for prop_name, prop in self.additional_properties.items(): field_list.append((prop_name, (None, str(prop).encode(), "text/plain"))) @@ -65,13 +67,18 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) a_string = d.pop("a_string") - file = File(payload=BytesIO(d.pop("file"))) + files = [] + _files = d.pop("files") + for files_item_data in _files: + files_item = File(payload=BytesIO(files_item_data)) + + files.append(files_item) - description = d.pop("description", UNSET) + description = d.pop("description") post_body_multipart_body = cls( a_string=a_string, - file=file, + files=files, description=description, ) diff --git a/integration-tests/integration_tests/models/post_body_multipart_response_200.py b/integration-tests/integration_tests/models/post_body_multipart_response_200.py index 9b4862f0b..08c170dd1 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_response_200.py +++ b/integration-tests/integration_tests/models/post_body_multipart_response_200.py @@ -1,9 +1,13 @@ from collections.abc import Mapping -from typing import Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field +if TYPE_CHECKING: + from ..models.post_body_multipart_response_200_files_item import PostBodyMultipartResponse200FilesItem + + T = TypeVar("T", bound="PostBodyMultipartResponse200") @@ -12,39 +16,32 @@ class PostBodyMultipartResponse200: """ Attributes: a_string (str): Echo of the 'a_string' input parameter from the form. - file_data (str): Echo of content of the 'file' input parameter from the form. description (str): Echo of the 'description' input parameter from the form. - file_name (str): The name of the file uploaded. - file_content_type (str): The content type of the file uploaded. + files (list['PostBodyMultipartResponse200FilesItem']): """ a_string: str - file_data: str description: str - file_name: str - file_content_type: str + files: list["PostBodyMultipartResponse200FilesItem"] additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: a_string = self.a_string - file_data = self.file_data - description = self.description - file_name = self.file_name - - file_content_type = self.file_content_type + files = [] + for files_item_data in self.files: + files_item = files_item_data.to_dict() + files.append(files_item) field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { "a_string": a_string, - "file_data": file_data, "description": description, - "file_name": file_name, - "file_content_type": file_content_type, + "files": files, } ) @@ -52,23 +49,24 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.post_body_multipart_response_200_files_item import PostBodyMultipartResponse200FilesItem + d = dict(src_dict) a_string = d.pop("a_string") - file_data = d.pop("file_data") - description = d.pop("description") - file_name = d.pop("file_name") + files = [] + _files = d.pop("files") + for files_item_data in _files: + files_item = PostBodyMultipartResponse200FilesItem.from_dict(files_item_data) - file_content_type = d.pop("file_content_type") + files.append(files_item) post_body_multipart_response_200 = cls( a_string=a_string, - file_data=file_data, description=description, - file_name=file_name, - file_content_type=file_content_type, + files=files, ) post_body_multipart_response_200.additional_properties = d diff --git a/integration-tests/integration_tests/models/post_body_multipart_response_200_files_item.py b/integration-tests/integration_tests/models/post_body_multipart_response_200_files_item.py new file mode 100644 index 000000000..e30f77566 --- /dev/null +++ b/integration-tests/integration_tests/models/post_body_multipart_response_200_files_item.py @@ -0,0 +1,77 @@ +from collections.abc import Mapping +from typing import Any, TypeVar, Union + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PostBodyMultipartResponse200FilesItem") + + +@_attrs_define +class PostBodyMultipartResponse200FilesItem: + """ + Attributes: + data (Union[Unset, str]): Echo of content of the 'file' input parameter from the form. + name (Union[Unset, str]): The name of the file uploaded. + content_type (Union[Unset, str]): The content type of the file uploaded. + """ + + data: Union[Unset, str] = UNSET + name: Union[Unset, str] = UNSET + content_type: Union[Unset, str] = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + data = self.data + + name = self.name + + content_type = self.content_type + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if data is not UNSET: + field_dict["data"] = data + if name is not UNSET: + field_dict["name"] = name + if content_type is not UNSET: + field_dict["content_type"] = content_type + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + data = d.pop("data", UNSET) + + name = d.pop("name", UNSET) + + content_type = d.pop("content_type", UNSET) + + post_body_multipart_response_200_files_item = cls( + data=data, + name=name, + content_type=content_type, + ) + + post_body_multipart_response_200_files_item.additional_properties = d + return post_body_multipart_response_200_files_item + + @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/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 536a55b32..43f1884d7 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "integration-tests" -version = "0.0.1" +version = "0.1.0" description = "A client library for accessing OpenAPI Test Server" authors = [] readme = "README.md" diff --git a/integration-tests/tests/test_api/test_body/test_post_body_multipart.py b/integration-tests/tests/test_api/test_body/test_post_body_multipart.py index 54498ebc5..6934b01ee 100644 --- a/integration-tests/tests/test_api/test_body/test_post_body_multipart.py +++ b/integration-tests/tests/test_api/test_body/test_post_body_multipart.py @@ -9,23 +9,29 @@ from integration_tests.models.post_body_multipart_response_200 import PostBodyMultipartResponse200 from integration_tests.types import File +files = [ + File( + payload=BytesIO(b"some file content"), + file_name="cool_stuff.txt", + mime_type="application/openapi-python-client", + ), + File( + payload=BytesIO(b"more file content"), + file_name=None, + mime_type=None, + ), +] + def test(client: Client) -> None: a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" description = "super descriptive thing" response = post_body_multipart.sync_detailed( client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -35,17 +41,16 @@ def test(client: Client) -> None: raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") assert content.a_string == a_string - assert content.file_name == file_name - assert content.file_content_type == mime_type - assert content.file_data.encode() == payload assert content.description == description + for i, file in enumerate(content.files): + files[i].payload.seek(0) + assert file.data == files[i].payload.read().decode() + assert file.name == files[i].file_name + assert file.content_type == files[i].mime_type def test_custom_hooks() -> None: a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" description = "super descriptive thing" request_hook_called = False @@ -67,11 +72,7 @@ def log_response(*_: Any, **__: Any) -> None: client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -82,9 +83,6 @@ def log_response(*_: Any, **__: Any) -> None: def test_context_manager(client: Client) -> None: a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" description = "super descriptive thing" with client as client: @@ -92,11 +90,7 @@ def test_context_manager(client: Client) -> None: client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -104,11 +98,7 @@ def test_context_manager(client: Client) -> None: client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -118,11 +108,7 @@ def test_context_manager(client: Client) -> None: client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -131,30 +117,17 @@ def test_context_manager(client: Client) -> None: if not isinstance(content, PostBodyMultipartResponse200): raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") - assert content.a_string == a_string - assert content.file_name == file_name - assert content.file_content_type == mime_type - assert content.file_data.encode() == payload - assert content.description == description - @pytest.mark.asyncio async def test_async(client: Client) -> None: a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" description = "super descriptive thing" response = await post_body_multipart.asyncio_detailed( client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -164,18 +137,17 @@ async def test_async(client: Client) -> None: raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") assert content.a_string == a_string - assert content.file_name == file_name - assert content.file_content_type == mime_type - assert content.file_data.encode() == payload assert content.description == description + for i, file in enumerate(content.files): + files[i].payload.seek(0) + assert file.data == files[i].payload.read().decode() + assert file.name == files[i].file_name + assert file.content_type == files[i].mime_type @pytest.mark.asyncio async def test_async_context_manager(client: Client) -> None: a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" description = "super descriptive thing" async with client as client: @@ -183,11 +155,7 @@ async def test_async_context_manager(client: Client) -> None: client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -195,11 +163,7 @@ async def test_async_context_manager(client: Client) -> None: client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -209,11 +173,7 @@ async def test_async_context_manager(client: Client) -> None: client=client, body=PostBodyMultipartBody( a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), + files=files, description=description, ), ) @@ -223,7 +183,9 @@ async def test_async_context_manager(client: Client) -> None: raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") assert content.a_string == a_string - assert content.file_name == file_name - assert content.file_content_type == mime_type - assert content.file_data.encode() == payload assert content.description == description + for i, file in enumerate(content.files): + files[i].payload.seek(0) + assert file.data == files[i].payload.read().decode() + assert file.name == files[i].file_name + assert file.content_type == files[i].mime_type diff --git a/pyproject.toml b/pyproject.toml index ac668fd78..c2a704086 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,7 @@ composite = ["test --cov openapi_python_client tests --cov-report=term-missing"] [tool.pdm.scripts.regen_integration] shell = """ -openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json --config integration-tests/config.yaml --meta pdm --output-path integration-tests \ +openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.yaml --config integration-tests/config.yaml --meta pdm --output-path integration-tests \ """ [build-system] From 008432f161ae0a67f59ac41262f4df1efc3e3533 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 31 May 2025 12:33:22 -0600 Subject: [PATCH 08/16] Pin integration test server to 0.2.0 --- .github/workflows/checks.yml | 2 +- end_to_end_tests/test_end_to_end.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 87d45a5c3..0e7399401 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -158,7 +158,7 @@ jobs: - "" services: openapi-test-server: - image: ghcr.io/openapi-generators/openapi-test-server:0.0.1 + image: ghcr.io/openapi-generators/openapi-test-server:0.2.0 ports: - "3000:3000" steps: diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index c1e14550a..72f1df51a 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -266,7 +266,7 @@ def test_generate_dir_already_exists(): def test_update_integration_tests(): - url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.yaml" + url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/refs/tags/v0.2.0/openapi.yaml" source_path = Path(__file__).parent.parent / "integration-tests" temp_dir = Path.cwd() / "test_update_integration_tests" shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/pyproject.toml b/pyproject.toml index c2a704086..b320190d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,7 @@ composite = ["test --cov openapi_python_client tests --cov-report=term-missing"] [tool.pdm.scripts.regen_integration] shell = """ -openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.yaml --config integration-tests/config.yaml --meta pdm --output-path integration-tests \ +openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/refs/tags/v0.2.0/openapi.yaml --config integration-tests/config.yaml --meta pdm --output-path integration-tests \ """ [build-system] From 2767da5d8ac36f2aa379d1b71a2965a1702763f8 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 1 Jun 2025 10:06:15 -0600 Subject: [PATCH 09/16] Pin integration test server to 0.2.0 --- .../raise_minimum_httpx_version_to_023.md | 5 + .github/workflows/checks.yml | 27 +- .../pyproject.toml | 2 +- end_to_end_tests/golden-record/pyproject.toml | 2 +- .../pyproject.toml | 2 +- .../metadata_snapshots/pdm.pyproject.toml | 2 +- .../metadata_snapshots/poetry.pyproject.toml | 2 +- end_to_end_tests/metadata_snapshots/setup.py | 2 +- .../test-3-1-golden-record/pyproject.toml | 2 +- integration-tests/pdm.minimal.lock | 365 ++++++++++++++++++ integration-tests/pyproject.toml | 6 +- .../templates/pyproject.toml.jinja | 4 +- .../templates/setup.py.jinja | 2 +- pdm.lock | 2 +- pdm.minimal.lock | 284 +++++--------- pyproject.toml | 2 +- 16 files changed, 497 insertions(+), 214 deletions(-) create mode 100644 .changeset/raise_minimum_httpx_version_to_023.md create mode 100644 integration-tests/pdm.minimal.lock diff --git a/.changeset/raise_minimum_httpx_version_to_023.md b/.changeset/raise_minimum_httpx_version_to_023.md new file mode 100644 index 000000000..74ecc6366 --- /dev/null +++ b/.changeset/raise_minimum_httpx_version_to_023.md @@ -0,0 +1,5 @@ +--- +default: major +--- + +# Raise minimum httpx version to 0.23 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 0e7399401..72f850774 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -153,9 +153,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - httpx_version: - - "0.20.0" - - "" + lockfile: + - "pdm.lock" + - "pdm.minimal.lock" services: openapi-test-server: image: ghcr.io/openapi-generators/openapi-test-server:0.2.0 @@ -170,34 +170,17 @@ jobs: - name: Get Python Version id: get_python_version run: echo "python_version=$(python --version)" >> $GITHUB_OUTPUT - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: .venv - key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies-${{ hashFiles('**/pdm.lock') }} - restore-keys: | - ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies - - name: Install dependencies - run: | - pip install pdm - python -m venv .venv - pdm install - name: Cache Generated Client Dependencies uses: actions/cache@v4 with: path: integration-tests/.venv - key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies-${{ hashFiles('**/pdm.lock') }} + key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies-${{ hashFiles('integration-tests/pdm*.lock') }} restore-keys: | ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies - - name: Set httpx version - if: matrix.httpx_version != '' - run: | - cd integration-tests - pdm add httpx==${{ matrix.httpx_version }} - name: Install Integration Dependencies run: | cd integration-tests - pdm install + pdm install -L ${{ matrix.lockfile }} - name: Run Tests run: | cd integration-tests diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/pyproject.toml b/end_to_end_tests/docstrings-on-attributes-golden-record/pyproject.toml index feca06dbd..03e355862 100644 --- a/end_to_end_tests/docstrings-on-attributes-golden-record/pyproject.toml +++ b/end_to_end_tests/docstrings-on-attributes-golden-record/pyproject.toml @@ -12,7 +12,7 @@ include = ["CHANGELOG.md", "my_test_api_client/py.typed"] [tool.poetry.dependencies] python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +httpx = ">=0.23.0,<0.29.0" attrs = ">=22.2.0" python-dateutil = "^2.8.0" diff --git a/end_to_end_tests/golden-record/pyproject.toml b/end_to_end_tests/golden-record/pyproject.toml index feca06dbd..03e355862 100644 --- a/end_to_end_tests/golden-record/pyproject.toml +++ b/end_to_end_tests/golden-record/pyproject.toml @@ -12,7 +12,7 @@ include = ["CHANGELOG.md", "my_test_api_client/py.typed"] [tool.poetry.dependencies] python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +httpx = ">=0.23.0,<0.29.0" attrs = ">=22.2.0" python-dateutil = "^2.8.0" diff --git a/end_to_end_tests/literal-enums-golden-record/pyproject.toml b/end_to_end_tests/literal-enums-golden-record/pyproject.toml index 2c4d6b4e3..c63a19c42 100644 --- a/end_to_end_tests/literal-enums-golden-record/pyproject.toml +++ b/end_to_end_tests/literal-enums-golden-record/pyproject.toml @@ -12,7 +12,7 @@ include = ["CHANGELOG.md", "my_enum_api_client/py.typed"] [tool.poetry.dependencies] python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +httpx = ">=0.23.0,<0.29.0" attrs = ">=22.2.0" python-dateutil = "^2.8.0" diff --git a/end_to_end_tests/metadata_snapshots/pdm.pyproject.toml b/end_to_end_tests/metadata_snapshots/pdm.pyproject.toml index c1f8a2a2b..4f744fb09 100644 --- a/end_to_end_tests/metadata_snapshots/pdm.pyproject.toml +++ b/end_to_end_tests/metadata_snapshots/pdm.pyproject.toml @@ -6,7 +6,7 @@ authors = [] readme = "README.md" requires-python = ">=3.9,<4.0" dependencies = [ - "httpx>=0.20.0,<0.29.0", + "httpx>=0.23.0,<0.29.0", "attrs>=22.2.0", "python-dateutil>=2.8.0", ] diff --git a/end_to_end_tests/metadata_snapshots/poetry.pyproject.toml b/end_to_end_tests/metadata_snapshots/poetry.pyproject.toml index 2e8cd6c04..c4bc566ea 100644 --- a/end_to_end_tests/metadata_snapshots/poetry.pyproject.toml +++ b/end_to_end_tests/metadata_snapshots/poetry.pyproject.toml @@ -12,7 +12,7 @@ include = ["CHANGELOG.md", "test_3_1_features_client/py.typed"] [tool.poetry.dependencies] python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +httpx = ">=0.23.0,<0.29.0" attrs = ">=22.2.0" python-dateutil = "^2.8.0" diff --git a/end_to_end_tests/metadata_snapshots/setup.py b/end_to_end_tests/metadata_snapshots/setup.py index 6c7a58b97..312241bb8 100644 --- a/end_to_end_tests/metadata_snapshots/setup.py +++ b/end_to_end_tests/metadata_snapshots/setup.py @@ -13,6 +13,6 @@ long_description_content_type="text/markdown", packages=find_packages(), python_requires=">=3.9, <4", - install_requires=["httpx >= 0.20.0, < 0.29.0", "attrs >= 22.2.0", "python-dateutil >= 2.8.0, < 3"], + install_requires=["httpx >= 0.23.0, < 0.29.0", "attrs >= 22.2.0", "python-dateutil >= 2.8.0, < 3"], package_data={"test_3_1_features_client": ["py.typed"]}, ) diff --git a/end_to_end_tests/test-3-1-golden-record/pyproject.toml b/end_to_end_tests/test-3-1-golden-record/pyproject.toml index 2e8cd6c04..c4bc566ea 100644 --- a/end_to_end_tests/test-3-1-golden-record/pyproject.toml +++ b/end_to_end_tests/test-3-1-golden-record/pyproject.toml @@ -12,7 +12,7 @@ include = ["CHANGELOG.md", "test_3_1_features_client/py.typed"] [tool.poetry.dependencies] python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +httpx = ">=0.23.0,<0.29.0" attrs = ">=22.2.0" python-dateutil = "^2.8.0" diff --git a/integration-tests/pdm.minimal.lock b/integration-tests/pdm.minimal.lock new file mode 100644 index 000000000..9589c1de9 --- /dev/null +++ b/integration-tests/pdm.minimal.lock @@ -0,0 +1,365 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +strategy = ["direct_minimal_versions", "inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:6d5a710a30407796a6b9820e29cb340039a53899b8b595a6431d2940e4bd7d5f" + +[[metadata.targets]] +requires_python = "~=3.9" + +[[package]] +name = "anyio" +version = "3.7.1" +requires_python = ">=3.7" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +groups = ["default"] +dependencies = [ + "exceptiongroup; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, +] + +[[package]] +name = "attrs" +version = "22.2.0" +requires_python = ">=3.6" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default"] +files = [ + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["dev"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default", "dev"] +marker = "python_version < \"3.11\"" +dependencies = [ + "typing-extensions>=4.6.0; python_version < \"3.13\"", +] +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[[package]] +name = "h11" +version = "0.12.0" +requires_python = ">=3.6" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default"] +files = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] + +[[package]] +name = "httpcore" +version = "0.15.0" +requires_python = ">=3.7" +summary = "A minimal low-level HTTP client." +groups = ["default"] +dependencies = [ + "anyio==3.*", + "certifi", + "h11<0.13,>=0.11", + "sniffio==1.*", +] +files = [ + {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, + {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, +] + +[[package]] +name = "httpx" +version = "0.23.0" +requires_python = ">=3.7" +summary = "The next generation HTTP client." +groups = ["default"] +dependencies = [ + "certifi", + "httpcore<0.16.0,>=0.15.0", + "rfc3986[idna2008]<2,>=1.3", + "sniffio", +] +files = [ + {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, + {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, +] + +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +requires_python = ">=3.8" +summary = "brain-dead simple config-ini parsing" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "mypy" +version = "1.13.0" +requires_python = ">=3.8" +summary = "Optional static typing for Python" +groups = ["dev"] +dependencies = [ + "mypy-extensions>=1.0.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +requires_python = ">=3.8" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +requires_python = ">=3.9" +summary = "plugin and hook calling mechanisms for python" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[[package]] +name = "pytest" +version = "8.0.1" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["dev"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=1.3.0", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, + {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.23.5" +requires_python = ">=3.8" +summary = "Pytest support for asyncio" +groups = ["dev"] +dependencies = [ + "pytest<9,>=7.0.0", +] +files = [ + {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, + {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, +] + +[[package]] +name = "python-dateutil" +version = "2.8.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Extensions to the standard Python datetime module" +groups = ["default"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.8.0.tar.gz", hash = "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"}, + {file = "python_dateutil-2.8.0-py2.py3-none-any.whl", hash = "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb"}, +] + +[[package]] +name = "rfc3986" +version = "1.5.0" +summary = "Validating URI References per RFC 3986" +groups = ["default"] +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] + +[[package]] +name = "rfc3986" +version = "1.5.0" +extras = ["idna2008"] +summary = "Validating URI References per RFC 3986" +groups = ["default"] +dependencies = [ + "idna", + "rfc3986==1.5.0", +] +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] + +[[package]] +name = "six" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +requires_python = ">=3.8" +summary = "A lil' TOML parser" +groups = ["dev"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default", "dev"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 43f1884d7..a7252bd1e 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -6,7 +6,7 @@ authors = [] readme = "README.md" requires-python = ">=3.9,<4.0" dependencies = [ - "httpx>=0.20.0,<0.29.0", + "httpx>=0.23.0,<0.29.0", "attrs>=22.2.0", "python-dateutil>=2.8.0", ] @@ -16,8 +16,8 @@ distribution = true [tool.pdm.dev-dependencies] dev = [ - "pytest", - "mypy", + "pytest>8", + "mypy>=1.13", "pytest-asyncio>=0.23.5", ] diff --git a/openapi_python_client/templates/pyproject.toml.jinja b/openapi_python_client/templates/pyproject.toml.jinja index e9344b436..5d55805eb 100644 --- a/openapi_python_client/templates/pyproject.toml.jinja +++ b/openapi_python_client/templates/pyproject.toml.jinja @@ -19,7 +19,7 @@ include = ["CHANGELOG.md", "{{ package_name }}/py.typed"] {% if pdm %} dependencies = [ - "httpx>=0.20.0,<0.29.0", + "httpx>=0.23.0,<0.29.0", "attrs>=22.2.0", "python-dateutil>=2.8.0", ] @@ -31,7 +31,7 @@ distribution = true [tool.poetry.dependencies] python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +httpx = ">=0.23.0,<0.29.0" attrs = ">=22.2.0" python-dateutil = "^2.8.0" {% endif %} diff --git a/openapi_python_client/templates/setup.py.jinja b/openapi_python_client/templates/setup.py.jinja index c7c1a5a94..68a6dcf73 100644 --- a/openapi_python_client/templates/setup.py.jinja +++ b/openapi_python_client/templates/setup.py.jinja @@ -13,6 +13,6 @@ setup( long_description_content_type="text/markdown", packages=find_packages(), python_requires=">=3.9, <4", - install_requires=["httpx >= 0.20.0, < 0.29.0", "attrs >= 22.2.0", "python-dateutil >= 2.8.0, < 3"], + install_requires=["httpx >= 0.23.0, < 0.29.0", "attrs >= 22.2.0", "python-dateutil >= 2.8.0, < 3"], package_data={"{{ package_name }}": ["py.typed"]}, ) diff --git a/pdm.lock b/pdm.lock index 8711af61c..124aead39 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:dba408d9e5f1146846a3c2bdfdbb55df2679944d739a1975ef13beddc6ca8227" +content_hash = "sha256:af4a602e8e6cec54bdd45bf89526aa06cbfabd35864e2008a7d6c9d31f41e972" [[metadata.targets]] requires_python = "~=3.9" diff --git a/pdm.minimal.lock b/pdm.minimal.lock index 343630bbc..fd5c308d5 100644 --- a/pdm.minimal.lock +++ b/pdm.minimal.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["direct_minimal_versions", "inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:18a45e099de16a3f298e7c5bf873869d842e647755df674a9d2c53c4ce9b0d71" +content_hash = "sha256:21593bbb67f0857067bd89a7b4880a7c70c169053a5c81f466b388c9549a8959" [[metadata.targets]] requires_python = "~=3.9" @@ -54,89 +54,13 @@ files = [ [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.4.26" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." groups = ["default"] files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -requires_python = ">=3.7" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["default"] -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, ] [[package]] @@ -168,86 +92,93 @@ files = [ [[package]] name = "coverage" -version = "7.6.12" +version = "7.8.2" requires_python = ">=3.9" summary = "Code coverage measurement for Python" groups = ["dev"] files = [ - {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, - {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, - {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, - {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, - {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, - {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, - {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, - {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, - {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, - {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, - {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, - {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, - {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, - {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, - {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, - {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, - {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, + {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"}, + {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"}, + {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"}, + {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"}, + {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"}, + {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"}, + {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"}, + {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"}, + {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"}, + {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, + {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, + {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, + {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, + {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, + {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"}, + {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"}, + {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"}, + {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"}, + {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"}, + {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"}, + {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"}, + {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"}, + {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"}, + {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"}, + {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"}, + {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"}, + {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"}, + {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"}, + {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"}, + {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, + {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, ] [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.0" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" groups = ["default", "dev"] marker = "python_version < \"3.11\"" +dependencies = [ + "typing-extensions>=4.6.0; python_version < \"3.13\"", +] files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] [[package]] @@ -274,37 +205,36 @@ files = [ [[package]] name = "httpcore" -version = "0.13.7" -requires_python = ">=3.6" +version = "0.15.0" +requires_python = ">=3.7" summary = "A minimal low-level HTTP client." groups = ["default"] dependencies = [ "anyio==3.*", + "certifi", "h11<0.13,>=0.11", "sniffio==1.*", ] files = [ - {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, - {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, + {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, + {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, ] [[package]] name = "httpx" -version = "0.20.0" -requires_python = ">=3.6" +version = "0.23.0" +requires_python = ">=3.7" summary = "The next generation HTTP client." groups = ["default"] dependencies = [ - "async-generator; python_version < \"3.7\"", "certifi", - "charset-normalizer", - "httpcore<0.14.0,>=0.13.3", + "httpcore<0.16.0,>=0.15.0", "rfc3986[idna2008]<2,>=1.3", "sniffio", ] files = [ - {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"}, - {file = "httpx-0.20.0.tar.gz", hash = "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b"}, + {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, + {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, ] [[package]] @@ -320,13 +250,13 @@ files = [ [[package]] name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" +version = "2.1.0" +requires_python = ">=3.8" summary = "brain-dead simple config-ini parsing" groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -456,35 +386,35 @@ files = [ [[package]] name = "mypy-extensions" -version = "1.0.0" -requires_python = ">=3.5" +version = "1.1.0" +requires_python = ">=3.8" summary = "Type system extensions for programs checked with the mypy type checker." groups = ["dev"] files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] name = "packaging" -version = "24.2" +version = "25.0" requires_python = ">=3.8" summary = "Core utilities for Python packages" groups = ["dev"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" +version = "1.6.0" +requires_python = ">=3.9" summary = "plugin and hook calling mechanisms for python" groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [[package]] @@ -662,8 +592,8 @@ files = [ [[package]] name = "pytest-xdist" -version = "3.6.1" -requires_python = ">=3.8" +version = "3.7.0" +requires_python = ">=3.9" summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" groups = ["dev"] dependencies = [ @@ -671,8 +601,8 @@ dependencies = [ "pytest>=7.0.0", ] files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, + {file = "pytest_xdist-3.7.0-py3-none-any.whl", hash = "sha256:7d3fbd255998265052435eb9daa4e99b62e6fb9cfb6efd1f858d4d8c0c7f0ca0"}, + {file = "pytest_xdist-3.7.0.tar.gz", hash = "sha256:f9248c99a7c15b7d2f90715df93610353a485827bc06eefb6566d23f6400f126"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 7074b8286..c27a72347 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "pydantic>=2.10,<3.0.0", "attrs>=22.2.0", "python-dateutil>=2.8.1,<3.0.0", - "httpx>=0.20.0,<0.29.0", + "httpx>=0.23.0,<0.29.0", "ruamel.yaml>=0.18.6,<0.19.0", "ruff>=0.2,<0.12", "typing-extensions>=4.8.0,<5.0.0", From 1822f6d120d08cacdca17d6eb72be1b7cea0c3cb Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 1 Jun 2025 10:13:19 -0600 Subject: [PATCH 10/16] ci: Install pdm in integration tests --- .github/workflows/checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 72f850774..edc4b5580 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -180,6 +180,7 @@ jobs: - name: Install Integration Dependencies run: | cd integration-tests + pip install pdm pdm install -L ${{ matrix.lockfile }} - name: Run Tests run: | From e798afe313edbab89040491a1a4106c33a463926 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 2 Jun 2025 20:07:29 -0600 Subject: [PATCH 11/16] Clean up body generation code --- ...ity_to_set_an_array_as_a_multipart_body.md | 15 ++ end_to_end_tests/baseline_openapi_3.0.json | 45 ----- end_to_end_tests/baseline_openapi_3.1.yaml | 45 ----- .../my_test_api_client/api/tests/__init__.py | 8 - .../my_test_api_client/types.py | 18 +- .../api/bodies/json_like.py | 3 +- .../api/bodies/post_bodies_multiple.py | 12 +- .../my_test_api_client/api/bodies/refs.py | 3 +- .../api/config/content_type_override.py | 3 +- ...st_naming_property_conflict_with_import.py | 3 +- .../api/tests/callback_test.py | 3 +- .../tests/json_body_tests_json_body_post.py | 3 +- .../octet_stream_tests_octet_stream_post.py | 3 +- .../api/tests/post_form_data.py | 3 +- .../api/tests/post_form_data_inline.py | 3 +- .../api/tests/post_tests_json_body_string.py | 3 +- .../api/tests/test_inline_objects.py | 3 +- .../tests/upload_file_tests_upload_post.py | 4 +- ...upload_multiple_files_tests_upload_post.py | 173 ------------------ .../body_upload_file_tests_upload_post.py | 57 +++--- .../models/post_bodies_multiple_files_body.py | 11 +- .../golden-record/my_test_api_client/types.py | 18 +- .../api/tests/post_user_list.py | 4 +- .../models/post_user_list_body.py | 25 ++- .../my_enum_api_client/types.py | 18 +- .../api/const/post_const_path.py | 3 +- .../api/prefix_items/post_prefix_items.py | 3 +- .../test_3_1_features_client/types.py | 18 +- .../api/body/post_body_multipart.py | 4 +- .../models/post_body_multipart_body.py | 15 +- integration-tests/integration_tests/types.py | 18 +- integration-tests/pdm.lock | 2 +- integration-tests/pyproject.toml | 2 +- .../parser/properties/file.py | 4 +- .../templates/endpoint_macros.py.jinja | 20 +- .../templates/endpoint_module.py.jinja | 7 +- .../templates/model.py.jinja | 9 +- .../property_templates/any_property.py.jinja | 4 +- .../boolean_property.py.jinja | 4 +- .../const_property.py.jinja | 4 +- .../property_templates/date_property.py.jinja | 5 +- .../datetime_property.py.jinja | 5 +- .../property_templates/enum_property.py.jinja | 5 +- .../property_templates/file_property.py.jinja | 4 +- .../float_property.py.jinja | 4 +- .../property_templates/int_property.py.jinja | 4 +- .../property_templates/list_property.py.jinja | 12 -- .../literal_enum_property.py.jinja | 5 +- .../model_property.py.jinja | 13 +- .../property_templates/uuid_property.py.jinja | 4 +- .../templates/types.py.jinja | 18 +- .../test_parser/test_properties/test_init.py | 2 +- 52 files changed, 212 insertions(+), 472 deletions(-) create mode 100644 .changeset/removed_ability_to_set_an_array_as_a_multipart_body.md delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_multiple_files_tests_upload_post.py diff --git a/.changeset/removed_ability_to_set_an_array_as_a_multipart_body.md b/.changeset/removed_ability_to_set_an_array_as_a_multipart_body.md new file mode 100644 index 000000000..1d12888ee --- /dev/null +++ b/.changeset/removed_ability_to_set_an_array_as_a_multipart_body.md @@ -0,0 +1,15 @@ +--- +default: major +--- + +# Removed ability to set an array as a multipart body + +Previously, when defining a request's body as `multipart/form-data`, the generator would attempt to generate code +for both `object` schemas and `array` schemas. However, most arrays could not generate valid multipart bodies, as +there would be no field names (required to set the `Content-Disposition` headers). + +The code to generate any body for `multipart/form-data` where the schema is `array` has been removed, and any such +bodies will be skipped. This is not _expected_ to be a breaking change in practice, since the code generated would +probably never work. + +If you have a use-case for `multipart/form-data` with an `array` schema, please [open a new discussion](https://github.com/openapi-generators/openapi-python-client/discussions) with an example schema and the desired functional Python code. diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index eec3e39ac..b07f3cc7b 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -431,51 +431,6 @@ } } }, - "/tests/upload/multiple": { - "post": { - "tags": [ - "tests" - ], - "summary": "Upload multiple files", - "description": "Upload several files in the same request", - "operationId": "upload_multiple_files_tests_upload_post", - "parameters": [], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "array", - "items": { - "type": "string", - "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 caddda8eb..5364e34ad 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -418,51 +418,6 @@ info: } } }, - "/tests/upload/multiple": { - "post": { - "tags": [ - "tests" - ], - "summary": "Upload multiple files", - "description": "Upload several files in the same request", - "operationId": "upload_multiple_files_tests_upload_post", - "parameters": [ ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "array", - "items": { - "type": "string", - "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 1b91acc98..d7ef5cd7c 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 @@ -21,7 +21,6 @@ token_with_cookie_auth_token_with_cookie_get, unsupported_content_tests_unsupported_content_get, upload_file_tests_upload_post, - upload_multiple_files_tests_upload_post, ) @@ -82,13 +81,6 @@ def upload_file_tests_upload_post(cls) -> types.ModuleType: """ return upload_file_tests_upload_post - @classmethod - def upload_multiple_files_tests_upload_post(cls) -> types.ModuleType: - """ - Upload several files in the same request - """ - return upload_multiple_files_tests_upload_post - @classmethod def json_body_tests_json_body_post(cls) -> types.ModuleType: """ diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/types.py b/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/types.py index b9ed58b8a..1b96ca408 100644 --- a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/types.py @@ -1,8 +1,8 @@ """Contains some shared types for properties""" -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar +from typing import IO, BinaryIO, Generic, Literal, Optional, TypeVar, Union from attrs import define @@ -14,7 +14,15 @@ def __bool__(self) -> Literal[False]: UNSET: Unset = Unset() -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # (filename, file (or bytes), content_type) + tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = list[tuple[str, FileTypes]] @define @@ -25,7 +33,7 @@ class File: file_name: Optional[str] = None mime_type: Optional[str] = None - def to_tuple(self) -> FileJsonType: + def to_tuple(self) -> FileTypes: """Return a tuple representation that httpx will accept for multipart/form-data""" return self.file_name, self.payload, self.mime_type @@ -43,4 +51,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py index 626bacfa6..e49c19427 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py @@ -20,9 +20,8 @@ def _get_kwargs( "url": "/bodies/json-like", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/vnd+json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py index 84ec8eee0..652e2c6db 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py @@ -28,24 +28,20 @@ def _get_kwargs( } if isinstance(body, PostBodiesMultipleJsonBody): - _json_body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _json_body headers["Content-Type"] = "application/json" if isinstance(body, File): - _content_body = body.payload + _kwargs["content"] = body.payload - _kwargs["content"] = _content_body headers["Content-Type"] = "application/octet-stream" if isinstance(body, PostBodiesMultipleDataBody): - _data_body = body.to_dict() + _kwargs["data"] = body.to_dict() - _kwargs["data"] = _data_body headers["Content-Type"] = "application/x-www-form-urlencoded" if isinstance(body, PostBodiesMultipleFilesBody): - _files_body = body.to_multipart() + _kwargs["files"] = body.to_multipart() - _kwargs["files"] = _files_body headers["Content-Type"] = "multipart/form-data" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py index a79cf178a..81812cdea 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py @@ -20,9 +20,8 @@ def _get_kwargs( "url": "/bodies/refs", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py b/end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py index 2bd74aac4..d2757f759 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py @@ -19,9 +19,8 @@ def _get_kwargs( "url": "/config/content-type-override", } - _body = body + _kwargs["json"] = body - _kwargs["json"] = _body headers["Content-Type"] = "openapi/python/client" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py b/end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py index 3bb8b698b..bf1ebf6ca 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py @@ -23,9 +23,8 @@ def _get_kwargs( "url": "/naming/property-conflict-with-import", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py index 0852815d2..dbda22bc3 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py @@ -21,9 +21,8 @@ def _get_kwargs( "url": "/tests/callback", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py index f33a23dc7..f256727c2 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -21,9 +21,8 @@ def _get_kwargs( "url": "/tests/json_body", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py index ea0cbd65a..f4e58f35a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py @@ -20,9 +20,8 @@ def _get_kwargs( "url": "/tests/octet_stream", } - _body = body.payload + _kwargs["content"] = body.payload - _kwargs["content"] = _body headers["Content-Type"] = "application/octet-stream" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py index ec65d0363..41610afc0 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py @@ -20,9 +20,8 @@ def _get_kwargs( "url": "/tests/post_form_data", } - _body = body.to_dict() + _kwargs["data"] = body.to_dict() - _kwargs["data"] = _body headers["Content-Type"] = "application/x-www-form-urlencoded" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py index bc5ad7cc4..9bb3cd7c0 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py @@ -20,9 +20,8 @@ def _get_kwargs( "url": "/tests/post_form_data_inline", } - _body = body.to_dict() + _kwargs["data"] = body.to_dict() - _kwargs["data"] = _body headers["Content-Type"] = "application/x-www-form-urlencoded" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py index ba40de26f..4f879eed8 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py @@ -20,9 +20,8 @@ def _get_kwargs( "url": "/tests/json_body/string", } - _body = body + _kwargs["json"] = body - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py index 287ea4a1a..74eb8ae5c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py @@ -21,9 +21,8 @@ def _get_kwargs( "url": "/tests/inline_objects", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 9f1864ec3..ad372b91f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -21,9 +21,7 @@ def _get_kwargs( "url": "/tests/upload", } - _body = body.to_multipart() - - _kwargs["files"] = _body + _kwargs["files"] = body.to_multipart() _kwargs["headers"] = headers return _kwargs diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_multiple_files_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_multiple_files_tests_upload_post.py deleted file mode 100644 index 3f8edc817..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_multiple_files_tests_upload_post.py +++ /dev/null @@ -1,173 +0,0 @@ -from http import HTTPStatus -from typing import Any, Optional, Union - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.http_validation_error import HTTPValidationError -from ...types import File, Response - - -def _get_kwargs( - *, - body: list[File], -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/tests/upload/multiple", - } - - _body = [] - for body_item_data in body: - body_item = body_item_data.to_tuple() - - _body.append(body_item) - - _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 == 200: - response_200 = response.json() - return response_200 - if response.status_code == 422: - 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: list[File], -) -> Response[Union[Any, HTTPValidationError]]: - """Upload multiple files - - Upload several files in the same request - - Args: - body (list[File]): - - 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: list[File], -) -> Optional[Union[Any, HTTPValidationError]]: - """Upload multiple files - - Upload several files in the same request - - Args: - body (list[File]): - - 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: list[File], -) -> Response[Union[Any, HTTPValidationError]]: - """Upload multiple files - - Upload several files in the same request - - Args: - body (list[File]): - - 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: list[File], -) -> Optional[Union[Any, HTTPValidationError]]: - """Upload multiple files - - Upload several files in the same request - - Args: - body (list[File]): - - 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/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 5bc69e864..642fbf703 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 @@ -8,8 +8,9 @@ from attrs import field as _attrs_field from dateutil.parser import isoparse +from .. import types from ..models.different_enum import DifferentEnum -from ..types import UNSET, File, FileJsonType, Unset +from ..types import UNSET, File, Unset if TYPE_CHECKING: from ..models.a_form_data import AFormData @@ -83,7 +84,7 @@ def to_dict(self) -> dict[str, Any]: else: some_nullable_object = self.some_nullable_object - some_optional_file: Union[Unset, FileJsonType] = UNSET + some_optional_file: Union[Unset, types.FileTypes] = UNSET if not isinstance(self.some_optional_file, Unset): some_optional_file = self.some_optional_file.to_tuple() @@ -168,75 +169,67 @@ def to_dict(self) -> dict[str, Any]: return field_dict - def to_multipart(self) -> list[tuple[str, Any]]: - field_list: list[tuple[str, Any]] = [] + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] - field_list.append(("some_file", self.some_file.to_tuple())) + files.append(("some_file", self.some_file.to_tuple())) - field_list.append(("some_required_number", (None, str(self.some_required_number).encode(), "text/plain"))) + files.append(("some_required_number", (None, str(self.some_required_number).encode(), "text/plain"))) - field_list.append(("some_object", (None, json.dumps(self.some_object.to_dict()).encode(), "application/json"))) + files.append(("some_object", (None, json.dumps(self.some_object.to_dict()).encode(), "application/json"))) if isinstance(self.some_nullable_object, BodyUploadFileTestsUploadPostSomeNullableObject): - field_list.append( + files.append( ( "some_nullable_object", (None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json"), ) ) else: - field_list.append(("some_nullable_object", (None, str(self.some_nullable_object).encode(), "text/plain"))) + files.append(("some_nullable_object", (None, str(self.some_nullable_object).encode(), "text/plain"))) if not isinstance(self.some_optional_file, Unset): - field_list.append(("some_optional_file", self.some_optional_file.to_tuple())) + files.append(("some_optional_file", self.some_optional_file.to_tuple())) if not isinstance(self.some_string, Unset): - field_list.append(("some_string", (None, str(self.some_string).encode(), "text/plain"))) + files.append(("some_string", (None, str(self.some_string).encode(), "text/plain"))) if not isinstance(self.a_datetime, Unset): - field_list.append(("a_datetime", self.a_datetime.isoformat().encode())) + files.append(("a_datetime", (None, self.a_datetime.isoformat().encode(), "text/plain"))) if not isinstance(self.a_date, Unset): - field_list.append(("a_date", self.a_date.isoformat().encode())) + files.append(("a_date", (None, self.a_date.isoformat().encode(), "text/plain"))) if not isinstance(self.some_number, Unset): - field_list.append(("some_number", (None, str(self.some_number).encode(), "text/plain"))) + files.append(("some_number", (None, str(self.some_number).encode(), "text/plain"))) if not isinstance(self.some_nullable_number, Unset): if isinstance(self.some_nullable_number, float): - field_list.append( - ("some_nullable_number", (None, str(self.some_nullable_number).encode(), "text/plain")) - ) + files.append(("some_nullable_number", (None, str(self.some_nullable_number).encode(), "text/plain"))) else: - field_list.append( - ("some_nullable_number", (None, str(self.some_nullable_number).encode(), "text/plain")) - ) + files.append(("some_nullable_number", (None, str(self.some_nullable_number).encode(), "text/plain"))) if not isinstance(self.some_int_array, Unset): for some_int_array_item_element in self.some_int_array: if isinstance(some_int_array_item_element, int): - field_list.append( - ("some_int_array", (None, str(some_int_array_item_element).encode(), "text/plain")) - ) + files.append(("some_int_array", (None, str(some_int_array_item_element).encode(), "text/plain"))) else: - field_list.append( - ("some_int_array", (None, str(some_int_array_item_element).encode(), "text/plain")) - ) + files.append(("some_int_array", (None, str(some_int_array_item_element).encode(), "text/plain"))) if not isinstance(self.some_array, Unset): if isinstance(self.some_array, list): for some_array_type_0_item_element in self.some_array: - field_list.append( + files.append( ( "some_array", (None, json.dumps(some_array_type_0_item_element.to_dict()).encode(), "application/json"), ) ) else: - field_list.append(("some_array", (None, str(self.some_array).encode(), "text/plain"))) + files.append(("some_array", (None, str(self.some_array).encode(), "text/plain"))) if not isinstance(self.some_optional_object, Unset): - field_list.append( + files.append( ( "some_optional_object", (None, json.dumps(self.some_optional_object.to_dict()).encode(), "application/json"), @@ -244,12 +237,12 @@ def to_multipart(self) -> list[tuple[str, Any]]: ) if not isinstance(self.some_enum, Unset): - field_list.append(("some_enum", (None, str(self.some_enum.value).encode(), "text/plain"))) + files.append(("some_enum", (None, str(self.some_enum.value).encode(), "text/plain"))) for prop_name, prop in self.additional_properties.items(): - field_list.append((prop_name, (None, json.dumps(prop.to_dict()).encode(), "application/json"))) + files.append((prop_name, (None, json.dumps(prop.to_dict()).encode(), "application/json"))) - return field_list + return files @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 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 81707d241..9d7b27eb1 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 @@ -4,6 +4,7 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field +from .. import types from ..types import UNSET, Unset T = TypeVar("T", bound="PostBodiesMultipleFilesBody") @@ -30,16 +31,16 @@ def to_dict(self) -> dict[str, Any]: return field_dict - def to_multipart(self) -> list[tuple[str, Any]]: - field_list: list[tuple[str, Any]] = [] + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] if not isinstance(self.a, Unset): - field_list.append(("a", (None, str(self.a).encode(), "text/plain"))) + files.append(("a", (None, str(self.a).encode(), "text/plain"))) for prop_name, prop in self.additional_properties.items(): - field_list.append((prop_name, (None, str(prop).encode(), "text/plain"))) + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) - return field_list + return files @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index b9ed58b8a..1b96ca408 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -1,8 +1,8 @@ """Contains some shared types for properties""" -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar +from typing import IO, BinaryIO, Generic, Literal, Optional, TypeVar, Union from attrs import define @@ -14,7 +14,15 @@ def __bool__(self) -> Literal[False]: UNSET: Unset = Unset() -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # (filename, file (or bytes), content_type) + tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = list[tuple[str, FileTypes]] @define @@ -25,7 +33,7 @@ class File: file_name: Optional[str] = None mime_type: Optional[str] = None - def to_tuple(self) -> FileJsonType: + def to_tuple(self) -> FileTypes: """Return a tuple representation that httpx will accept for multipart/form-data""" return self.file_name, self.payload, self.mime_type @@ -43,4 +51,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py index 82df1a8f8..223f5c073 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py @@ -21,9 +21,7 @@ def _get_kwargs( "url": "/tests/", } - _body = body.to_multipart() - - _kwargs["files"] = _body + _kwargs["files"] = body.to_multipart() _kwargs["headers"] = headers return _kwargs diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py index e582bd869..86212c124 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py @@ -4,6 +4,7 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field +from .. import types from ..models.an_all_of_enum import AnAllOfEnum, check_an_all_of_enum from ..models.an_enum import AnEnum, check_an_enum from ..models.an_enum_with_null import AnEnumWithNull, check_an_enum_with_null @@ -93,24 +94,24 @@ def to_dict(self) -> dict[str, Any]: return field_dict - def to_multipart(self) -> list[tuple[str, Any]]: - field_list: list[tuple[str, Any]] = [] + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] if not isinstance(self.an_enum_value, Unset): for an_enum_value_item_element in self.an_enum_value: - field_list.append(("an_enum_value", (None, str(an_enum_value_item_element).encode(), "text/plain"))) + files.append(("an_enum_value", (None, str(an_enum_value_item_element).encode(), "text/plain"))) if not isinstance(self.an_enum_value_with_null, Unset): for an_enum_value_with_null_item_element in self.an_enum_value_with_null: if an_enum_value_with_null_item_element is None: - field_list.append( + files.append( ( "an_enum_value_with_null", (None, str(an_enum_value_with_null_item_element).encode(), "text/plain"), ) ) else: - field_list.append( + files.append( ( "an_enum_value_with_null", (None, str(an_enum_value_with_null_item_element).encode(), "text/plain"), @@ -119,7 +120,7 @@ def to_multipart(self) -> list[tuple[str, Any]]: if not isinstance(self.an_enum_value_with_only_null, Unset): for an_enum_value_with_only_null_item_element in self.an_enum_value_with_only_null: - field_list.append( + files.append( ( "an_enum_value_with_only_null", (None, str(an_enum_value_with_only_null_item_element).encode(), "text/plain"), @@ -127,7 +128,7 @@ def to_multipart(self) -> list[tuple[str, Any]]: ) if not isinstance(self.an_allof_enum_with_overridden_default, Unset): - field_list.append( + files.append( ( "an_allof_enum_with_overridden_default", (None, str(self.an_allof_enum_with_overridden_default).encode(), "text/plain"), @@ -135,14 +136,12 @@ def to_multipart(self) -> list[tuple[str, Any]]: ) if not isinstance(self.an_optional_allof_enum, Unset): - field_list.append( - ("an_optional_allof_enum", (None, str(self.an_optional_allof_enum).encode(), "text/plain")) - ) + files.append(("an_optional_allof_enum", (None, str(self.an_optional_allof_enum).encode(), "text/plain"))) if not isinstance(self.nested_list_of_enums, Unset): for nested_list_of_enums_item_element in self.nested_list_of_enums: for nested_list_of_enums_item_item_element in nested_list_of_enums_item_element: - field_list.append( + files.append( ( "nested_list_of_enums", (None, str(nested_list_of_enums_item_item_element).encode(), "text/plain"), @@ -150,9 +149,9 @@ def to_multipart(self) -> list[tuple[str, Any]]: ) for prop_name, prop in self.additional_properties.items(): - field_list.append((prop_name, (None, str(prop).encode(), "text/plain"))) + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) - return field_list + return files @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py index b9ed58b8a..1b96ca408 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py @@ -1,8 +1,8 @@ """Contains some shared types for properties""" -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar +from typing import IO, BinaryIO, Generic, Literal, Optional, TypeVar, Union from attrs import define @@ -14,7 +14,15 @@ def __bool__(self) -> Literal[False]: UNSET: Unset = Unset() -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # (filename, file (or bytes), content_type) + tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = list[tuple[str, FileTypes]] @define @@ -25,7 +33,7 @@ class File: file_name: Optional[str] = None mime_type: Optional[str] = None - def to_tuple(self) -> FileJsonType: + def to_tuple(self) -> FileTypes: """Return a tuple representation that httpx will accept for multipart/form-data""" return self.file_name, self.payload, self.mime_type @@ -43,4 +51,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py index 0d05b8189..1bb532823 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py @@ -32,9 +32,8 @@ def _get_kwargs( "params": params, } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py index f12d53f18..48a2c1733 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py @@ -20,9 +20,8 @@ def _get_kwargs( "url": "/prefixItems", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py index b9ed58b8a..1b96ca408 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py @@ -1,8 +1,8 @@ """Contains some shared types for properties""" -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar +from typing import IO, BinaryIO, Generic, Literal, Optional, TypeVar, Union from attrs import define @@ -14,7 +14,15 @@ def __bool__(self) -> Literal[False]: UNSET: Unset = Unset() -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # (filename, file (or bytes), content_type) + tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = list[tuple[str, FileTypes]] @define @@ -25,7 +33,7 @@ class File: file_name: Optional[str] = None mime_type: Optional[str] = None - def to_tuple(self) -> FileJsonType: + def to_tuple(self) -> FileTypes: """Return a tuple representation that httpx will accept for multipart/form-data""" return self.file_name, self.payload, self.mime_type @@ -43,4 +51,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/integration-tests/integration_tests/api/body/post_body_multipart.py b/integration-tests/integration_tests/api/body/post_body_multipart.py index 13c943ad7..3f5c883f4 100644 --- a/integration-tests/integration_tests/api/body/post_body_multipart.py +++ b/integration-tests/integration_tests/api/body/post_body_multipart.py @@ -22,9 +22,7 @@ def _get_kwargs( "url": "/body/multipart", } - _body = body.to_multipart() - - _kwargs["files"] = _body + _kwargs["files"] = body.to_multipart() _kwargs["headers"] = headers return _kwargs 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 528d862f1..12d56d0fe 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -5,6 +5,7 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field +from .. import types from ..types import File T = TypeVar("T", bound="PostBodyMultipartBody") @@ -47,20 +48,20 @@ def to_dict(self) -> dict[str, Any]: return field_dict - def to_multipart(self) -> list[tuple[str, Any]]: - field_list: list[tuple[str, Any]] = [] + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] - field_list.append(("a_string", (None, str(self.a_string).encode(), "text/plain"))) + files.append(("a_string", (None, str(self.a_string).encode(), "text/plain"))) for files_item_element in self.files: - field_list.append(("files", files_item_element.to_tuple())) + files.append(("files", files_item_element.to_tuple())) - field_list.append(("description", (None, str(self.description).encode(), "text/plain"))) + files.append(("description", (None, str(self.description).encode(), "text/plain"))) for prop_name, prop in self.additional_properties.items(): - field_list.append((prop_name, (None, str(prop).encode(), "text/plain"))) + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) - return field_list + return files @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: diff --git a/integration-tests/integration_tests/types.py b/integration-tests/integration_tests/types.py index b9ed58b8a..1b96ca408 100644 --- a/integration-tests/integration_tests/types.py +++ b/integration-tests/integration_tests/types.py @@ -1,8 +1,8 @@ """Contains some shared types for properties""" -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar +from typing import IO, BinaryIO, Generic, Literal, Optional, TypeVar, Union from attrs import define @@ -14,7 +14,15 @@ def __bool__(self) -> Literal[False]: UNSET: Unset = Unset() -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # (filename, file (or bytes), content_type) + tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = list[tuple[str, FileTypes]] @define @@ -25,7 +33,7 @@ class File: file_name: Optional[str] = None mime_type: Optional[str] = None - def to_tuple(self) -> FileJsonType: + def to_tuple(self) -> FileTypes: """Return a tuple representation that httpx will accept for multipart/form-data""" return self.file_name, self.payload, self.mime_type @@ -43,4 +51,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/integration-tests/pdm.lock b/integration-tests/pdm.lock index ccfdb98fd..9139ccc36 100644 --- a/integration-tests/pdm.lock +++ b/integration-tests/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:a575c5fc1f04f52530c52becbcc2f40498a54612d0eeeaddf6701fe5336986ac" +content_hash = "sha256:6d5a710a30407796a6b9820e29cb340039a53899b8b595a6431d2940e4bd7d5f" [[metadata.targets]] requires_python = "~=3.9" diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index a7252bd1e..32f10d25b 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -24,7 +24,7 @@ dev = [ [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" - + [tool.ruff] line-length = 120 diff --git a/openapi_python_client/parser/properties/file.py b/openapi_python_client/parser/properties/file.py index 505876b63..97de3e093 100644 --- a/openapi_python_client/parser/properties/file.py +++ b/openapi_python_client/parser/properties/file.py @@ -22,7 +22,7 @@ class FileProperty(PropertyProtocol): _type_string: ClassVar[str] = "File" # Return type of File.to_tuple() - _json_type_string: ClassVar[str] = "FileJsonType" + _json_type_string: ClassVar[str] = "types.FileTypes" template: ClassVar[str] = "file_property.py.jinja" @classmethod @@ -63,5 +63,5 @@ def get_imports(self, *, prefix: str) -> set[str]: back to the root of the generated client. """ imports = super().get_imports(prefix=prefix) - imports.update({f"from {prefix}types import File, FileJsonType", "from io import BytesIO"}) + imports.update({f"from {prefix}types import File, FileTypes", "from io import BytesIO"}) return imports diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 2da580af4..1f4ee138f 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -58,33 +58,33 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None} {% endif %} {% endmacro %} -{% macro body_to_kwarg(body, destination) %} +{% macro body_to_kwarg(body) %} {% if body.body_type == "data" %} -{{ destination }} = body.to_dict() +_kwargs["data"] = body.to_dict() {% elif body.body_type == "files"%} -{{ multipart_body(body, destination) }} +{{ multipart_body(body) }} {% elif body.body_type == "json" %} -{{ json_body(body, destination) }} +{{ json_body(body) }} {% elif body.body_type == "content" %} -{{ destination }} = body.payload +_kwargs["content"] = body.payload {% endif %} {% endmacro %} -{% macro json_body(body, destination) %} +{% macro json_body(body) %} {% set property = body.prop %} {% import "property_templates/" + property.template as prop_template %} {% if prop_template.transform %} -{{ prop_template.transform(property, property.python_name, destination) }} +{{ prop_template.transform(property, property.python_name, "_kwargs[\"json\"]") }} {% else %} -{{ destination }} = {{ property.python_name }} +_kwargs["json"] = {{ property.python_name }} {% endif %} {% endmacro %} -{% macro multipart_body(body, destination) %} +{% macro multipart_body(body) %} {% set property = body.prop %} {% import "property_templates/" + property.template as prop_template %} {% if prop_template.transform_multipart_body %} -{{ prop_template.transform_multipart_body(property, property.python_name, destination) }} +{{ prop_template.transform_multipart_body(property, property.python_name) }} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 35090614c..802fcc2ea 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -48,15 +48,12 @@ def _get_kwargs( {% if endpoint.bodies | length > 1 %} {% for body in endpoint.bodies %} if isinstance(body, {{body.prop.get_type_string() }}): - {% set destination = "_" + body.body_type + "_body" %} - {{ body_to_kwarg(body, destination) | indent(8) }} - _kwargs["{{ body.body_type.value }}"] = {{ destination }} + {{ body_to_kwarg(body) | indent(8) }} headers["Content-Type"] = "{{ body.content_type }}" {% endfor %} {% elif endpoint.bodies | length == 1 %} {% set body = endpoint.bodies[0] %} - {{ body_to_kwarg(body, "_body") | indent(4) }} - _kwargs["{{ body.body_type.value }}"] = _body + {{ body_to_kwarg(body) | indent(4) }} {% if body.content_type != "multipart/form-data" %}{# Need httpx to set the boundary automatically #} headers["Content-Type"] = "{{ body.content_type }}" {% endif %} diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 2f48654ec..d792797c3 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -1,10 +1,11 @@ from collections.abc import Mapping -from typing import Any, TypeVar, Optional, BinaryIO, TextIO, TYPE_CHECKING +from typing import Any, TypeVar, Optional, BinaryIO, TextIO, TYPE_CHECKING, Generator from attrs import define as _attrs_define from attrs import field as _attrs_field {% if model.is_multipart_body %} import json +from .. import types {% endif %} from ..types import UNSET, Unset @@ -147,8 +148,8 @@ return field_dict {{ _to_dict() | indent(8) }} {% if model.is_multipart_body %} - def to_multipart(self) -> list[tuple[str, Any]]: - field_list: list[tuple[str, Any]] = [] + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] {% for property in model.required_properties + model.optional_properties %} {% set destination = "\"" + property.name + "\"" %} @@ -161,7 +162,7 @@ return field_dict {{ multipart(model.additional_properties, "prop", "prop_name") | indent(4) }} {% endif %} - return field_list + return files {% endif %} diff --git a/openapi_python_client/templates/property_templates/any_property.py.jinja b/openapi_python_client/templates/property_templates/any_property.py.jinja index 0eff2d50e..ad3f195a4 100644 --- a/openapi_python_client/templates/property_templates/any_property.py.jinja +++ b/openapi_python_client/templates/property_templates/any_property.py.jinja @@ -1,3 +1,3 @@ -{% macro multipart(property, source, destination) %} -field_list.append(({{ destination }}, (None, str({{source}}).encode(), "text/plain"))) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} \ No newline at end of file diff --git a/openapi_python_client/templates/property_templates/boolean_property.py.jinja b/openapi_python_client/templates/property_templates/boolean_property.py.jinja index b9a837e9c..e2c3392a1 100644 --- a/openapi_python_client/templates/property_templates/boolean_property.py.jinja +++ b/openapi_python_client/templates/property_templates/boolean_property.py.jinja @@ -2,6 +2,6 @@ "true" if {{ source }} else "false" {% endmacro %} -{% macro multipart(property, source, destination) %} -field_list.append(({{ destination }}, (None, str({{source}}).encode(), "text/plain"))) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/const_property.py.jinja b/openapi_python_client/templates/property_templates/const_property.py.jinja index 8ad5bbff9..d348de0ff 100644 --- a/openapi_python_client/templates/property_templates/const_property.py.jinja +++ b/openapi_python_client/templates/property_templates/const_property.py.jinja @@ -4,6 +4,6 @@ if {{ property.python_name }} != {{ property.value.python_code }}{% if not prope raise ValueError(f"{{ property.name }} must match const {{ property.value.python_code }}, got '{{'{' + property.python_name + '}' }}'") {%- endmacro %} -{% macro multipart(property, source, destination) %} -field_list.append(({{ destination }}, source)) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, {{ source }}, "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/date_property.py.jinja b/openapi_python_client/templates/property_templates/date_property.py.jinja index bf88444fe..3ca8faee9 100644 --- a/openapi_python_client/templates/property_templates/date_property.py.jinja +++ b/openapi_python_client/templates/property_templates/date_property.py.jinja @@ -26,7 +26,6 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro multipart(property, source, destination) %} -{% set transformed = source + ".isoformat().encode()" %} -field_list.append(({{ destination }}, {{ transformed }})) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, {{ source }}.isoformat().encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/datetime_property.py.jinja b/openapi_python_client/templates/property_templates/datetime_property.py.jinja index 29c38bc97..bf7e601d1 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.py.jinja +++ b/openapi_python_client/templates/property_templates/datetime_property.py.jinja @@ -26,7 +26,6 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro multipart(property, source, destination) %} -{% set transformed = source + ".isoformat().encode()" %} -field_list.append(({{ destination }}, {{ transformed }})) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, {{ source }}.isoformat().encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/enum_property.py.jinja b/openapi_python_client/templates/property_templates/enum_property.py.jinja index aac89aeac..401fa59b7 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -22,9 +22,8 @@ if not isinstance({{ source }}, Unset): {% endif %} {% endmacro %} -{% macro multipart(property, source, destination) %} -{% set transformed = "(None, str(" + source + ".value" + ").encode(), \"text/plain\")" %} -field_list.append(({{ destination }}, {{ transformed }})) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{ source }}.value).encode(), "text/plain"))) {% endmacro %} {% macro transform_header(source) %} diff --git a/openapi_python_client/templates/property_templates/file_property.py.jinja b/openapi_python_client/templates/property_templates/file_property.py.jinja index 275650db3..b08a13b46 100644 --- a/openapi_python_client/templates/property_templates/file_property.py.jinja +++ b/openapi_python_client/templates/property_templates/file_property.py.jinja @@ -22,6 +22,6 @@ if not isinstance({{ source }}, Unset): {% endif %} {% endmacro %} -{% macro multipart(property, source, destination) %} -field_list.append(({{ destination }}, {{ source }}.to_tuple())) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, {{ source }}.to_tuple())) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/float_property.py.jinja b/openapi_python_client/templates/property_templates/float_property.py.jinja index 0729c82fb..dc982cb68 100644 --- a/openapi_python_client/templates/property_templates/float_property.py.jinja +++ b/openapi_python_client/templates/property_templates/float_property.py.jinja @@ -2,6 +2,6 @@ str({{ source }}) {% endmacro %} -{% macro multipart(property, source, destination) %} -field_list.append(({{ destination }}, (None, str({{source}}).encode(), "text/plain"))) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/int_property.py.jinja b/openapi_python_client/templates/property_templates/int_property.py.jinja index 0729c82fb..dc982cb68 100644 --- a/openapi_python_client/templates/property_templates/int_property.py.jinja +++ b/openapi_python_client/templates/property_templates/int_property.py.jinja @@ -2,6 +2,6 @@ str({{ source }}) {% endmacro %} -{% macro multipart(property, source, destination) %} -field_list.append(({{ destination }}, (None, str({{source}}).encode(), "text/plain"))) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/list_property.py.jinja b/openapi_python_client/templates/property_templates/list_property.py.jinja index fc68cb2ec..785d0b675 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -45,18 +45,6 @@ if not isinstance({{ source }}, Unset): {% endif %} {% endmacro %} -{% macro transform_multipart_body(property, source, destination) %} -{% set inner_property = property.inner_property %} -{% set type_string = property.get_type_string(json=True) %} -{% if property.required %} -{{ _transform(property, source, destination, "to_multipart") }} -{% else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ _transform(property, source, destination, "to_multipart") | indent(4)}} -{% endif %} -{% endmacro %} - {% macro multipart(property, source, destination) %} {% set inner_property = property.inner_property %} {% import "property_templates/" + inner_property.template as inner_template %} diff --git a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja index 442d102ea..91c8d31be 100644 --- a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja @@ -21,9 +21,8 @@ if not isinstance({{ source }}, Unset): {% endif %} {% endmacro %} -{% macro multipart(property, source, destination) %} -{% set transformed = "(None, str(" + source + ").encode(), \"text/plain\")" %} -field_list.append(({{ destination }}, {{ transformed }})) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{ source }}).encode(), "text/plain"))) {% endmacro %} {% macro transform_header(source) %} diff --git a/openapi_python_client/templates/property_templates/model_property.py.jinja b/openapi_python_client/templates/property_templates/model_property.py.jinja index fbc517b43..203ea3de0 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -22,19 +22,16 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro transform_multipart_body(property, source, destination) %} +{% macro transform_multipart_body(property, source) %} {% set transformed = source + ".to_multipart()" %} -{% set type_string = property.get_type_string(multipart=True) %} {% if property.required %} -{{ destination }} = {{ transformed }} +_kwargs["files"] = {{ transformed }} {%- else %} -{{ destination }}: {{ type_string }} = UNSET if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} + _kwargs["files"] = {{ transformed }} {%- endif %} {% endmacro %} -{% macro multipart(property, source, destination) %} -{% set transformed = "(None, json.dumps(" + source + ".to_dict()" + ").encode(), 'application/json')" %} -field_list.append(({{ destination }}, {{ transformed }})) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, json.dumps( {{source}}.to_dict()).encode(), "application/json"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/uuid_property.py.jinja b/openapi_python_client/templates/property_templates/uuid_property.py.jinja index 922cf0f89..3a6ce46bb 100644 --- a/openapi_python_client/templates/property_templates/uuid_property.py.jinja +++ b/openapi_python_client/templates/property_templates/uuid_property.py.jinja @@ -26,6 +26,6 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro multipart(property, source, destination) %} -field_list.append(({{ destination }},str({{ source }}))) +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{ source }}), "text/plain")) {% endmacro %} diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index 6e0d6206c..b090f8dd4 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -2,7 +2,7 @@ from collections.abc import MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Optional, TypeVar, Literal +from typing import BinaryIO, Generic, Optional, TypeVar, Literal, Union, IO, Mapping from attrs import define @@ -14,9 +14,15 @@ class Unset: UNSET: Unset = Unset() -{# Used as `FileProperty._json_type_string` #} -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] - +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # (filename, file (or bytes), content_type) + tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = list[tuple[str, FileTypes]] @define class File: @@ -26,7 +32,7 @@ class File: file_name: Optional[str] = None mime_type: Optional[str] = None - def to_tuple(self) -> FileJsonType: + def to_tuple(self) -> FileTypes: """ Return a tuple representation that httpx will accept for multipart/form-data """ return self.file_name, self.payload, self.mime_type @@ -44,4 +50,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 1af6342ad..3468700db 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -23,7 +23,7 @@ def test_get_imports(self, file_property_factory, required): expected = { "from io import BytesIO", - "from ...types import File, FileJsonType", + "from ...types import File, FileTypes", } if not required: expected |= { From af9c9fa0c00495b1781785ada1852cc3e81d1cbe Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 2 Jun 2025 20:14:53 -0600 Subject: [PATCH 12/16] Remove now-dead code --- .../parser/properties/const.py | 1 - .../parser/properties/list_property.py | 3 --- .../parser/properties/model_property.py | 3 --- .../parser/properties/protocol.py | 4 ---- .../parser/properties/union.py | 17 +++++++---------- .../property_templates/enum_property.py.jinja | 2 +- .../literal_enum_property.py.jinja | 2 +- .../property_templates/union_property.py.jinja | 6 +++--- 8 files changed, 12 insertions(+), 26 deletions(-) diff --git a/openapi_python_client/parser/properties/const.py b/openapi_python_client/parser/properties/const.py index 8b9967f19..15f7d0f24 100644 --- a/openapi_python_client/parser/properties/const.py +++ b/openapi_python_client/parser/properties/const.py @@ -94,7 +94,6 @@ def get_type_string( no_optional: bool = False, json: bool = False, *, - multipart: bool = False, quoted: bool = False, ) -> str: lit = f"Literal[{self.value.python_code}]" diff --git a/openapi_python_client/parser/properties/list_property.py b/openapi_python_client/parser/properties/list_property.py index bf0756fe3..7a4a4f209 100644 --- a/openapi_python_client/parser/properties/list_property.py +++ b/openapi_python_client/parser/properties/list_property.py @@ -138,7 +138,6 @@ def get_type_string( no_optional: bool = False, json: bool = False, *, - multipart: bool = False, quoted: bool = False, ) -> str: """ @@ -150,8 +149,6 @@ def get_type_string( """ if json: type_string = self.get_base_json_type_string() - elif multipart: - type_string = "tuple[None, bytes, str]" else: type_string = self.get_base_type_string() diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 762624501..687ef2542 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -189,7 +189,6 @@ def get_type_string( no_optional: bool = False, json: bool = False, *, - multipart: bool = False, quoted: bool = False, ) -> str: """ @@ -201,8 +200,6 @@ def get_type_string( """ if json: type_string = self.get_base_json_type_string() - elif multipart: - type_string = "tuple[None, bytes, str]" else: type_string = self.get_base_type_string() diff --git a/openapi_python_client/parser/properties/protocol.py b/openapi_python_client/parser/properties/protocol.py index 9a5b51828..7c1891545 100644 --- a/openapi_python_client/parser/properties/protocol.py +++ b/openapi_python_client/parser/properties/protocol.py @@ -101,7 +101,6 @@ def get_type_string( no_optional: bool = False, json: bool = False, *, - multipart: bool = False, quoted: bool = False, ) -> str: """ @@ -110,13 +109,10 @@ def get_type_string( Args: no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) json: True if the type refers to the property after JSON serialization - multipart: True if the type should be used in a multipart request quoted: True if the type should be wrapped in quotes (if not a base type) """ if json: type_string = self.get_base_json_type_string(quoted=quoted) - elif multipart: - type_string = "tuple[None, bytes, str]" else: type_string = self.get_base_type_string(quoted=quoted) diff --git a/openapi_python_client/parser/properties/union.py b/openapi_python_client/parser/properties/union.py index 8b7b02a48..b562a07a8 100644 --- a/openapi_python_client/parser/properties/union.py +++ b/openapi_python_client/parser/properties/union.py @@ -106,10 +106,9 @@ def convert_value(self, value: Any) -> Value | None | PropertyError: return value_or_error return value_or_error - def _get_inner_type_strings(self, json: bool, multipart: bool) -> set[str]: + def _get_inner_type_strings(self, json: bool) -> set[str]: return { - p.get_type_string(no_optional=True, json=json, multipart=multipart, quoted=not p.is_base_type) - for p in self.inner_properties + p.get_type_string(no_optional=True, json=json, quoted=not p.is_base_type) for p in self.inner_properties } @staticmethod @@ -119,12 +118,12 @@ def _get_type_string_from_inner_type_strings(inner_types: set[str]) -> str: return f"Union[{', '.join(sorted(inner_types))}]" def get_base_type_string(self, *, quoted: bool = False) -> str: - return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=False, multipart=False)) + return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=False)) def get_base_json_type_string(self, *, quoted: bool = False) -> str: - return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=True, multipart=False)) + return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=True)) - def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool, multipart: bool) -> set[str]: + def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool) -> set[str]: """ Get the set of all the types that should appear within the `Union` representing this property. @@ -133,12 +132,11 @@ def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool, mu Args: no_optional: Do not include `None` or `Unset` in this set. json: If True, this returns the JSON types, not the Python types, of this property. - multipart: If True, this returns the multipart types, not the Python types, of this property. Returns: A set of strings containing the types that should appear within `Union`. """ - type_strings = self._get_inner_type_strings(json=json, multipart=multipart) + type_strings = self._get_inner_type_strings(json=json) if no_optional: return type_strings if not self.required: @@ -150,7 +148,6 @@ def get_type_string( no_optional: bool = False, json: bool = False, *, - multipart: bool = False, quoted: bool = False, ) -> str: """ @@ -158,7 +155,7 @@ def get_type_string( This implementation differs slightly from `Property.get_type_string` in order to collapse nested union types. """ - type_strings_in_union = self.get_type_strings_in_union(no_optional=no_optional, json=json, multipart=multipart) + type_strings_in_union = self.get_type_strings_in_union(no_optional=no_optional, json=json) return self._get_type_string_from_inner_type_strings(type_strings_in_union) def get_imports(self, *, prefix: str) -> set[str]: diff --git a/openapi_python_client/templates/property_templates/enum_property.py.jinja b/openapi_python_client/templates/property_templates/enum_property.py.jinja index 401fa59b7..af8ca6eff 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -10,7 +10,7 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.value_type.__name__ }}){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False) %} +{% macro transform(property, source, destination, declare_type=True) %} {% set transformed = source + ".value" %} {% set type_string = property.get_type_string(json=True) %} {% if property.required %} diff --git a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja index 91c8d31be..2cc4558c6 100644 --- a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja @@ -10,7 +10,7 @@ check_{{ property.get_class_name_snake_case() }}({{ source }}) {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.get_instance_type_string() }}){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False) %} +{% macro transform(property, source, destination, declare_type=True) %} {% set type_string = property.get_type_string(json=True) %} {% if property.required %} {{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = {{ source }} diff --git a/openapi_python_client/templates/property_templates/union_property.py.jinja b/openapi_python_client/templates/property_templates/union_property.py.jinja index 5f76702f9..09b6e6e09 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -1,10 +1,10 @@ {% macro construct(property, source) %} def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_string() }}: - {% if "None" in property.get_type_strings_in_union(json=True, multipart=False) %} + {% if "None" in property.get_type_strings_in_union(json=True) %} if data is None: return data {% endif %} - {% if "Unset" in property.get_type_strings_in_union(json=True, multipart=False) %} + {% if "Unset" in property.get_type_strings_in_union(json=True) %} if isinstance(data, Unset): return data {% endif %} @@ -41,7 +41,7 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri {% macro transform(property, source, destination, declare_type=True) %} {% set ns = namespace(contains_properties_without_transform = false, contains_modified_properties = not property.required, has_if = false) %} -{% if declare_type %}{{ destination }}: {{ property.get_type_string(json=True, multipart=False) }}{% endif %} +{% if declare_type %}{{ destination }}: {{ property.get_type_string(json=True) }}{% endif %} {% if not property.required %} if isinstance({{ source }}, Unset): From 368083f25eb3b4a2b59c2ec0ac944f8ff893b6ef Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 2 Jun 2025 20:45:34 -0600 Subject: [PATCH 13/16] Document main multipart array change --- ...ds_in_multipart_instead_of_serializing_as_json.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/repeat_array_fields_in_multipart_instead_of_serializing_as_json.md diff --git a/.changeset/repeat_array_fields_in_multipart_instead_of_serializing_as_json.md b/.changeset/repeat_array_fields_in_multipart_instead_of_serializing_as_json.md new file mode 100644 index 000000000..072ec3a6c --- /dev/null +++ b/.changeset/repeat_array_fields_in_multipart_instead_of_serializing_as_json.md @@ -0,0 +1,12 @@ +--- +default: major +--- + +# Change default multipart array serialization + +Previously, any arrays of values in a `multipart/form-data` body would be serialized as an `application/json` part. +This matches the default behavior specified by OpenAPI and supports arrays of files (`binary` format strings). +However, because this generator doesn't yet support specifying `encoding` per property, this may result in +now-incorrect code when the encoding _was_ explicitly set to `application/json` for arrays of scalar values. + +PR #938 fixes #692. Thanks @micha91 for the fix, @ratgen and @FabianSchurig for testing, and @davidlizeng for the original report... many years ago 😅. From 023a1d41461d2ce2247c44a7ca611dc3fcd46247 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 2 Jun 2025 20:46:52 -0600 Subject: [PATCH 14/16] Simplify transform_multipart_body macro --- openapi_python_client/templates/endpoint_macros.py.jinja | 2 +- .../templates/property_templates/model_property.py.jinja | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 1f4ee138f..1b53becdd 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -84,7 +84,7 @@ _kwargs["json"] = {{ property.python_name }} {% set property = body.prop %} {% import "property_templates/" + property.template as prop_template %} {% if prop_template.transform_multipart_body %} -{{ prop_template.transform_multipart_body(property, property.python_name) }} +{{ prop_template.transform_multipart_body(property) }} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/model_property.py.jinja b/openapi_python_client/templates/property_templates/model_property.py.jinja index 203ea3de0..d1a4b5d34 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -22,12 +22,12 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} -{% macro transform_multipart_body(property, source) %} -{% set transformed = source + ".to_multipart()" %} +{% macro transform_multipart_body(property) %} +{% set transformed = property.python_name + ".to_multipart()" %} {% if property.required %} _kwargs["files"] = {{ transformed }} {%- else %} -if not isinstance({{ source }}, Unset): +if not isinstance({{ property.python_name }}, Unset): _kwargs["files"] = {{ transformed }} {%- endif %} {% endmacro %} From b0e43201edde464ecaef8b77ce3d9e5c091ad242 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 2 Jun 2025 21:31:10 -0600 Subject: [PATCH 15/16] Add array of scalar and JSON objects to multipart integration tests --- .github/workflows/checks.yml | 2 +- end_to_end_tests/test_end_to_end.py | 2 +- .../integration_tests/models/__init__.py | 6 +- .../integration_tests/models/an_object.py | 67 +++++++ ...art_response_200_files_item.py => file.py} | 10 +- .../models/post_body_multipart_body.py | 49 ++++- .../post_body_multipart_response_200.py | 46 ++++- .../test_body/test_post_body_multipart.py | 173 +++++++----------- .../templates/types.py.jinja | 4 +- pyproject.toml | 2 +- 10 files changed, 232 insertions(+), 129 deletions(-) create mode 100644 integration-tests/integration_tests/models/an_object.py rename integration-tests/integration_tests/models/{post_body_multipart_response_200_files_item.py => file.py} (86%) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index edc4b5580..b777cde9f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -158,7 +158,7 @@ jobs: - "pdm.minimal.lock" services: openapi-test-server: - image: ghcr.io/openapi-generators/openapi-test-server:0.2.0 + image: ghcr.io/openapi-generators/openapi-test-server:0.2.1 ports: - "3000:3000" steps: diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index 72f1df51a..4496403e1 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -266,7 +266,7 @@ def test_generate_dir_already_exists(): def test_update_integration_tests(): - url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/refs/tags/v0.2.0/openapi.yaml" + url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/refs/tags/v0.2.1/openapi.yaml" source_path = Path(__file__).parent.parent / "integration-tests" temp_dir = Path.cwd() / "test_update_integration_tests" shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/integration-tests/integration_tests/models/__init__.py b/integration-tests/integration_tests/models/__init__.py index 3243b59b0..257cfe9fa 100644 --- a/integration-tests/integration_tests/models/__init__.py +++ b/integration-tests/integration_tests/models/__init__.py @@ -1,16 +1,18 @@ """Contains all the data models used in inputs/outputs""" +from .an_object import AnObject +from .file import File from .post_body_multipart_body import PostBodyMultipartBody from .post_body_multipart_response_200 import PostBodyMultipartResponse200 -from .post_body_multipart_response_200_files_item import PostBodyMultipartResponse200FilesItem from .post_parameters_header_response_200 import PostParametersHeaderResponse200 from .problem import Problem from .public_error import PublicError __all__ = ( + "AnObject", + "File", "PostBodyMultipartBody", "PostBodyMultipartResponse200", - "PostBodyMultipartResponse200FilesItem", "PostParametersHeaderResponse200", "Problem", "PublicError", diff --git a/integration-tests/integration_tests/models/an_object.py b/integration-tests/integration_tests/models/an_object.py new file mode 100644 index 000000000..b9b5e74c2 --- /dev/null +++ b/integration-tests/integration_tests/models/an_object.py @@ -0,0 +1,67 @@ +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="AnObject") + + +@_attrs_define +class AnObject: + """ + Attributes: + an_int (int): + a_float (float): + """ + + an_int: int + a_float: float + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + an_int = self.an_int + + a_float = self.a_float + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "an_int": an_int, + "a_float": a_float, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + an_int = d.pop("an_int") + + a_float = d.pop("a_float") + + an_object = cls( + an_int=an_int, + a_float=a_float, + ) + + an_object.additional_properties = d + return an_object + + @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/integration-tests/integration_tests/models/post_body_multipart_response_200_files_item.py b/integration-tests/integration_tests/models/file.py similarity index 86% rename from integration-tests/integration_tests/models/post_body_multipart_response_200_files_item.py rename to integration-tests/integration_tests/models/file.py index e30f77566..133b1a288 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_response_200_files_item.py +++ b/integration-tests/integration_tests/models/file.py @@ -6,11 +6,11 @@ from ..types import UNSET, Unset -T = TypeVar("T", bound="PostBodyMultipartResponse200FilesItem") +T = TypeVar("T", bound="File") @_attrs_define -class PostBodyMultipartResponse200FilesItem: +class File: """ Attributes: data (Union[Unset, str]): Echo of content of the 'file' input parameter from the form. @@ -51,14 +51,14 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: content_type = d.pop("content_type", UNSET) - post_body_multipart_response_200_files_item = cls( + file = cls( data=data, name=name, content_type=content_type, ) - post_body_multipart_response_200_files_item.additional_properties = d - return post_body_multipart_response_200_files_item + file.additional_properties = d + return file @property def additional_keys(self) -> list[str]: 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 12d56d0fe..cb73d1d8b 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -1,13 +1,20 @@ +import datetime +import json from collections.abc import Mapping from io import BytesIO -from typing import Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field +from dateutil.parser import isoparse from .. import types from ..types import File +if TYPE_CHECKING: + from ..models.an_object import AnObject + + T = TypeVar("T", bound="PostBodyMultipartBody") @@ -18,11 +25,15 @@ class PostBodyMultipartBody: a_string (str): files (list[File]): description (str): + objects (list['AnObject']): + times (list[datetime.datetime]): """ a_string: str files: list[File] description: str + objects: list["AnObject"] + times: list[datetime.datetime] additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -36,6 +47,16 @@ def to_dict(self) -> dict[str, Any]: description = self.description + objects = [] + for objects_item_data in self.objects: + objects_item = objects_item_data.to_dict() + objects.append(objects_item) + + times = [] + for times_item_data in self.times: + times_item = times_item_data.isoformat() + times.append(times_item) + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -43,6 +64,8 @@ def to_dict(self) -> dict[str, Any]: "a_string": a_string, "files": files, "description": description, + "objects": objects, + "times": times, } ) @@ -58,6 +81,12 @@ def to_multipart(self) -> types.RequestFiles: files.append(("description", (None, str(self.description).encode(), "text/plain"))) + for objects_item_element in self.objects: + files.append(("objects", (None, json.dumps(objects_item_element.to_dict()).encode(), "application/json"))) + + for times_item_element in self.times: + files.append(("times", (None, times_item_element.isoformat().encode(), "text/plain"))) + for prop_name, prop in self.additional_properties.items(): files.append((prop_name, (None, str(prop).encode(), "text/plain"))) @@ -65,6 +94,8 @@ def to_multipart(self) -> types.RequestFiles: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.an_object import AnObject + d = dict(src_dict) a_string = d.pop("a_string") @@ -77,10 +108,26 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: description = d.pop("description") + objects = [] + _objects = d.pop("objects") + for objects_item_data in _objects: + objects_item = AnObject.from_dict(objects_item_data) + + objects.append(objects_item) + + times = [] + _times = d.pop("times") + for times_item_data in _times: + times_item = isoparse(times_item_data) + + times.append(times_item) + post_body_multipart_body = cls( a_string=a_string, files=files, description=description, + objects=objects, + times=times, ) post_body_multipart_body.additional_properties = d diff --git a/integration-tests/integration_tests/models/post_body_multipart_response_200.py b/integration-tests/integration_tests/models/post_body_multipart_response_200.py index 08c170dd1..1462b17ff 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_response_200.py +++ b/integration-tests/integration_tests/models/post_body_multipart_response_200.py @@ -1,11 +1,14 @@ +import datetime from collections.abc import Mapping from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field +from dateutil.parser import isoparse if TYPE_CHECKING: - from ..models.post_body_multipart_response_200_files_item import PostBodyMultipartResponse200FilesItem + from ..models.an_object import AnObject + from ..models.file import File T = TypeVar("T", bound="PostBodyMultipartResponse200") @@ -17,12 +20,16 @@ class PostBodyMultipartResponse200: Attributes: a_string (str): Echo of the 'a_string' input parameter from the form. description (str): Echo of the 'description' input parameter from the form. - files (list['PostBodyMultipartResponse200FilesItem']): + files (list['File']): + times (list[datetime.datetime]): + objects (list['AnObject']): """ a_string: str description: str - files: list["PostBodyMultipartResponse200FilesItem"] + files: list["File"] + times: list[datetime.datetime] + objects: list["AnObject"] additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -35,6 +42,16 @@ def to_dict(self) -> dict[str, Any]: files_item = files_item_data.to_dict() files.append(files_item) + times = [] + for times_item_data in self.times: + times_item = times_item_data.isoformat() + times.append(times_item) + + objects = [] + for objects_item_data in self.objects: + objects_item = objects_item_data.to_dict() + objects.append(objects_item) + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -42,6 +59,8 @@ def to_dict(self) -> dict[str, Any]: "a_string": a_string, "description": description, "files": files, + "times": times, + "objects": objects, } ) @@ -49,7 +68,8 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - from ..models.post_body_multipart_response_200_files_item import PostBodyMultipartResponse200FilesItem + from ..models.an_object import AnObject + from ..models.file import File d = dict(src_dict) a_string = d.pop("a_string") @@ -59,14 +79,30 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: files = [] _files = d.pop("files") for files_item_data in _files: - files_item = PostBodyMultipartResponse200FilesItem.from_dict(files_item_data) + files_item = File.from_dict(files_item_data) files.append(files_item) + times = [] + _times = d.pop("times") + for times_item_data in _times: + times_item = isoparse(times_item_data) + + times.append(times_item) + + objects = [] + _objects = d.pop("objects") + for objects_item_data in _objects: + objects_item = AnObject.from_dict(objects_item_data) + + objects.append(objects_item) + post_body_multipart_response_200 = cls( a_string=a_string, description=description, files=files, + times=times, + objects=objects, ) post_body_multipart_response_200.additional_properties = d diff --git a/integration-tests/tests/test_api/test_body/test_post_body_multipart.py b/integration-tests/tests/test_api/test_body/test_post_body_multipart.py index 6934b01ee..52ac5dd00 100644 --- a/integration-tests/tests/test_api/test_body/test_post_body_multipart.py +++ b/integration-tests/tests/test_api/test_body/test_post_body_multipart.py @@ -1,58 +1,72 @@ +from datetime import datetime, timedelta, timezone from io import BytesIO -from typing import Any +from typing import Any, Union import pytest from integration_tests.api.body import post_body_multipart from integration_tests.client import Client +from integration_tests.models import AnObject, PublicError from integration_tests.models.post_body_multipart_body import PostBodyMultipartBody from integration_tests.models.post_body_multipart_response_200 import PostBodyMultipartResponse200 -from integration_tests.types import File - -files = [ - File( - payload=BytesIO(b"some file content"), - file_name="cool_stuff.txt", - mime_type="application/openapi-python-client", - ), - File( - payload=BytesIO(b"more file content"), - file_name=None, - mime_type=None, - ), -] - - -def test(client: Client) -> None: - a_string = "a test string" - description = "super descriptive thing" - - response = post_body_multipart.sync_detailed( - client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, +from integration_tests.types import File, Response + +body = PostBodyMultipartBody( + a_string="a test string", + description="super descriptive thing", + files=[ + File( + payload=BytesIO(b"some file content"), + file_name="cool_stuff.txt", + mime_type="application/openapi-python-client", ), - ) + File( + payload=BytesIO(b"more file content"), + file_name=None, + mime_type=None, + ), + ], + times=[datetime.now(timezone.utc) - timedelta(days=1), datetime.now(timezone.utc)], + objects=[ + AnObject( + an_int=1, + a_float=2.3, + ), + AnObject( + an_int=4, + a_float=5.6, + ), + ], +) + +def check_response(response: Response[Union[PostBodyMultipartResponse200, PublicError]]) -> None: content = response.parsed if not isinstance(content, PostBodyMultipartResponse200): raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") - assert content.a_string == a_string - assert content.description == description + assert content.a_string == body.a_string + assert content.description == body.description + assert content.times == body.times + assert content.objects == body.objects + assert len(content.files) == len(body.files) for i, file in enumerate(content.files): - files[i].payload.seek(0) - assert file.data == files[i].payload.read().decode() - assert file.name == files[i].file_name - assert file.content_type == files[i].mime_type + body.files[i].payload.seek(0) + assert file.data == body.files[i].payload.read().decode() + assert file.name == body.files[i].file_name + assert file.content_type == body.files[i].mime_type -def test_custom_hooks() -> None: - a_string = "a test string" - description = "super descriptive thing" +def test(client: Client) -> None: + response = post_body_multipart.sync_detailed( + client=client, + body=body, + ) + + check_response(response) + +def test_custom_hooks() -> None: request_hook_called = False response_hook_called = False @@ -70,11 +84,7 @@ def log_response(*_: Any, **__: Any) -> None: post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, - ), + body=body, ) assert request_hook_called @@ -82,110 +92,51 @@ def log_response(*_: Any, **__: Any) -> None: def test_context_manager(client: Client) -> None: - a_string = "a test string" - description = "super descriptive thing" - with client as client: post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, - ), + body=body, ) response = post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, - ), + body=body, ) with pytest.raises(RuntimeError): post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, - ), + body=body, ) - content = response.parsed - if not isinstance(content, PostBodyMultipartResponse200): - raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") + check_response(response) @pytest.mark.asyncio async def test_async(client: Client) -> None: - a_string = "a test string" - description = "super descriptive thing" - response = await post_body_multipart.asyncio_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, - ), + body=body, ) - content = response.parsed - if not isinstance(content, PostBodyMultipartResponse200): - raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") - - assert content.a_string == a_string - assert content.description == description - for i, file in enumerate(content.files): - files[i].payload.seek(0) - assert file.data == files[i].payload.read().decode() - assert file.name == files[i].file_name - assert file.content_type == files[i].mime_type + check_response(response) @pytest.mark.asyncio async def test_async_context_manager(client: Client) -> None: - a_string = "a test string" - description = "super descriptive thing" - async with client as client: await post_body_multipart.asyncio_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, - ), + body=body, ) response = await post_body_multipart.asyncio_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, - ), + body=body, ) with pytest.raises(RuntimeError): await post_body_multipart.asyncio_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - files=files, - description=description, - ), + body=body, ) - content = response.parsed - if not isinstance(content, PostBodyMultipartResponse200): - raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") - - assert content.a_string == a_string - assert content.description == description - for i, file in enumerate(content.files): - files[i].payload.seek(0) - assert file.data == files[i].payload.read().decode() - assert file.name == files[i].file_name - assert file.content_type == files[i].mime_type + check_response(response) diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index b090f8dd4..2330892ca 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -1,8 +1,8 @@ """ Contains some shared types for properties """ -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Optional, TypeVar, Literal, Union, IO, Mapping +from typing import BinaryIO, Generic, Optional, TypeVar, Literal, Union, IO from attrs import define diff --git a/pyproject.toml b/pyproject.toml index c27a72347..fc760c12f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,7 @@ composite = ["test --cov openapi_python_client tests --cov-report=term-missing"] [tool.pdm.scripts.regen_integration] shell = """ -openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/refs/tags/v0.2.0/openapi.yaml --config integration-tests/config.yaml --meta pdm --output-path integration-tests \ +openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/refs/tags/v0.2.1/openapi.yaml --config integration-tests/config.yaml --meta none --output-path integration-tests/integration_tests \ """ [build-system] From 04e39da56bfa1801d56d3a901e5f30e3cc3d2929 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 2 Jun 2025 21:43:31 -0600 Subject: [PATCH 16/16] Fix mypy for integration tests --- integration-tests/pdm.lock | 13 ++++++++++++- integration-tests/pdm.minimal.lock | 23 +++++++++++++++++------ integration-tests/pyproject.toml | 1 + 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/integration-tests/pdm.lock b/integration-tests/pdm.lock index 9139ccc36..960f6862e 100644 --- a/integration-tests/pdm.lock +++ b/integration-tests/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:6d5a710a30407796a6b9820e29cb340039a53899b8b595a6431d2940e4bd7d5f" +content_hash = "sha256:bb0d1c899358eb4a41eab5a5eca87db988a6b92adf898ed0d9edfef6243410e9" [[metadata.targets]] requires_python = "~=3.9" @@ -332,6 +332,17 @@ files = [ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250516" +requires_python = ">=3.9" +summary = "Typing stubs for python-dateutil" +groups = ["dev"] +files = [ + {file = "types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93"}, + {file = "types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5"}, +] + [[package]] name = "typing-extensions" version = "4.13.2" diff --git a/integration-tests/pdm.minimal.lock b/integration-tests/pdm.minimal.lock index 9589c1de9..65bf986c3 100644 --- a/integration-tests/pdm.minimal.lock +++ b/integration-tests/pdm.minimal.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["direct_minimal_versions", "inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:6d5a710a30407796a6b9820e29cb340039a53899b8b595a6431d2940e4bd7d5f" +content_hash = "sha256:f4f3ea8410959314995c7373eee2541cda27a1f6033b7acfc0f030626ad8c13f" [[metadata.targets]] requires_python = "~=3.9" @@ -354,12 +354,23 @@ files = [ ] [[package]] -name = "typing-extensions" -version = "4.13.2" +name = "types-python-dateutil" +version = "2.9.0.20240315" requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" +summary = "Typing stubs for python-dateutil" +groups = ["dev"] +files = [ + {file = "types-python-dateutil-2.9.0.20240315.tar.gz", hash = "sha256:c1f6310088eb9585da1b9f811765b989ed2e2cdd4203c1a367e944b666507e4e"}, + {file = "types_python_dateutil-2.9.0.20240315-py3-none-any.whl", hash = "sha256:78aa9124f360df90bb6e85eb1a4d06e75425445bf5ecb13774cb0adef7ff3956"}, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" groups = ["default", "dev"] files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, + {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, + {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 32f10d25b..e572f62c3 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -19,6 +19,7 @@ dev = [ "pytest>8", "mypy>=1.13", "pytest-asyncio>=0.23.5", + "types-python-dateutil>=2.9", ] [build-system]