diff --git a/end_to_end_tests/custom-templates-golden-record/README.md b/end_to_end_tests/custom-templates-golden-record/README.md new file mode 100644 index 000000000..e5106eea7 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/README.md @@ -0,0 +1 @@ +my-test-api-client \ No newline at end of file diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py new file mode 100644 index 000000000..3ee5dbaf0 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py @@ -0,0 +1,21 @@ +""" Contains methods for accessing the API """ + +from typing import Type + +from my_test_api_client.api.default import DefaultEndpoints +from my_test_api_client.api.parameters import ParametersEndpoints +from my_test_api_client.api.tests import TestsEndpoints + + +class MyTestApiClientApi: + @classmethod + def tests(cls) -> Type[TestsEndpoints]: + return TestsEndpoints + + @classmethod + def default(cls) -> Type[DefaultEndpoints]: + return DefaultEndpoints + + @classmethod + def parameters(cls) -> Type[ParametersEndpoints]: + return ParametersEndpoints diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py new file mode 100644 index 000000000..4d0eb4fb5 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py @@ -0,0 +1,15 @@ +""" Contains methods for accessing the API Endpoints """ + +import types + +from my_test_api_client.api.default import get_common_parameters, post_common_parameters + + +class DefaultEndpoints: + @classmethod + def get_common_parameters(cls) -> types.ModuleType: + return get_common_parameters + + @classmethod + def post_common_parameters(cls) -> types.ModuleType: + return post_common_parameters diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py new file mode 100644 index 000000000..b92c6d96b --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py @@ -0,0 +1,11 @@ +""" Contains methods for accessing the API Endpoints """ + +import types + +from my_test_api_client.api.parameters import get_same_name_multiple_locations_param + + +class ParametersEndpoints: + @classmethod + def get_same_name_multiple_locations_param(cls) -> types.ModuleType: + return get_same_name_multiple_locations_param 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 new file mode 100644 index 000000000..dcb864fe9 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py @@ -0,0 +1,136 @@ +""" Contains methods for accessing the API Endpoints """ + +import types + +from my_test_api_client.api.tests import ( + defaults_tests_defaults_post, + get_basic_list_of_booleans, + get_basic_list_of_floats, + get_basic_list_of_integers, + get_basic_list_of_strings, + get_user_list, + int_enum_tests_int_enum_post, + json_body_tests_json_body_post, + no_response_tests_no_response_get, + octet_stream_tests_octet_stream_get, + optional_value_tests_optional_query_param, + post_form_data, + test_inline_objects, + token_with_cookie_auth_token_with_cookie_get, + unsupported_content_tests_unsupported_content_get, + upload_file_tests_upload_post, +) + + +class TestsEndpoints: + @classmethod + def get_user_list(cls) -> types.ModuleType: + """ + Get a list of things + """ + return get_user_list + + @classmethod + def get_basic_list_of_strings(cls) -> types.ModuleType: + """ + Get a list of strings + """ + return get_basic_list_of_strings + + @classmethod + def get_basic_list_of_integers(cls) -> types.ModuleType: + """ + Get a list of integers + """ + return get_basic_list_of_integers + + @classmethod + def get_basic_list_of_floats(cls) -> types.ModuleType: + """ + Get a list of floats + """ + return get_basic_list_of_floats + + @classmethod + def get_basic_list_of_booleans(cls) -> types.ModuleType: + """ + Get a list of booleans + """ + return get_basic_list_of_booleans + + @classmethod + def post_form_data(cls) -> types.ModuleType: + """ + Post form data + """ + return post_form_data + + @classmethod + def upload_file_tests_upload_post(cls) -> types.ModuleType: + """ + Upload a file + """ + return upload_file_tests_upload_post + + @classmethod + def json_body_tests_json_body_post(cls) -> types.ModuleType: + """ + Try sending a JSON body + """ + return json_body_tests_json_body_post + + @classmethod + def defaults_tests_defaults_post(cls) -> types.ModuleType: + """ + Defaults + """ + return defaults_tests_defaults_post + + @classmethod + def octet_stream_tests_octet_stream_get(cls) -> types.ModuleType: + """ + Octet Stream + """ + return octet_stream_tests_octet_stream_get + + @classmethod + def no_response_tests_no_response_get(cls) -> types.ModuleType: + """ + No Response + """ + return no_response_tests_no_response_get + + @classmethod + def unsupported_content_tests_unsupported_content_get(cls) -> types.ModuleType: + """ + Unsupported Content + """ + return unsupported_content_tests_unsupported_content_get + + @classmethod + def int_enum_tests_int_enum_post(cls) -> types.ModuleType: + """ + Int Enum + """ + return int_enum_tests_int_enum_post + + @classmethod + def test_inline_objects(cls) -> types.ModuleType: + """ + Test Inline Objects + """ + return test_inline_objects + + @classmethod + def optional_value_tests_optional_query_param(cls) -> types.ModuleType: + """ + Test optional query parameters + """ + return optional_value_tests_optional_query_param + + @classmethod + def token_with_cookie_auth_token_with_cookie_get(cls) -> types.ModuleType: + """ + Test optional cookie parameters + """ + return token_with_cookie_auth_token_with_cookie_get 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 7148ce5f3..4b2d294cb 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 @@ -5,7 +5,7 @@ from ...client import Client from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, File, Response, Unset +from ...types import UNSET, Response, Unset def _get_kwargs( @@ -22,21 +22,14 @@ def _get_kwargs( if keep_alive is not UNSET: headers["keep-alive"] = keep_alive - files = {} - data = {} - for key, value in multipart_data.to_dict().items(): - if isinstance(value, File): - files[key] = value - else: - data[key] = value + multipart_multipart_data = multipart_data.to_multipart() return { "url": url, "headers": headers, "cookies": cookies, "timeout": client.get_timeout(), - "files": files, - "data": data, + "files": multipart_multipart_data, } 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 0f7516048..c71152ef6 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 @@ -9,6 +9,10 @@ from .an_int_enum import AnIntEnum from .another_all_of_sub_model import AnotherAllOfSubModel from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost +from .body_upload_file_tests_upload_post_additional_property import BodyUploadFileTestsUploadPostAdditionalProperty +from .body_upload_file_tests_upload_post_some_nullable_object import BodyUploadFileTestsUploadPostSomeNullableObject +from .body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject +from .body_upload_file_tests_upload_post_some_optional_object import BodyUploadFileTestsUploadPostSomeOptionalObject from .different_enum import DifferentEnum from .free_form_model import FreeFormModel from .http_validation_error import HTTPValidationError diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py index 797b57b38..c71bf8dcf 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py @@ -19,8 +19,8 @@ class AModelWithPropertiesReferenceThatAreNotObject: str_properties_ref: List[str] date_properties_ref: List[datetime.date] datetime_properties_ref: List[datetime.datetime] - int_32_properties_ref: List[int] - int_64_properties_ref: List[int] + int32_properties_ref: List[int] + int64_properties_ref: List[int] float_properties_ref: List[float] double_properties_ref: List[float] file_properties_ref: List[File] @@ -29,8 +29,8 @@ class AModelWithPropertiesReferenceThatAreNotObject: str_properties: List[str] date_properties: List[datetime.date] datetime_properties: List[datetime.datetime] - int_32_properties: List[int] - int_64_properties: List[int] + int32_properties: List[int] + int64_properties: List[int] float_properties: List[float] double_properties: List[float] file_properties: List[File] @@ -39,8 +39,8 @@ class AModelWithPropertiesReferenceThatAreNotObject: str_property_ref: str date_property_ref: datetime.date datetime_property_ref: datetime.datetime - int_32_property_ref: int - int_64_property_ref: int + int32_property_ref: int + int64_property_ref: int float_property_ref: float double_property_ref: float file_property_ref: File @@ -71,9 +71,9 @@ def to_dict(self) -> Dict[str, Any]: datetime_properties_ref.append(componentsschemas_an_other_array_of_date_time_item) - int_32_properties_ref = self.int_32_properties_ref + int32_properties_ref = self.int32_properties_ref - int_64_properties_ref = self.int_64_properties_ref + int64_properties_ref = self.int64_properties_ref float_properties_ref = self.float_properties_ref @@ -108,9 +108,9 @@ def to_dict(self) -> Dict[str, Any]: datetime_properties.append(componentsschemas_an_array_of_date_time_item) - int_32_properties = self.int_32_properties + int32_properties = self.int32_properties - int_64_properties = self.int_64_properties + int64_properties = self.int64_properties float_properties = self.float_properties @@ -130,8 +130,8 @@ def to_dict(self) -> Dict[str, Any]: date_property_ref = self.date_property_ref.isoformat() datetime_property_ref = self.datetime_property_ref.isoformat() - int_32_property_ref = self.int_32_property_ref - int_64_property_ref = self.int_64_property_ref + int32_property_ref = self.int32_property_ref + int64_property_ref = self.int64_property_ref float_property_ref = self.float_property_ref double_property_ref = self.double_property_ref file_property_ref = self.file_property_ref.to_tuple() @@ -146,8 +146,8 @@ def to_dict(self) -> Dict[str, Any]: "str_properties_ref": str_properties_ref, "date_properties_ref": date_properties_ref, "datetime_properties_ref": datetime_properties_ref, - "int32_properties_ref": int_32_properties_ref, - "int64_properties_ref": int_64_properties_ref, + "int32_properties_ref": int32_properties_ref, + "int64_properties_ref": int64_properties_ref, "float_properties_ref": float_properties_ref, "double_properties_ref": double_properties_ref, "file_properties_ref": file_properties_ref, @@ -156,8 +156,8 @@ def to_dict(self) -> Dict[str, Any]: "str_properties": str_properties, "date_properties": date_properties, "datetime_properties": datetime_properties, - "int32_properties": int_32_properties, - "int64_properties": int_64_properties, + "int32_properties": int32_properties, + "int64_properties": int64_properties, "float_properties": float_properties, "double_properties": double_properties, "file_properties": file_properties, @@ -166,8 +166,8 @@ def to_dict(self) -> Dict[str, Any]: "str_property_ref": str_property_ref, "date_property_ref": date_property_ref, "datetime_property_ref": datetime_property_ref, - "int32_property_ref": int_32_property_ref, - "int64_property_ref": int_64_property_ref, + "int32_property_ref": int32_property_ref, + "int64_property_ref": int64_property_ref, "float_property_ref": float_property_ref, "double_property_ref": double_property_ref, "file_property_ref": file_property_ref, @@ -207,9 +207,9 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: datetime_properties_ref.append(componentsschemas_an_other_array_of_date_time_item) - int_32_properties_ref = cast(List[int], d.pop("int32_properties_ref")) + int32_properties_ref = cast(List[int], d.pop("int32_properties_ref")) - int_64_properties_ref = cast(List[int], d.pop("int64_properties_ref")) + int64_properties_ref = cast(List[int], d.pop("int64_properties_ref")) float_properties_ref = cast(List[float], d.pop("float_properties_ref")) @@ -249,9 +249,9 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: datetime_properties.append(componentsschemas_an_array_of_date_time_item) - int_32_properties = cast(List[int], d.pop("int32_properties")) + int32_properties = cast(List[int], d.pop("int32_properties")) - int_64_properties = cast(List[int], d.pop("int64_properties")) + int64_properties = cast(List[int], d.pop("int64_properties")) float_properties = cast(List[float], d.pop("float_properties")) @@ -276,9 +276,9 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: datetime_property_ref = isoparse(d.pop("datetime_property_ref")) - int_32_property_ref = d.pop("int32_property_ref") + int32_property_ref = d.pop("int32_property_ref") - int_64_property_ref = d.pop("int64_property_ref") + int64_property_ref = d.pop("int64_property_ref") float_property_ref = d.pop("float_property_ref") @@ -293,8 +293,8 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: str_properties_ref=str_properties_ref, date_properties_ref=date_properties_ref, datetime_properties_ref=datetime_properties_ref, - int_32_properties_ref=int_32_properties_ref, - int_64_properties_ref=int_64_properties_ref, + int32_properties_ref=int32_properties_ref, + int64_properties_ref=int64_properties_ref, float_properties_ref=float_properties_ref, double_properties_ref=double_properties_ref, file_properties_ref=file_properties_ref, @@ -303,8 +303,8 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: str_properties=str_properties, date_properties=date_properties, datetime_properties=datetime_properties, - int_32_properties=int_32_properties, - int_64_properties=int_64_properties, + int32_properties=int32_properties, + int64_properties=int64_properties, float_properties=float_properties, double_properties=double_properties, file_properties=file_properties, @@ -313,8 +313,8 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: str_property_ref=str_property_ref, date_property_ref=date_property_ref, datetime_property_ref=datetime_property_ref, - int_32_property_ref=int_32_property_ref, - int_64_property_ref=int_64_property_ref, + int32_property_ref=int32_property_ref, + int64_property_ref=int64_property_ref, float_property_ref=float_property_ref, double_property_ref=double_property_ref, file_property_ref=file_property_ref, 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 d2d263353..683025d4e 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 @@ -1,9 +1,21 @@ +import json from io import BytesIO -from typing import Any, Dict, Type, TypeVar, Union +from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union, cast import attr -from ..types import UNSET, File, Unset +from ..models.body_upload_file_tests_upload_post_additional_property import ( + BodyUploadFileTestsUploadPostAdditionalProperty, +) +from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( + BodyUploadFileTestsUploadPostSomeNullableObject, +) +from ..models.body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject +from ..models.body_upload_file_tests_upload_post_some_optional_object import ( + BodyUploadFileTestsUploadPostSomeOptionalObject, +) +from ..models.different_enum import DifferentEnum +from ..types import UNSET, File, FileJsonType, Unset T = TypeVar("T", bound="BodyUploadFileTestsUploadPost") @@ -13,21 +25,122 @@ class BodyUploadFileTestsUploadPost: """ """ some_file: File + some_object: BodyUploadFileTestsUploadPostSomeObject + some_nullable_object: Optional[BodyUploadFileTestsUploadPostSomeNullableObject] + some_optional_file: Union[Unset, File] = UNSET some_string: Union[Unset, str] = "some_default_string" + some_number: Union[Unset, float] = UNSET + some_array: Union[Unset, List[float]] = UNSET + some_optional_object: Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject] = UNSET + some_enum: Union[Unset, DifferentEnum] = UNSET + additional_properties: Dict[str, BodyUploadFileTestsUploadPostAdditionalProperty] = attr.ib( + init=False, factory=dict + ) def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() + some_object = self.some_object.to_dict() + + some_optional_file: Union[Unset, FileJsonType] = UNSET + if not isinstance(self.some_optional_file, Unset): + some_optional_file = self.some_optional_file.to_tuple() + some_string = self.some_string + some_number = self.some_number + some_array: Union[Unset, List[float]] = UNSET + if not isinstance(self.some_array, Unset): + some_array = self.some_array + + some_optional_object: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.some_optional_object, Unset): + some_optional_object = self.some_optional_object.to_dict() + + some_nullable_object = self.some_nullable_object.to_dict() if self.some_nullable_object else None + + some_enum: Union[Unset, str] = UNSET + if not isinstance(self.some_enum, Unset): + some_enum = self.some_enum.value 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, + "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 some_number is not UNSET: + field_dict["some_number"] = some_number + if some_array is not UNSET: + field_dict["some_array"] = some_array + if some_optional_object is not UNSET: + field_dict["some_optional_object"] = some_optional_object + if some_enum is not UNSET: + field_dict["some_enum"] = some_enum + + return field_dict + + def to_multipart(self) -> Dict[str, Any]: + some_file = self.some_file.to_tuple() + + some_object = (None, json.dumps(self.some_object.to_dict()), "application/json") + + some_optional_file: Union[Unset, FileJsonType] = UNSET + if not isinstance(self.some_optional_file, Unset): + some_optional_file = self.some_optional_file.to_tuple() + + some_string = self.some_string if self.some_string is UNSET else (None, str(self.some_string), "text/plain") + some_number = self.some_number if self.some_number is UNSET else (None, str(self.some_number), "text/plain") + some_array: Union[Unset, Tuple[None, str, str]] = UNSET + if not isinstance(self.some_array, Unset): + _temp_some_array = self.some_array + some_array = (None, json.dumps(_temp_some_array), "application/json") + + some_optional_object: Union[Unset, Tuple[None, str, str]] = UNSET + if not isinstance(self.some_optional_object, Unset): + some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()), "application/json") + + some_nullable_object = ( + (None, json.dumps(self.some_nullable_object.to_dict()), "application/json") + if self.some_nullable_object + else None + ) + + some_enum: Union[Unset, Tuple[None, str, str]] = UNSET + if not isinstance(self.some_enum, Unset): + some_enum = (None, str(self.some_enum.value), "text/plain") + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = (None, json.dumps(prop.to_dict()), "application/json") + + field_dict.update( + { + "some_file": some_file, + "some_object": some_object, + "some_nullable_object": some_nullable_object, + } + ) + if some_optional_file is not UNSET: + field_dict["some_optional_file"] = some_optional_file + if some_string is not UNSET: + field_dict["some_string"] = some_string + if some_number is not UNSET: + field_dict["some_number"] = some_number + if some_array is not UNSET: + field_dict["some_array"] = some_array + if some_optional_object is not UNSET: + field_dict["some_optional_object"] = some_optional_object + if some_enum is not UNSET: + field_dict["some_enum"] = some_enum return field_dict @@ -36,11 +149,75 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: d = src_dict.copy() some_file = File(payload=BytesIO(d.pop("some_file"))) + some_object = BodyUploadFileTestsUploadPostSomeObject.from_dict(d.pop("some_object")) + + _some_optional_file = d.pop("some_optional_file", UNSET) + some_optional_file: Union[Unset, File] + if isinstance(_some_optional_file, Unset): + some_optional_file = UNSET + else: + some_optional_file = File(payload=BytesIO(_some_optional_file)) + some_string = d.pop("some_string", UNSET) + some_number = d.pop("some_number", UNSET) + + some_array = cast(List[float], d.pop("some_array", UNSET)) + + _some_optional_object = d.pop("some_optional_object", UNSET) + some_optional_object: Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject] + if isinstance(_some_optional_object, Unset): + some_optional_object = UNSET + else: + some_optional_object = BodyUploadFileTestsUploadPostSomeOptionalObject.from_dict(_some_optional_object) + + _some_nullable_object = d.pop("some_nullable_object") + some_nullable_object: Optional[BodyUploadFileTestsUploadPostSomeNullableObject] + if _some_nullable_object is None: + some_nullable_object = None + else: + some_nullable_object = BodyUploadFileTestsUploadPostSomeNullableObject.from_dict(_some_nullable_object) + + _some_enum = d.pop("some_enum", UNSET) + some_enum: Union[Unset, DifferentEnum] + if isinstance(_some_enum, Unset): + some_enum = UNSET + else: + some_enum = DifferentEnum(_some_enum) + body_upload_file_tests_upload_post = cls( some_file=some_file, + some_object=some_object, + some_optional_file=some_optional_file, some_string=some_string, + some_number=some_number, + some_array=some_array, + some_optional_object=some_optional_object, + some_nullable_object=some_nullable_object, + some_enum=some_enum, ) + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = BodyUploadFileTestsUploadPostAdditionalProperty.from_dict(prop_dict) + + additional_properties[prop_name] = additional_property + + body_upload_file_tests_upload_post.additional_properties = additional_properties return body_upload_file_tests_upload_post + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> BodyUploadFileTestsUploadPostAdditionalProperty: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: BodyUploadFileTestsUploadPostAdditionalProperty) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py new file mode 100644 index 000000000..b2ce8457e --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BodyUploadFileTestsUploadPostAdditionalProperty") + + +@attr.s(auto_attribs=True) +class BodyUploadFileTestsUploadPostAdditionalProperty: + """ """ + + foo: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + foo = self.foo + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if foo is not UNSET: + field_dict["foo"] = foo + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + foo = d.pop("foo", UNSET) + + body_upload_file_tests_upload_post_additional_property = cls( + foo=foo, + ) + + body_upload_file_tests_upload_post_additional_property.additional_properties = d + return body_upload_file_tests_upload_post_additional_property + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py new file mode 100644 index 000000000..f97e865aa --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeNullableObject") + + +@attr.s(auto_attribs=True) +class BodyUploadFileTestsUploadPostSomeNullableObject: + """ """ + + bar: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + bar = self.bar + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if bar is not UNSET: + field_dict["bar"] = bar + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + bar = d.pop("bar", UNSET) + + body_upload_file_tests_upload_post_some_nullable_object = cls( + bar=bar, + ) + + body_upload_file_tests_upload_post_some_nullable_object.additional_properties = d + return body_upload_file_tests_upload_post_some_nullable_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/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py new file mode 100644 index 000000000..85eaba04e --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py @@ -0,0 +1,60 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeObject") + + +@attr.s(auto_attribs=True) +class BodyUploadFileTestsUploadPostSomeObject: + """ """ + + num: float + text: str + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + num = self.num + text = self.text + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "num": num, + "text": text, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + num = d.pop("num") + + text = d.pop("text") + + body_upload_file_tests_upload_post_some_object = cls( + num=num, + text=text, + ) + + body_upload_file_tests_upload_post_some_object.additional_properties = d + return body_upload_file_tests_upload_post_some_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/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py new file mode 100644 index 000000000..f983f83f4 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeOptionalObject") + + +@attr.s(auto_attribs=True) +class BodyUploadFileTestsUploadPostSomeOptionalObject: + """ """ + + foo: str + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + foo = self.foo + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "foo": foo, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + foo = d.pop("foo") + + body_upload_file_tests_upload_post_some_optional_object = cls( + foo=foo, + ) + + body_upload_file_tests_upload_post_some_optional_object.additional_properties = d + return body_upload_file_tests_upload_post_some_optional_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/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 2b1cfc5b8..a6f00ece9 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 @@ -40,4 +40,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["File", "Response"] +__all__ = ["File", "Response", "FileJsonType"] diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index bbd227358..4d40f108e 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1053,9 +1053,7 @@ }, "Body_upload_file_tests_upload_post": { "title": "Body_upload_file_tests_upload_post", - "required": [ - "some_file" - ], + "required": ["some_file", "some_object", "some_nullable_object"], "type": "object", "properties": { "some_file": { @@ -1063,13 +1061,72 @@ "type": "string", "format": "binary" }, + "some_optional_file": { + "title": "Some Optional File", + "type": "string", + "format": "binary" + }, "some_string": { "title": "Some String", "type": "string", "default": "some_default_string" + }, + "some_number": { + "title": "Some Number", + "type": "number" + }, + "some_array": { + "title": "Some Array", + "type": "array", + "items": { + "type": "number" + } + }, + "some_object": { + "title": "Some Object", + "type": "object", + "required": ["num", "text"], + "properties": { + "num": { + "type": "number" + }, + "text": { + "type": "string" + } + } + }, + "some_optional_object": { + "title": "Some Optional Object", + "type": "object", + "required": ["foo"], + "properties": { + "foo": { + "type": "string" + } + } + }, + "some_nullable_object": { + "title": "Some Nullable Object", + "type": "object", + "nullable": true, + "properties": { + "bar": { + "type": "string" + } + } + }, + "some_enum": { + "$ref": "#/components/schemas/DifferentEnum" } }, - "additionalProperties": false + "additionalProperties": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } }, "DifferentEnum": { "title": "DifferentEnum", diff --git a/end_to_end_tests/regen_golden_record.py b/end_to_end_tests/regen_golden_record.py index aaa6aa850..1d4dc943d 100644 --- a/end_to_end_tests/regen_golden_record.py +++ b/end_to_end_tests/regen_golden_record.py @@ -1,12 +1,16 @@ """ Regenerate golden-record """ +import filecmp +import os import shutil +import tempfile from pathlib import Path from typer.testing import CliRunner from openapi_python_client.cli import app -if __name__ == "__main__": + +def regen_golden_record(): runner = CliRunner() openapi_path = Path(__file__).parent / "openapi.json" @@ -24,3 +28,52 @@ if result.exception: raise result.exception output_path.rename(gr_path) + + +def regen_custom_template_golden_record(): + runner = CliRunner() + openapi_path = Path(__file__).parent / "openapi.json" + tpl_dir = Path(__file__).parent / "test_custom_templates" + + gr_path = Path(__file__).parent / "golden-record" + tpl_gr_path = Path(__file__).parent / "custom-templates-golden-record" + + output_path = Path(tempfile.mkdtemp()) + config_path = Path(__file__).parent / "config.yml" + + shutil.rmtree(tpl_gr_path, ignore_errors=True) + + os.chdir(str(output_path.absolute())) + result = runner.invoke( + app, ["generate", f"--config={config_path}", f"--path={openapi_path}", f"--custom-template-path={tpl_dir}"] + ) + + if result.stdout: + generated_output_path = output_path / "my-test-api-client" + for f in generated_output_path.glob("**/*"): # nb: works for Windows and Unix + relative_to_generated = f.relative_to(generated_output_path) + gr_file = gr_path / relative_to_generated + if not gr_file.exists(): + print(f"{gr_file} does not exist, ignoring") + continue + + if not gr_file.is_file(): + continue + + if not filecmp.cmp(gr_file, f, shallow=False): + target_file = tpl_gr_path / relative_to_generated + target_dir = target_file.parent + + target_dir.mkdir(parents=True, exist_ok=True) + shutil.copy(f"{f}", f"{target_file}") + + shutil.rmtree(output_path, ignore_errors=True) + + if result.exception: + shutil.rmtree(output_path, ignore_errors=True) + raise result.exception + + +if __name__ == "__main__": + regen_golden_record() + regen_custom_template_golden_record() diff --git a/end_to_end_tests/test_custom_templates/api_init.py.jinja b/end_to_end_tests/test_custom_templates/api_init.py.jinja new file mode 100644 index 000000000..03c2a2f6f --- /dev/null +++ b/end_to_end_tests/test_custom_templates/api_init.py.jinja @@ -0,0 +1,13 @@ +""" Contains methods for accessing the API """ + +from typing import Type +{% for tag in endpoint_collections_by_tag.keys() %} +from {{ package_name }}.api.{{ tag }} import {{ utils.pascal_case(tag) }}Endpoints +{% endfor %} + +class {{ utils.pascal_case(package_name) }}Api: +{% for tag in endpoint_collections_by_tag.keys() %} + @classmethod + def {{ tag }}(cls) -> Type[{{ utils.pascal_case(tag) }}Endpoints]: + return {{ utils.pascal_case(tag) }}Endpoints +{% endfor %} diff --git a/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja new file mode 100644 index 000000000..57e8ba124 --- /dev/null +++ b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja @@ -0,0 +1,24 @@ +""" Contains methods for accessing the API Endpoints """ + +import types +{% for endpoint in endpoint_collection.endpoints %} +from {{ package_name }}.api.{{ endpoint_collection.tag }} import {{ utils.snake_case(endpoint.name) }} +{% endfor %} + +class {{ utils.pascal_case(endpoint_collection.tag) }}Endpoints: + +{% for endpoint in endpoint_collection.endpoints %} + + @classmethod + def {{ utils.snake_case(endpoint.name) }}(cls) -> types.ModuleType: + {% if endpoint.description %} + """ + {{ endpoint.description }} + """ + {% elif endpoint.summary %} + """ + {{ endpoint.summary }} + """ + {% endif %} + return {{ utils.snake_case(endpoint.name) }} +{% endfor %} diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index fa4d21598..bcc8b12e1 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -1,7 +1,7 @@ import shutil from filecmp import cmpfiles, dircmp from pathlib import Path -from typing import Dict, Optional +from typing import Dict, List, Optional import pytest from typer.testing import CliRunner @@ -12,8 +12,18 @@ def _compare_directories( record: Path, test_subject: Path, - expected_differences: Optional[Dict[str, str]] = None, + expected_differences: Dict[Path, str], + depth=0, ): + """ + Compare two directories and assert that only expected_differences are different + + Args: + record: Path to the expected output + test_subject: Path to the generated code being checked + expected_differences: key: path relative to generated directory, value: expected generated content + depth: Used to track recursion + """ first_printable = record.relative_to(Path.cwd()) second_printable = test_subject.relative_to(Path.cwd()) dc = dircmp(record, test_subject) @@ -22,30 +32,42 @@ def _compare_directories( pytest.fail(f"{first_printable} or {second_printable} was missing: {missing_files}", pytrace=False) expected_differences = expected_differences or {} - _, mismatch, errors = cmpfiles(record, test_subject, dc.common_files, shallow=False) - mismatch = set(mismatch) - - for file_name in mismatch | set(expected_differences.keys()): - if file_name not in expected_differences: + _, mismatches, errors = cmpfiles(record, test_subject, dc.common_files, shallow=False) + mismatches = set(mismatches) + + expected_path_mismatches = [] + for file_name in mismatches: + mismatch_file_path = test_subject.joinpath(file_name) + expected_content = expected_differences.get(mismatch_file_path) + if expected_content is None: continue - if file_name not in mismatch: - pytest.fail(f"Expected {file_name} to be different but it was not", pytrace=False) - generated = (test_subject / file_name).read_text() - assert generated == expected_differences[file_name], f"Unexpected output in {file_name}" - del expected_differences[file_name] - mismatch.remove(file_name) - - if mismatch: + + generated_content = (test_subject / file_name).read_text() + assert generated_content == expected_content, f"Unexpected output in {mismatch_file_path}" + expected_path_mismatches.append(mismatch_file_path) + + for path_mismatch in expected_path_mismatches: + matched_file_name = path_mismatch.name + mismatches.remove(matched_file_name) + del expected_differences[path_mismatch] + + if mismatches: pytest.fail( - f"{first_printable} and {second_printable} had differing files: {mismatch}, and errors {errors}", + f"{first_printable} and {second_printable} had differing files: {mismatches}, and errors {errors}", pytrace=False, ) for sub_path in dc.common_dirs: - _compare_directories(record / sub_path, test_subject / sub_path, expected_differences=expected_differences) + _compare_directories( + record / sub_path, test_subject / sub_path, expected_differences=expected_differences, depth=depth + 1 + ) + + if depth == 0 and len(expected_differences.keys()) > 0: + failure = "\n".join([f"Expected {path} to be different but it was not" for path in expected_differences.keys()]) + pytest.fail(failure, pytrace=False) -def run_e2e_test(extra_args=None, expected_differences=None): +def run_e2e_test(extra_args: List[str], expected_differences: Dict[Path, str]): runner = CliRunner() openapi_path = Path(__file__).parent / "openapi.json" config_path = Path(__file__).parent / "config.yml" @@ -60,6 +82,9 @@ def run_e2e_test(extra_args=None, expected_differences=None): if result.exit_code != 0: raise result.exception + + # Use absolute paths for expected differences for easier comparisons + expected_differences = {output_path.joinpath(key): value for key, value in expected_differences.items()} _compare_directories(gr_path, output_path, expected_differences=expected_differences) import mypy.api @@ -71,11 +96,24 @@ def run_e2e_test(extra_args=None, expected_differences=None): def test_end_to_end(): - run_e2e_test() + run_e2e_test([], {}) def test_custom_templates(): + expected_differences = {} # key: path relative to generated directory, value: expected generated content + expected_difference_paths = [ + Path("README.md"), + Path("my_test_api_client").joinpath("api", "__init__.py"), + Path("my_test_api_client").joinpath("api", "tests", "__init__.py"), + Path("my_test_api_client").joinpath("api", "default", "__init__.py"), + Path("my_test_api_client").joinpath("api", "parameters", "__init__.py"), + ] + + golden_tpls_root_dir = Path(__file__).parent.joinpath("custom-templates-golden-record") + for expected_difference_path in expected_difference_paths: + expected_differences[expected_difference_path] = (golden_tpls_root_dir / expected_difference_path).read_text() + run_e2e_test( - extra_args=["--custom-template-path=end_to_end_tests/test_custom_templates"], - expected_differences={"README.md": "my-test-api-client"}, + extra_args=["--custom-template-path=end_to_end_tests/test_custom_templates/"], + expected_differences=expected_differences, ) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 2a7cf574b..b1458e1a4 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -81,6 +81,15 @@ def __init__( self.version: str = config.package_version_override or openapi.version self.env.filters.update(TEMPLATE_FILTERS) + self.env.globals.update( + utils=utils, + package_name=self.package_name, + package_dir=self.package_dir, + package_description=self.package_description, + package_version=self.version, + project_name=self.project_name, + project_dir=self.project_dir, + ) def build(self) -> Sequence[GeneratorError]: """Create the project from templates""" @@ -143,9 +152,7 @@ def _create_package(self) -> None: package_init = self.package_dir / "__init__.py" package_init_template = self.env.get_template("package_init.py.jinja") - package_init.write_text( - package_init_template.render(description=self.package_description), encoding=self.file_encoding - ) + package_init.write_text(package_init_template.render(), encoding=self.file_encoding) if self.meta != MetaType.NONE: pytyped = self.package_dir / "py.typed" @@ -167,9 +174,7 @@ def _build_metadata(self) -> None: readme = self.project_dir / "README.md" readme_template = self.env.get_template("README.md.jinja") readme.write_text( - readme_template.render( - project_name=self.project_name, description=self.package_description, package_name=self.package_name - ), + readme_template.render(), encoding=self.file_encoding, ) @@ -183,12 +188,7 @@ def _build_pyproject_toml(self, *, use_poetry: bool) -> None: pyproject_template = self.env.get_template(template) pyproject_path = self.project_dir / "pyproject.toml" pyproject_path.write_text( - pyproject_template.render( - project_name=self.project_name, - package_name=self.package_name, - version=self.version, - description=self.package_description, - ), + pyproject_template.render(), encoding=self.file_encoding, ) @@ -196,12 +196,7 @@ def _build_setup_py(self) -> None: template = self.env.get_template("setup.py.jinja") path = self.project_dir / "setup.py" path.write_text( - template.render( - project_name=self.project_name, - package_name=self.package_name, - version=self.version, - description=self.package_description, - ), + template.render(), encoding=self.file_encoding, ) @@ -239,16 +234,29 @@ def _build_api(self) -> None: client_path.write_text(client_template.render(), encoding=self.file_encoding) # Generate endpoints + endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag api_dir = self.package_dir / "api" api_dir.mkdir() - api_init = api_dir / "__init__.py" - api_init.write_text('""" Contains methods for accessing the API """', encoding=self.file_encoding) + api_init_path = api_dir / "__init__.py" + api_init_template = self.env.get_template("api_init.py.jinja") + api_init_path.write_text( + api_init_template.render( + endpoint_collections_by_tag=endpoint_collections_by_tag, + ), + encoding=self.file_encoding, + ) endpoint_template = self.env.get_template("endpoint_module.py.jinja") - for tag, collection in self.openapi.endpoint_collections_by_tag.items(): + for tag, collection in endpoint_collections_by_tag.items(): tag_dir = api_dir / tag tag_dir.mkdir() - (tag_dir / "__init__.py").touch() + + endpoint_init_path = tag_dir / "__init__.py" + endpoint_init_template = self.env.get_template("endpoint_init.py.jinja") + endpoint_init_path.write_text( + endpoint_init_template.render(endpoint_collection=collection), + encoding=self.file_encoding, + ) for endpoint in collection.endpoints: module_path = tag_dir / f"{snake_case(endpoint.name)}.py" diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index f0b9774ce..a1bd5489b 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union +import attr from pydantic import ValidationError from .. import schema as oai @@ -94,7 +95,7 @@ class Endpoint: responses: List[Response] = field(default_factory=list) form_body_class: Optional[Class] = None json_body: Optional[Property] = None - multipart_body_class: Optional[Class] = None + multipart_body: Optional[Property] = None errors: List[ParseError] = field(default_factory=list) @staticmethod @@ -107,13 +108,26 @@ def parse_request_form_body(*, body: oai.RequestBody, config: Config) -> Optiona return None @staticmethod - def parse_multipart_body(*, body: oai.RequestBody, config: Config) -> Optional[Class]: - """Return form_body_reference""" + def parse_multipart_body( + *, body: oai.RequestBody, schemas: Schemas, parent_name: str, config: Config + ) -> Tuple[Union[Property, PropertyError, None], Schemas]: + """Return multipart_body""" body_content = body.content - json_body = body_content.get("multipart/form-data") - if json_body is not None and isinstance(json_body.media_type_schema, oai.Reference): - return Class.from_string(string=json_body.media_type_schema.ref, config=config) - return None + multipart_body = body_content.get("multipart/form-data") + if multipart_body is not None and multipart_body.media_type_schema is not None: + prop, schemas = property_from_data( + name="multipart_data", + required=True, + data=multipart_body.media_type_schema, + schemas=schemas, + parent_name=parent_name, + config=config, + ) + if isinstance(prop, ModelProperty): + prop = attr.evolve(prop, is_multipart_body=True) + schemas = attr.evolve(schemas, classes_by_name={**schemas.classes_by_name, prop.class_info.name: prop}) + return prop, schemas + return None, schemas @staticmethod def parse_request_json_body( @@ -153,19 +167,31 @@ def _add_body( if isinstance(json_body, ParseError): return ( ParseError( - header=f"Cannot parse body of endpoint {endpoint.name}", + header=f"Cannot parse JSON body of endpoint {endpoint.name}", detail=json_body.detail, data=json_body.data, ), schemas, ) - endpoint.multipart_body_class = Endpoint.parse_multipart_body(body=data.requestBody, config=config) + multipart_body, schemas = Endpoint.parse_multipart_body( + body=data.requestBody, schemas=schemas, parent_name=endpoint.name, config=config + ) + if isinstance(multipart_body, ParseError): + return ( + ParseError( + header=f"Cannot parse multipart body of endpoint {endpoint.name}", + detail=multipart_body.detail, + data=multipart_body.data, + ), + schemas, + ) if endpoint.form_body_class: endpoint.relative_imports.add(import_string_from_class(endpoint.form_body_class, prefix="...models")) - if endpoint.multipart_body_class: - endpoint.relative_imports.add(import_string_from_class(endpoint.multipart_body_class, prefix="...models")) + if multipart_body is not None: + endpoint.multipart_body = multipart_body + endpoint.relative_imports.update(endpoint.multipart_body.get_imports(prefix="...")) if json_body is not None: endpoint.json_body = json_body endpoint.relative_imports.update(endpoint.json_body.get_imports(prefix="...")) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 908745a6a..243554fb6 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -94,7 +94,7 @@ class FileProperty(Property): _type_string: ClassVar[str] = "File" # Return type of File.to_tuple() - _json_type_string: ClassVar[str] = "Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]]" + _json_type_string: ClassVar[str] = "FileJsonType" template: ClassVar[str] = "file_property.py.jinja" def get_imports(self, *, prefix: str) -> Set[str]: @@ -106,7 +106,7 @@ 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", "from io import BytesIO"}) + imports.update({f"from {prefix}types import File, FileJsonType", "from io import BytesIO"}) return imports @@ -354,7 +354,7 @@ def build_union_property( sub_properties: List[Property] = [] for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf)): sub_prop, schemas = property_from_data( - name=f"{name}_type{i}", + name=f"{name}_type_{i}", required=required, data=sub_prop_data, schemas=schemas, diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index a40460886..79ac48764 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -25,6 +25,7 @@ class ModelProperty(Property): template: ClassVar[str] = "model_property.py.jinja" json_is_dict: ClassVar[bool] = True + is_multipart_body: bool = False def get_base_type_string(self, json: bool = False) -> str: return self.class_info.name diff --git a/openapi_python_client/templates/README.md.jinja b/openapi_python_client/templates/README.md.jinja index 2a5d18d87..e6de0dda5 100644 --- a/openapi_python_client/templates/README.md.jinja +++ b/openapi_python_client/templates/README.md.jinja @@ -1,5 +1,5 @@ # {{ project_name }} -{{ description }} +{{ package_description }} ## Usage First, create a client: diff --git a/openapi_python_client/templates/api_init.py.jinja b/openapi_python_client/templates/api_init.py.jinja new file mode 100644 index 000000000..dc035f4ce --- /dev/null +++ b/openapi_python_client/templates/api_init.py.jinja @@ -0,0 +1 @@ +""" Contains methods for accessing the API """ diff --git a/openapi_python_client/templates/endpoint_init.py.jinja b/openapi_python_client/templates/endpoint_init.py.jinja new file mode 100644 index 000000000..e69de29bb diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 66d6209b3..add4c68b2 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -71,6 +71,17 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None} {% endif %} {% endmacro %} +{% macro multipart_body(endpoint) %} +{% if endpoint.multipart_body %} + {% set property = endpoint.multipart_body %} + {% set destination = "multipart_" + property.python_name %} + {% if property.template %} + {% from "property_templates/" + property.template import transform_multipart %} +{{ transform_multipart(property, property.python_name, destination) }} + {% endif %} +{% endif %} +{% endmacro %} + {# The all the kwargs passed into an endpoint (and variants thereof)) #} {% macro arguments(endpoint) %} *, @@ -89,8 +100,8 @@ client: Client, form_data: {{ endpoint.form_body_class.name }}, {% endif %} {# Multipart data if any #} -{% if endpoint.multipart_body_class %} -multipart_data: {{ endpoint.multipart_body_class.name }}, +{% if endpoint.multipart_body %} +multipart_data: {{ endpoint.multipart_body.get_type_string() }}, {% endif %} {# JSON body if any #} {% if endpoint.json_body %} @@ -118,7 +129,7 @@ client=client, {% if endpoint.form_body_class %} form_data=form_data, {% endif %} -{% if endpoint.multipart_body_class %} +{% if endpoint.multipart_body %} multipart_data=multipart_data, {% endif %} {% if endpoint.json_body %} diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 687705def..a55ea14cd 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -3,13 +3,13 @@ from typing import Any, Dict, List, Optional, Union, cast import httpx from ...client import AuthenticatedClient, Client -from ...types import Response, UNSET{% if endpoint.multipart_body_class %}, File {% endif %} +from ...types import Response, UNSET {% for relative in endpoint.relative_imports %} {{ relative }} {% endfor %} -{% from "endpoint_macros.py.jinja" import header_params, cookie_params, query_params, json_body, arguments, client, kwargs, parse_response %} +{% from "endpoint_macros.py.jinja" import header_params, cookie_params, query_params, json_body, multipart_body, arguments, client, kwargs, parse_response %} {% set return_string = endpoint.response_type() %} {% set parsed_responses = (endpoint.responses | length > 0) and return_string != "None" %} @@ -35,15 +35,7 @@ def _get_kwargs( {{ json_body(endpoint) | indent(4) }} - {% if endpoint.multipart_body_class %} - files = {} - data = {} - for key, value in multipart_data.to_dict().items(): - if isinstance(value, File): - files[key] = value - else: - data[key] = value - {% endif %} + {{ multipart_body(endpoint) | indent(4) }} return { "url": url, @@ -52,9 +44,8 @@ def _get_kwargs( "timeout": client.get_timeout(), {% if endpoint.form_body_class %} "data": form_data.to_dict(), - {% elif endpoint.multipart_body_class %} - "files": files, - "data": data, + {% elif endpoint.multipart_body %} + "files": {{ "multipart_" + endpoint.multipart_body.python_name }}, {% elif endpoint.json_body %} "json": {{ "json_" + endpoint.json_body.python_name }}, {% endif %} diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 0cc98b105..c4c23c878 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -6,6 +6,9 @@ from typing import List {% endif %} import attr +{% if model.is_multipart_body %} +import json +{% endif %} from ..types import UNSET, Unset @@ -40,42 +43,57 @@ class {{ class_name }}: additional_properties: Dict[str, {{ additional_property_type }}] = attr.ib(init=False, factory=dict) {% endif %} +{% macro _to_dict(multipart=False) %} +{% for property in model.required_properties + model.optional_properties %} +{% if property.template %} +{% from "property_templates/" + property.template import transform %} +{{ transform(property, "self." + property.python_name, property.python_name, stringify=multipart) }} +{% elif multipart %} +{{ property.python_name }} = self.{{ property.python_name }} if self.{{ property.python_name }} is UNSET else (None, str(self.{{ property.python_name }}), "text/plain") +{% else %} +{{ property.python_name }} = self.{{ property.python_name }} +{% endif %} +{% endfor %} + +field_dict: Dict[str, Any] = {} +{% if model.additional_properties %} +{% if model.additional_properties.template %} +{% from "property_templates/" + model.additional_properties.template import transform %} +for prop_name, prop in self.additional_properties.items(): + {{ transform(model.additional_properties, "prop", "field_dict[prop_name]", stringify=multipart) | indent(4) }} +{% elif multipart %} +field_dict.update({ + key: (None, str(value), "text/plain") + for key, value in self.additional_properties.items() +}) +{% else %} +field_dict.update(self.additional_properties) +{% endif %} +{% endif %} +field_dict.update({ + {% for property in model.required_properties + model.optional_properties %} + {% if property.required %} + "{{ property.name }}": {{ property.python_name }}, + {% endif %} + {% endfor %} +}) +{% for property in model.optional_properties %} +{% if not property.required %} +if {{ property.python_name }} is not UNSET: + field_dict["{{ property.name }}"] = {{ property.python_name }} +{% endif %} +{% endfor %} + +return field_dict +{% endmacro %} def to_dict(self) -> Dict[str, Any]: - {% for property in model.required_properties + model.optional_properties %} - {% if property.template %} - {% from "property_templates/" + property.template import transform %} - {{ transform(property, "self." + property.python_name, property.python_name) | indent(8) }} - {% else %} - {{ property.python_name }} = self.{{ property.python_name }} - {% endif %} - {% endfor %} - - field_dict: Dict[str, Any] = {} - {% if model.additional_properties %} - {% if model.additional_properties.template %} - {% from "property_templates/" + model.additional_properties.template import transform %} - for prop_name, prop in self.additional_properties.items(): - {{ transform(model.additional_properties, "prop", "field_dict[prop_name]") | indent(12) }} - {% else %} - field_dict.update(self.additional_properties) - {% endif %} - {% endif %} - field_dict.update({ - {% for property in model.required_properties + model.optional_properties %} - {% if property.required %} - "{{ property.name }}": {{ property.python_name }}, - {% endif %} - {% endfor %} - }) - {% for property in model.optional_properties %} - {% if not property.required %} - if {{ property.python_name }} is not UNSET: - field_dict["{{ property.name }}"] = {{ property.python_name }} - {% endif %} - {% endfor %} - - 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) }} +{% endif %} @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: diff --git a/openapi_python_client/templates/package_init.py.jinja b/openapi_python_client/templates/package_init.py.jinja index 917cd7dde..f146549d0 100644 --- a/openapi_python_client/templates/package_init.py.jinja +++ b/openapi_python_client/templates/package_init.py.jinja @@ -1,2 +1,2 @@ -""" {{ description }} """ +""" {{ package_description }} """ from .client import AuthenticatedClient, Client 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 65672d2e7..7c4cebfbd 100644 --- a/openapi_python_client/templates/property_templates/date_property.py.jinja +++ b/openapi_python_client/templates/property_templates/date_property.py.jinja @@ -10,7 +10,7 @@ isoparse({{ source }}).date() {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if property.required %} {{ destination }} = {{ source }}.isoformat() {% if property.nullable %}if {{ source }} else None {%endif%} {% else %} 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 de1e8427f..0984773e0 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.py.jinja +++ b/openapi_python_client/templates/property_templates/datetime_property.py.jinja @@ -10,7 +10,7 @@ isoparse({{ source }}) {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.isoformat() if {{ source }} else None 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 9dd051b38..340d67359 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -10,20 +10,26 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.value_type.__name__ }}){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} +{% set transformed = source + ".value" %} +{% set type_string = property.get_type_string(json=True) %} +{% if stringify %} + {% set transformed = "(None, str(" + transformed + "), 'text/plain')" %} + {% set type_string = "Union[Unset, Tuple[None, str, str]]" %} +{% endif %} {% if property.required %} {% if property.nullable %} -{{ destination }} = {{ source }}.value if {{ source }} else None +{{ destination }} = {{ transformed }} if {{ source }} else None {% else %} -{{ destination }} = {{ source }}.value +{{ destination }} = {{ transformed }} {% endif %} {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): {% if property.nullable %} - {{ destination }} = {{ source }}.value if {{ source }} else None + {{ destination }} = {{ transformed }} if {{ source }} else None {% else %} - {{ destination }} = {{ source }}.value + {{ destination }} = {{ transformed }} {% endif %} {% endif %} {% endmacro %} 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 f8fd0c193..e63cac53d 100644 --- a/openapi_python_client/templates/property_templates/file_property.py.jinja +++ b/openapi_python_client/templates/property_templates/file_property.py.jinja @@ -12,7 +12,7 @@ File( {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, bytes){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None 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 c6ef85254..b955ad40a 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -17,8 +17,12 @@ for {{ inner_source }} in (_{{ property.python_name }} or []): {% endif %} {% endmacro %} -{% macro _transform(property, source, destination) %} +{% macro _transform(property, source, destination, stringify) %} {% set inner_property = property.inner_property %} +{% if stringify %} +{% set stringified_destination = destination %} +{% set destination = "_temp_" + destination %} +{% endif %} {% if inner_property.template %} {% set inner_source = inner_property.python_name + "_data" %} {{ destination }} = [] @@ -29,31 +33,39 @@ for {{ inner_source }} in {{ source }}: {% else %} {{ destination }} = {{ source }} {% endif %} +{% if stringify %} +{{ stringified_destination }} = (None, json.dumps({{ destination }}), 'application/json') +{% endif %} {% endmacro %} {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, list){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% set inner_property = property.inner_property %} +{% if stringify %} + {% set type_string = "Union[Unset, Tuple[None, str, str]]" %} +{% else %} + {% set type_string = property.get_type_string(json=True) %} +{% endif %} {% if property.required %} {% if property.nullable %} if {{ source }} is None: {{ destination }} = None else: - {{ _transform(property, source, destination) | indent(4) }} + {{ _transform(property, source, destination, stringify) | indent(4) }} {% else %} -{{ _transform(property, source, destination) }} +{{ _transform(property, source, destination, stringify) }} {% endif %} {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): {% if property.nullable %} if {{ source }} is None: {{ destination }} = None else: - {{ _transform(property, source, destination) | indent(8)}} + {{ _transform(property, source, destination, stringify) | indent(8)}} {% else %} - {{ _transform(property, source, destination) | indent(4)}} + {{ _transform(property, source, destination, stringify) | indent(4)}} {% endif %} {% endif %} 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 2772918cf..b5b986863 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -10,20 +10,31 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, dict){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False, transform_method="to_dict") %} +{% set transformed = source + "." + transform_method + "()" %} +{% if stringify %} + {% set transformed = "(None, json.dumps(" + transformed + "), 'application/json')" %} + {% set type_string = "Union[Unset, Tuple[None, str, str]]" %} +{% else %} + {% set type_string = property.get_type_string(json=True) %} +{% endif %} {% if property.required %} {% if property.nullable %} -{{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{{ destination }} = {{ transformed }} if {{ source }} else None {% else %} -{{ destination }} = {{ source }}.to_dict() +{{ destination }} = {{ transformed }} {% endif %} {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): {% if property.nullable %} - {{ destination }} = {{ source }}.to_dict() if {{ source }} else None + {{ destination }} = {{ transformed }} if {{ source }} else None {% else %} - {{ destination }} = {{ source }}.to_dict() + {{ destination }} = {{ transformed }} {% endif %} {% endif %} {% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{{ transform(property, source, destination, transform_method="to_multipart") }} +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/none_property.py.jinja b/openapi_python_client/templates/property_templates/none_property.py.jinja index adc6b1524..864802c28 100644 --- a/openapi_python_client/templates/property_templates/none_property.py.jinja +++ b/openapi_python_client/templates/property_templates/none_property.py.jinja @@ -4,6 +4,6 @@ {% macro check_type_for_construct(property, source) %}{{ source }} is None{% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {{ destination }} = None {% 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 87ea9820f..ce988a913 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -37,7 +37,7 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri {# For now we assume there will be no unions of unions #} {% macro check_type_for_construct(property, source) %}True{% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if not property.required or property.nullable %} {{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} @@ -63,7 +63,7 @@ elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): else: {% endif %} {% from "property_templates/" + inner_property.template import transform %} - {{ transform(inner_property, source, destination, declare_type=False) | indent(4) }} + {{ transform(inner_property, source, destination, declare_type=False, stringify=stringify) | indent(4) }} {% endfor %} {% if property.has_properties_without_templates and (property.inner_properties_with_template() | any or not property.required)%} else: diff --git a/openapi_python_client/templates/pyproject.toml.jinja b/openapi_python_client/templates/pyproject.toml.jinja index 9e311a1a8..695092f48 100644 --- a/openapi_python_client/templates/pyproject.toml.jinja +++ b/openapi_python_client/templates/pyproject.toml.jinja @@ -1,7 +1,7 @@ [tool.poetry] name = "{{ project_name }}" -version = "{{ version }}" -description = "{{ description }}" +version = "{{ package_version }}" +description = "{{ package_description }}" authors = [] diff --git a/openapi_python_client/templates/setup.py.jinja b/openapi_python_client/templates/setup.py.jinja index 0dd31d23b..027120ab9 100644 --- a/openapi_python_client/templates/setup.py.jinja +++ b/openapi_python_client/templates/setup.py.jinja @@ -7,8 +7,8 @@ long_description = (here / "README.md").read_text(encoding="utf-8") setup( name="{{ project_name }}", - version="{{ version }}", - description="{{ description }}", + version="{{ package_version }}", + description="{{ package_description }}", long_description=long_description, long_description_content_type="text/markdown", package_dir={"": "{{ package_name }}"}, diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index 116054226..70daf2af4 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -1,5 +1,5 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union +from typing import Any, BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr @@ -41,4 +41,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["File", "Response"] +__all__ = ["File", "Response", "FileJsonType"] diff --git a/openapi_python_client/utils.py b/openapi_python_client/utils.py index 7a7c84185..8c9e436f5 100644 --- a/openapi_python_client/utils.py +++ b/openapi_python_client/utils.py @@ -12,8 +12,10 @@ def sanitize(value: str) -> str: def split_words(value: str) -> List[str]: - """Split a string on non-capital letters and known delimiters""" - value = " ".join(re.split("([A-Z]?[a-z]+)", value)) + """Split a string on words and known delimiters""" + # We can't guess words if there is no capital letter + if any(c.isupper() for c in value): + value = " ".join(re.split("([A-Z]?[a-z]+)", value)) return re.findall(rf"[^{delimiters}]+", value) diff --git a/tests/test___init__.py b/tests/test___init__.py index 0579e83f0..3e1efbd5c 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -403,11 +403,7 @@ def test__build_metadata_poetry(self, mocker): project._build_metadata() project.env.get_template.assert_has_calls([mocker.call("README.md.jinja"), mocker.call(".gitignore.jinja")]) - readme_template.render.assert_called_once_with( - description=project.package_description, - project_name=project.project_name, - package_name=project.package_name, - ) + readme_template.render.assert_called_once_with() readme_path.write_text.assert_called_once_with(readme_template.render(), encoding="utf-8") git_ignore_template.render.assert_called_once() git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render(), encoding="utf-8") @@ -440,11 +436,7 @@ def test__build_metadata_setup(self, mocker): project._build_metadata() project.env.get_template.assert_has_calls([mocker.call("README.md.jinja"), mocker.call(".gitignore.jinja")]) - readme_template.render.assert_called_once_with( - description=project.package_description, - project_name=project.project_name, - package_name=project.package_name, - ) + readme_template.render.assert_called_once_with() readme_path.write_text.assert_called_once_with(readme_template.render(), encoding="utf-8") git_ignore_template.render.assert_called_once() git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render(), encoding="utf-8") @@ -483,12 +475,7 @@ def test__build_pyproject_toml(self, mocker, use_poetry): project.env.get_template.assert_called_once_with(template_path) - pyproject_template.render.assert_called_once_with( - project_name=project.project_name, - package_name=project.package_name, - version=project.version, - description=project.package_description, - ) + pyproject_template.render.assert_called_once_with() pyproject_path.write_text.assert_called_once_with(pyproject_template.render(), encoding="utf-8") def test__build_setup_py(self, mocker): @@ -511,12 +498,7 @@ def test__build_setup_py(self, mocker): project.env.get_template.assert_called_once_with("setup.py.jinja") - setup_template.render.assert_called_once_with( - project_name=project.project_name, - package_name=project.package_name, - version=project.version, - description=project.package_description, - ) + setup_template.render.assert_called_once_with() setup_path.write_text.assert_called_once_with(setup_template.render(), encoding="utf-8") diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index a220d243c..05e8d4255 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -167,29 +167,79 @@ def test_parse_request_form_body_no_data(self): assert result is None - def test_parse_multipart_body(self, mocker): - ref = mocker.MagicMock() + def test_parse_multipart_body(self, mocker, model_property_factory): + from openapi_python_client.parser.openapi import Endpoint, Schemas + from openapi_python_client.parser.properties import Class + + class_info = Class(name="class_name", module_name="module_name") + prop_before = model_property_factory(class_info=class_info, is_multipart_body=False) + + schema = mocker.MagicMock() body = oai.RequestBody.construct( - content={"multipart/form-data": oai.MediaType.construct(media_type_schema=oai.Reference.construct(ref=ref))} + content={"multipart/form-data": oai.MediaType.construct(media_type_schema=schema)} ) - from_string = mocker.patch(f"{MODULE_NAME}.Class.from_string") + schemas_before = Schemas() config = MagicMock() + property_from_data = mocker.patch( + f"{MODULE_NAME}.property_from_data", return_value=(prop_before, schemas_before) + ) - from openapi_python_client.parser.openapi import Endpoint + result = Endpoint.parse_multipart_body(body=body, schemas=schemas_before, parent_name="parent", config=config) - result = Endpoint.parse_multipart_body(body=body, config=config) + property_from_data.assert_called_once_with( + name="multipart_data", + required=True, + data=schema, + schemas=schemas_before, + parent_name="parent", + config=config, + ) + prop_after = model_property_factory(class_info=class_info, is_multipart_body=True) + schemas_after = Schemas(classes_by_name={class_info.name: prop_after}) + assert result == (prop_after, schemas_after) - from_string.assert_called_once_with(string=ref, config=config) - assert result == from_string.return_value + def test_parse_multipart_body_existing_schema(self, mocker, model_property_factory): + from openapi_python_client.parser.openapi import Endpoint, Schemas + from openapi_python_client.parser.properties import Class + + class_info = Class(name="class_name", module_name="module_name") + prop_before = model_property_factory(class_info=class_info, is_multipart_body=False) + schemas_before = Schemas(classes_by_name={class_info.name: prop_before}) + + schema = mocker.MagicMock() + body = oai.RequestBody.construct( + content={"multipart/form-data": oai.MediaType.construct(media_type_schema=schema)} + ) + config = MagicMock() + property_from_data = mocker.patch( + f"{MODULE_NAME}.property_from_data", return_value=(prop_before, schemas_before) + ) + + result = Endpoint.parse_multipart_body(body=body, schemas=schemas_before, parent_name="parent", config=config) + + property_from_data.assert_called_once_with( + name="multipart_data", + required=True, + data=schema, + schemas=schemas_before, + parent_name="parent", + config=config, + ) + prop_after = model_property_factory(class_info=class_info, is_multipart_body=True) + schemas_after = Schemas(classes_by_name={class_info.name: prop_after}) + assert result == (prop_after, schemas_after) def test_parse_multipart_body_no_data(self): - body = oai.RequestBody.construct(content={}) + from openapi_python_client.parser.openapi import Endpoint, Schemas - from openapi_python_client.parser.openapi import Endpoint + body = oai.RequestBody.construct(content={}) + schemas = Schemas() - result = Endpoint.parse_multipart_body(body=body, config=MagicMock()) + prop, schemas = Endpoint.parse_multipart_body( + body=body, schemas=schemas, parent_name="parent", config=MagicMock() + ) - assert result is None + assert prop is None def test_parse_request_json_body(self, mocker): from openapi_python_client.parser.openapi import Endpoint, Schemas @@ -230,7 +280,7 @@ def test_add_body_no_data(self, mocker): parse_request_form_body.assert_not_called() - def test_add_body_bad_data(self, mocker): + def test_add_body_bad_json_data(self, mocker): from openapi_python_client.parser.openapi import Endpoint, Schemas mocker.patch.object(Endpoint, "parse_request_form_body") @@ -250,7 +300,35 @@ def test_add_body_bad_data(self, mocker): assert result == ( ParseError( - header=f"Cannot parse body of endpoint {endpoint.name}", + header=f"Cannot parse JSON body of endpoint {endpoint.name}", + detail=parse_error.detail, + data=parse_error.data, + ), + other_schemas, + ) + + def test_add_body_bad_multipart_data(self, mocker): + from openapi_python_client.parser.openapi import Endpoint, Schemas + + mocker.patch.object(Endpoint, "parse_request_form_body") + mocker.patch.object(Endpoint, "parse_request_json_body", return_value=(mocker.MagicMock(), mocker.MagicMock())) + parse_error = ParseError(data=mocker.MagicMock(), detail=mocker.MagicMock()) + other_schemas = mocker.MagicMock() + mocker.patch.object(Endpoint, "parse_multipart_body", return_value=(parse_error, other_schemas)) + endpoint = self.make_endpoint() + request_body = mocker.MagicMock() + schemas = Schemas() + + result = Endpoint._add_body( + endpoint=endpoint, + data=oai.Operation.construct(requestBody=request_body), + schemas=schemas, + config=MagicMock(), + ) + + assert result == ( + ParseError( + header=f"Cannot parse multipart body of endpoint {endpoint.name}", detail=parse_error.detail, data=parse_error.data, ), @@ -264,20 +342,24 @@ def test_add_body_happy(self, mocker): request_body = mocker.MagicMock() config = mocker.MagicMock() form_body_class = Class(name="A", module_name="a") - multipart_body_class = Class(name="B", module_name="b") parse_request_form_body = mocker.patch.object(Endpoint, "parse_request_form_body", return_value=form_body_class) - parse_multipart_body = mocker.patch.object(Endpoint, "parse_multipart_body", return_value=multipart_body_class) + + multipart_body = mocker.MagicMock(autospec=Property) + multipart_body_imports = mocker.MagicMock() + multipart_body.get_imports.return_value = {multipart_body_imports} + multipart_schemas = mocker.MagicMock() + parse_multipart_body = mocker.patch.object( + Endpoint, "parse_multipart_body", return_value=(multipart_body, multipart_schemas) + ) json_body = mocker.MagicMock(autospec=Property) json_body_imports = mocker.MagicMock() json_body.get_imports.return_value = {json_body_imports} - parsed_schemas = mocker.MagicMock() + json_schemas = mocker.MagicMock() parse_request_json_body = mocker.patch.object( - Endpoint, "parse_request_json_body", return_value=(json_body, parsed_schemas) - ) - import_string_from_class = mocker.patch( - f"{MODULE_NAME}.import_string_from_class", side_effect=["import_1", "import_2"] + Endpoint, "parse_request_json_body", return_value=(json_body, json_schemas) ) + import_string_from_class = mocker.patch(f"{MODULE_NAME}.import_string_from_class", return_value="import_1") endpoint = self.make_endpoint() initial_schemas = mocker.MagicMock() @@ -289,23 +371,21 @@ def test_add_body_happy(self, mocker): config=config, ) - assert response_schemas == parsed_schemas + assert response_schemas == multipart_schemas parse_request_form_body.assert_called_once_with(body=request_body, config=config) parse_request_json_body.assert_called_once_with( body=request_body, schemas=initial_schemas, parent_name="name", config=config ) - parse_multipart_body.assert_called_once_with(body=request_body, config=config) - import_string_from_class.assert_has_calls( - [ - mocker.call(form_body_class, prefix="...models"), - mocker.call(multipart_body_class, prefix="...models"), - ] + parse_multipart_body.assert_called_once_with( + body=request_body, schemas=json_schemas, parent_name="name", config=config ) + import_string_from_class.assert_called_once_with(form_body_class, prefix="...models") json_body.get_imports.assert_called_once_with(prefix="...") - assert endpoint.relative_imports == {"import_1", "import_2", "import_3", json_body_imports} + multipart_body.get_imports.assert_called_once_with(prefix="...") + assert endpoint.relative_imports == {"import_1", "import_3", json_body_imports, multipart_body_imports} assert endpoint.json_body == json_body assert endpoint.form_body_class == form_body_class - assert endpoint.multipart_body_class == multipart_body_class + assert endpoint.multipart_body == multipart_body def test__add_responses_status_code_error(self, mocker): from openapi_python_client.parser.openapi import Endpoint, Schemas diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 1f5646d74..57cf3ad22 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -166,13 +166,13 @@ def test_get_imports(self): p = FileProperty(name="test", required=True, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { "from io import BytesIO", - "from ...types import File", + "from ...types import File, FileJsonType", } p = FileProperty(name="test", required=False, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { "from io import BytesIO", - "from ...types import File", + "from ...types import File, FileJsonType", "from typing import Union", "from ...types import UNSET, Unset", } @@ -180,7 +180,7 @@ def test_get_imports(self): p = FileProperty(name="test", required=False, default=None, nullable=True) assert p.get_imports(prefix=prefix) == { "from io import BytesIO", - "from ...types import File", + "from ...types import File, FileJsonType", "from typing import Union", "from typing import Optional", "from ...types import UNSET, Unset", diff --git a/tests/test_utils.py b/tests/test_utils.py index ec78371ad..9d3b57f4b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,6 +3,22 @@ from openapi_python_client import utils +@pytest.mark.parametrize( + "before, after", + [ + ("connectionID", ["connection", "ID"]), + ("connection_id", ["connection", "id"]), + ("connection-id", ["connection", "id"]), + ("Response200", ["Response", "200"]), + ("Response200Okay", ["Response", "200", "Okay"]), + ("S3Config", ["S3", "Config"]), + ("s3config", ["s3config"]), + ], +) +def test_split_words(before, after): + assert utils.split_words(before) == after + + def test_snake_case_uppercase_str(): assert utils.snake_case("HTTP") == "http" assert utils.snake_case("HTTP RESPONSE") == "http_response"