Skip to content

feat: add support for lists of basic python types #165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions end_to_end_tests/fastapi_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
104 changes: 104 additions & 0 deletions end_to_end_tests/fastapi_app/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
60 changes: 60 additions & 0 deletions end_to_end_tests/golden-master/my_test_api_client/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need the cast here since we're converting to the correct type with str. I suppose it doesn't hurt though, so we can leave it in.

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[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down
25 changes: 25 additions & 0 deletions openapi_python_client/parser/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down Expand Up @@ -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}")
57 changes: 57 additions & 0 deletions tests/test_openapi_parser/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down