From 3cca61417350f8ef16270dabb2921b7606cdb83c Mon Sep 17 00:00:00 2001 From: Gustav Bylund Date: Thu, 27 Aug 2020 16:38:54 +0200 Subject: [PATCH] feat: add support for lists of basic python types Added support for lists of strings, floats, integers and booleans. --- CHANGELOG.md | 1 + end_to_end_tests/fastapi_app/__init__.py | 24 ++++ end_to_end_tests/fastapi_app/openapi.json | 104 ++++++++++++++++++ .../my_test_api_client/api/tests.py | 60 ++++++++++ .../my_test_api_client/async_api/tests.py | 64 +++++++++++ openapi_python_client/parser/responses.py | 25 +++++ tests/test_openapi_parser/test_responses.py | 57 ++++++++++ 7 files changed, 335 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 185e871ce..d15fb4782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for octet-stream content type (#116) - Support for [nullable](https://swagger.io/docs/specification/data-models/data-types/#null) (#99) - Union properties defined using oneOf (#98) +- Added support for lists of strings, integers, floats and booleans (#165). Thanks @Maistho! ## 0.5.3 - 2020-08-13 diff --git a/end_to_end_tests/fastapi_app/__init__.py b/end_to_end_tests/fastapi_app/__init__.py index fb0901fc5..0b58cb56d 100644 --- a/end_to_end_tests/fastapi_app/__init__.py +++ b/end_to_end_tests/fastapi_app/__init__.py @@ -57,6 +57,30 @@ def get_list( return +@test_router.get("/basic_lists/strings", response_model=List[str], operation_id="getBasicListOfStrings") +def get_basic_list_of_strings(): + """ Get a list of strings """ + return + + +@test_router.get("/basic_lists/integers", response_model=List[int], operation_id="getBasicListOfIntegers") +def get_basic_list_of_integers(): + """ Get a list of integers """ + return + + +@test_router.get("/basic_lists/floats", response_model=List[float], operation_id="getBasicListOfFloats") +def get_basic_list_of_floats(): + """ Get a list of floats """ + return + + +@test_router.get("/basic_lists/booleans", response_model=List[bool], operation_id="getBasicListOfBooleans") +def get_basic_list_of_booleans(): + """ Get a list of booleans """ + return + + @test_router.post("/upload") async def upload_file(some_file: UploadFile = File(...), keep_alive: bool = Header(None)): """ Upload a file """ diff --git a/end_to_end_tests/fastapi_app/openapi.json b/end_to_end_tests/fastapi_app/openapi.json index 05cbb0b31..7c36f444f 100644 --- a/end_to_end_tests/fastapi_app/openapi.json +++ b/end_to_end_tests/fastapi_app/openapi.json @@ -94,6 +94,110 @@ } } }, + "/tests/basic_lists/strings": { + "get": { + "tags": [ + "tests" + ], + "summary": "Get Basic List Of Strings", + "description": "Get a list of strings ", + "operationId": "getBasicListOfStrings", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Basic List Of Strings Tests Basic Lists Strings Get", + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "/tests/basic_lists/integers": { + "get": { + "tags": [ + "tests" + ], + "summary": "Get Basic List Of Integers", + "description": "Get a list of integers ", + "operationId": "getBasicListOfIntegers", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Basic List Of Integers Tests Basic Lists Integers Get", + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + } + }, + "/tests/basic_lists/floats": { + "get": { + "tags": [ + "tests" + ], + "summary": "Get Basic List Of Floats", + "description": "Get a list of floats ", + "operationId": "getBasicListOfFloats", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Basic List Of Floats Tests Basic Lists Floats Get", + "type": "array", + "items": { + "type": "number" + } + } + } + } + } + } + } + }, + "/tests/basic_lists/booleans": { + "get": { + "tags": [ + "tests" + ], + "summary": "Get Basic List Of Booleans", + "description": "Get a list of booleans ", + "operationId": "getBasicListOfBooleans", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Basic List Of Booleans Tests Basic Lists Booleans Get", + "type": "array", + "items": { + "type": "boolean" + } + } + } + } + } + } + } + }, "/tests/upload": { "post": { "tags": [ diff --git a/end_to_end_tests/golden-master/my_test_api_client/api/tests.py b/end_to_end_tests/golden-master/my_test_api_client/api/tests.py index 7304e371e..91ece074e 100644 --- a/end_to_end_tests/golden-master/my_test_api_client/api/tests.py +++ b/end_to_end_tests/golden-master/my_test_api_client/api/tests.py @@ -50,6 +50,66 @@ def get_user_list( raise ApiResponseError(response=response) +def get_basic_list_of_strings(*, client: Client,) -> List[str]: + + """ Get a list of strings """ + url = "{}/tests/basic_lists/strings".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + response = httpx.get(url=url, headers=headers,) + + if response.status_code == 200: + return [str(item) for item in cast(List[str], response.json())] + else: + raise ApiResponseError(response=response) + + +def get_basic_list_of_integers(*, client: Client,) -> List[int]: + + """ Get a list of integers """ + url = "{}/tests/basic_lists/integers".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + response = httpx.get(url=url, headers=headers,) + + if response.status_code == 200: + return [int(item) for item in cast(List[int], response.json())] + else: + raise ApiResponseError(response=response) + + +def get_basic_list_of_floats(*, client: Client,) -> List[float]: + + """ Get a list of floats """ + url = "{}/tests/basic_lists/floats".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + response = httpx.get(url=url, headers=headers,) + + if response.status_code == 200: + return [float(item) for item in cast(List[float], response.json())] + else: + raise ApiResponseError(response=response) + + +def get_basic_list_of_booleans(*, client: Client,) -> List[bool]: + + """ Get a list of booleans """ + url = "{}/tests/basic_lists/booleans".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + response = httpx.get(url=url, headers=headers,) + + if response.status_code == 200: + return [bool(item) for item in cast(List[bool], response.json())] + else: + raise ApiResponseError(response=response) + + def upload_file_tests_upload_post( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, keep_alive: Optional[bool] = None, ) -> Union[ diff --git a/end_to_end_tests/golden-master/my_test_api_client/async_api/tests.py b/end_to_end_tests/golden-master/my_test_api_client/async_api/tests.py index bbe7e11c8..3ea1ebc7c 100644 --- a/end_to_end_tests/golden-master/my_test_api_client/async_api/tests.py +++ b/end_to_end_tests/golden-master/my_test_api_client/async_api/tests.py @@ -51,6 +51,70 @@ async def get_user_list( raise ApiResponseError(response=response) +async def get_basic_list_of_strings(*, client: Client,) -> List[str]: + + """ Get a list of strings """ + url = "{}/tests/basic_lists/strings".format(client.base_url,) + + headers: Dict[str, Any] = client.get_headers() + + async with httpx.AsyncClient() as _client: + response = await _client.get(url=url, headers=headers,) + + if response.status_code == 200: + return [str(item) for item in cast(List[str], response.json())] + else: + raise ApiResponseError(response=response) + + +async def get_basic_list_of_integers(*, client: Client,) -> List[int]: + + """ Get a list of integers """ + url = "{}/tests/basic_lists/integers".format(client.base_url,) + + headers: Dict[str, Any] = client.get_headers() + + async with httpx.AsyncClient() as _client: + response = await _client.get(url=url, headers=headers,) + + if response.status_code == 200: + return [int(item) for item in cast(List[int], response.json())] + else: + raise ApiResponseError(response=response) + + +async def get_basic_list_of_floats(*, client: Client,) -> List[float]: + + """ Get a list of floats """ + url = "{}/tests/basic_lists/floats".format(client.base_url,) + + headers: Dict[str, Any] = client.get_headers() + + async with httpx.AsyncClient() as _client: + response = await _client.get(url=url, headers=headers,) + + if response.status_code == 200: + return [float(item) for item in cast(List[float], response.json())] + else: + raise ApiResponseError(response=response) + + +async def get_basic_list_of_booleans(*, client: Client,) -> List[bool]: + + """ Get a list of booleans """ + url = "{}/tests/basic_lists/booleans".format(client.base_url,) + + headers: Dict[str, Any] = client.get_headers() + + async with httpx.AsyncClient() as _client: + response = await _client.get(url=url, headers=headers,) + + if response.status_code == 200: + return [bool(item) for item in cast(List[bool], response.json())] + else: + raise ApiResponseError(response=response) + + async def upload_file_tests_upload_post( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, keep_alive: Optional[bool] = None, ) -> Union[ diff --git a/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index 716abff35..d888bf3fa 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -51,6 +51,25 @@ def constructor(self) -> str: return f"{self.reference.class_name}.from_dict(cast(Dict[str, Any], response.json()))" +@dataclass +class ListBasicResponse(Response): + """ Response is a list of some basic type """ + + openapi_type: InitVar[str] + python_type: str = field(init=False) + + def __post_init__(self, openapi_type: str) -> None: + self.python_type = openapi_types_to_python_type_strings[openapi_type] + + def return_string(self) -> str: + """ How this Response should be represented as a return type """ + return f"List[{self.python_type}]" + + def constructor(self) -> str: + """ How the return value of this response should be constructed """ + return f"[{self.python_type}(item) for item in cast(List[{self.python_type}], response.json())]" + + @dataclass class BasicResponse(Response): """ Response is a basic type """ @@ -118,6 +137,12 @@ def response_from_data(*, status_code: int, data: Union[oai.Response, oai.Refere return Response(status_code=status_code) if response_type == "array" and isinstance(schema_data.items, oai.Reference): return ListRefResponse(status_code=status_code, reference=Reference.from_ref(schema_data.items.ref),) + if ( + response_type == "array" + and isinstance(schema_data.items, oai.Schema) + and schema_data.items.type in openapi_types_to_python_type_strings + ): + return ListBasicResponse(status_code=status_code, openapi_type=schema_data.items.type) if response_type in openapi_types_to_python_type_strings: return BasicResponse(status_code=status_code, openapi_type=response_type) return ParseError(data=data, detail=f"Unrecognized type {schema_data.type}") diff --git a/tests/test_openapi_parser/test_responses.py b/tests/test_openapi_parser/test_responses.py index 8edba902f..ce93efb15 100644 --- a/tests/test_openapi_parser/test_responses.py +++ b/tests/test_openapi_parser/test_responses.py @@ -54,6 +54,46 @@ def test_constructor(self, mocker): assert r.constructor() == "SuperCoolClass.from_dict(cast(Dict[str, Any], response.json()))" +class TestListBasicResponse: + def test_return_string(self): + from openapi_python_client.parser.responses import ListBasicResponse + + r = ListBasicResponse(200, "string") + + assert r.return_string() == "List[str]" + + r = ListBasicResponse(200, "number") + + assert r.return_string() == "List[float]" + + r = ListBasicResponse(200, "integer") + + assert r.return_string() == "List[int]" + + r = ListBasicResponse(200, "boolean") + + assert r.return_string() == "List[bool]" + + def test_constructor(self): + from openapi_python_client.parser.responses import ListBasicResponse + + r = ListBasicResponse(200, "string") + + assert r.constructor() == "[str(item) for item in cast(List[str], response.json())]" + + r = ListBasicResponse(200, "number") + + assert r.constructor() == "[float(item) for item in cast(List[float], response.json())]" + + r = ListBasicResponse(200, "integer") + + assert r.constructor() == "[int(item) for item in cast(List[int], response.json())]" + + r = ListBasicResponse(200, "boolean") + + assert r.constructor() == "[bool(item) for item in cast(List[bool], response.json())]" + + class TestBasicResponse: def test_return_string(self): from openapi_python_client.parser.responses import BasicResponse @@ -193,6 +233,23 @@ def test_response_from_data_array(self, mocker): ListRefResponse.assert_called_once_with(status_code=status_code, reference=from_ref()) assert response == ListRefResponse() + def test_response_from_basic_array(self, mocker): + status_code = mocker.MagicMock(autospec=int) + data = oai.Response.construct( + content={ + "application/json": oai.MediaType.construct( + media_type_schema=oai.Schema.construct(type="array", items=oai.Schema.construct(type="string")) + ) + } + ) + ListBasicResponse = mocker.patch(f"{MODULE_NAME}.ListBasicResponse") + from openapi_python_client.parser.responses import response_from_data + + response = response_from_data(status_code=status_code, data=data) + + ListBasicResponse.assert_called_once_with(status_code=status_code, openapi_type="string") + assert response == ListBasicResponse.return_value + def test_response_from_data_basic(self, mocker): status_code = mocker.MagicMock(autospec=int) data = oai.Response.construct(