From 84f7d167f6ff8cdb651d9fb4dea24f7151ac339f Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Thu, 26 Oct 2023 09:59:59 +1300 Subject: [PATCH 1/2] Support for Decimal inputs --- .../standard/templates/async_methods.jinja2 | 14 ++++++-------- .../standard/templates/http_py.jinja2 | 15 ++++++++++++--- .../standard/templates/schemas_py.jinja2 | 6 +++--- .../standard/templates/sync_methods.jinja2 | 17 +++++++++++------ example_openapi_specs/best.json | 9 +++++++-- tests/async_test_client/http.py | 17 ++++++++++++++--- tests/async_test_client/schemas.py | 7 ++++--- tests/test_async_generated_client.py | 6 +++--- tests/test_client/http.py | 17 ++++++++++++++--- tests/test_client/schemas.py | 7 ++++--- tests/test_generated_client.py | 6 +++--- 11 files changed, 81 insertions(+), 40 deletions(-) diff --git a/clientele/generators/standard/templates/async_methods.jinja2 b/clientele/generators/standard/templates/async_methods.jinja2 index a84f8cd..bbecff0 100644 --- a/clientele/generators/standard/templates/async_methods.jinja2 +++ b/clientele/generators/standard/templates/async_methods.jinja2 @@ -6,20 +6,18 @@ async def get(url: str, headers: typing.Optional[dict] = None) -> httpx.Response return await async_client.get(parse_url(url), headers=headers) -async def post( - url: str, data: dict, headers: typing.Optional[dict] = None -) -> httpx.Response: +async def post(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response: """Issue an HTTP POST request""" + json_data = json.dumps(data, default=json_serializer) async with httpx.AsyncClient(headers=headers) as async_client: - return await async_client.post(parse_url(url), json=data, headers=headers) + return await async_client.post(parse_url(url), json=json_data, headers=headers) -async def put( - url: str, data: dict, headers: typing.Optional[dict] = None -) -> httpx.Response: +async def put(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response: """Issue an HTTP PUT request""" + json_data = json.dumps(data, default=json_serializer) async with httpx.AsyncClient(headers=headers) as async_client: - return await async_client.put(parse_url(url), json=data, headers=headers) + return await async_client.put(parse_url(url), json=json_data, headers=headers) async def delete(url: str, headers: typing.Optional[dict] = None) -> httpx.Response: diff --git a/clientele/generators/standard/templates/http_py.jinja2 b/clientele/generators/standard/templates/http_py.jinja2 index cb546a8..75310a6 100644 --- a/clientele/generators/standard/templates/http_py.jinja2 +++ b/clientele/generators/standard/templates/http_py.jinja2 @@ -1,13 +1,22 @@ -import typing -from urllib.parse import parse_qs, urlencode, urlparse, urlunparse +from __future__ import annotations +import json {% if new_unions %} import types {% endif %} -import httpx # noqa +import typing +from decimal import Decimal +from urllib.parse import parse_qs, urlencode, urlparse, urlunparse + +import httpx from {{client_project_directory_path}} import config as c # noqa +def json_serializer(obj): + if isinstance(obj, Decimal): + return str(obj) + + class APIException(Exception): """Could not match API response to return type of this function""" diff --git a/clientele/generators/standard/templates/schemas_py.jinja2 b/clientele/generators/standard/templates/schemas_py.jinja2 index 66a7974..8f91a7d 100644 --- a/clientele/generators/standard/templates/schemas_py.jinja2 +++ b/clientele/generators/standard/templates/schemas_py.jinja2 @@ -1,8 +1,8 @@ from __future__ import annotations -import inspect # noqa -import typing # noqa +import inspect +import typing from enum import Enum # noqa from decimal import Decimal #noqa -import pydantic # noqa +import pydantic diff --git a/clientele/generators/standard/templates/sync_methods.jinja2 b/clientele/generators/standard/templates/sync_methods.jinja2 index c0ba6a2..064b298 100644 --- a/clientele/generators/standard/templates/sync_methods.jinja2 +++ b/clientele/generators/standard/templates/sync_methods.jinja2 @@ -1,17 +1,22 @@ def get(url: str, headers: typing.Optional[dict] = None) -> httpx.Response: - """ Issue an HTTP GET request """ + """Issue an HTTP GET request""" return client.get(parse_url(url), headers=headers) + def post(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response: - """ Issue an HTTP POST request """ - return client.post(parse_url(url), json=data, headers=headers) + """Issue an HTTP POST request""" + json_data = json.dumps(data, default=json_serializer) + return client.post(parse_url(url), json=json_data, headers=headers) + def put(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response: - """ Issue an HTTP PUT request """ - return client.put(parse_url(url), json=data, headers=headers) + """Issue an HTTP PUT request""" + json_data = json.dumps(data, default=json_serializer) + return client.put(parse_url(url), json=json_data, headers=headers) + def delete(url: str, headers: typing.Optional[dict] = None) -> httpx.Response: - """ Issue an HTTP DELETE request """ + """Issue an HTTP DELETE request""" return client.delete(parse_url(url), headers=headers) diff --git a/example_openapi_specs/best.json b/example_openapi_specs/best.json index 48b053d..06447ef 100644 --- a/example_openapi_specs/best.json +++ b/example_openapi_specs/best.json @@ -185,10 +185,15 @@ "my_input": { "title": "My Input", "type": "string" - } + }, + "my_decimal_input": { + "title": "A Decimal input", + "type": "number", + "format": "decimal" + }, }, "required": [ - "my_input" + "my_input", "my_decimal_input" ], "title": "RequestDataRequest", "type": "object" diff --git a/tests/async_test_client/http.py b/tests/async_test_client/http.py index edfcb0d..a7bfd61 100644 --- a/tests/async_test_client/http.py +++ b/tests/async_test_client/http.py @@ -1,12 +1,21 @@ +from __future__ import annotations + +import json import types import typing +from decimal import Decimal from urllib.parse import parse_qs, urlencode, urlparse, urlunparse -import httpx # noqa +import httpx from tests.async_test_client import config as c # noqa +def json_serializer(obj): + if isinstance(obj, Decimal): + return str(obj) + + class APIException(Exception): """Could not match API response to return type of this function""" @@ -121,14 +130,16 @@ async def get(url: str, headers: typing.Optional[dict] = None) -> httpx.Response async def post(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response: """Issue an HTTP POST request""" + json_data = json.dumps(data, default=json_serializer) async with httpx.AsyncClient(headers=headers) as async_client: - return await async_client.post(parse_url(url), json=data, headers=headers) + return await async_client.post(parse_url(url), json=json_data, headers=headers) async def put(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response: """Issue an HTTP PUT request""" + json_data = json.dumps(data, default=json_serializer) async with httpx.AsyncClient(headers=headers) as async_client: - return await async_client.put(parse_url(url), json=data, headers=headers) + return await async_client.put(parse_url(url), json=json_data, headers=headers) async def delete(url: str, headers: typing.Optional[dict] = None) -> httpx.Response: diff --git a/tests/async_test_client/schemas.py b/tests/async_test_client/schemas.py index e05ee6c..3059c2e 100644 --- a/tests/async_test_client/schemas.py +++ b/tests/async_test_client/schemas.py @@ -1,11 +1,11 @@ from __future__ import annotations -import inspect # noqa -import typing # noqa +import inspect +import typing from decimal import Decimal # noqa from enum import Enum # noqa -import pydantic # noqa +import pydantic class AnotherModel(pydantic.BaseModel): @@ -58,6 +58,7 @@ class RequestDataAndParameterResponse(pydantic.BaseModel): class RequestDataRequest(pydantic.BaseModel): my_input: str + my_decimal_input: Decimal class RequestDataResponse(pydantic.BaseModel): diff --git a/tests/test_async_generated_client.py b/tests/test_async_generated_client.py index 5e37be9..b3dc37b 100644 --- a/tests/test_async_generated_client.py +++ b/tests/test_async_generated_client.py @@ -168,7 +168,7 @@ async def test_request_data_request_data_post(respx_mock: MockRouter): mock_path = "/request-data" respx_mock.post(mock_path).mock(return_value=Response(json=mocked_response, status_code=200)) # When - data = schemas.RequestDataRequest(my_input="test") + data = schemas.RequestDataRequest(my_input="test", my_decimal_input=Decimal(0.1)) response = await client.request_data_request_data_post(data=data) # Then assert isinstance(response, schemas.RequestDataResponse) @@ -186,7 +186,7 @@ async def test_request_data_request_data_put(respx_mock: MockRouter): mock_path = "/request-data" respx_mock.put(mock_path).mock(return_value=Response(json=mocked_response, status_code=200)) # When - data = schemas.RequestDataRequest(my_input="test") + data = schemas.RequestDataRequest(my_input="test", my_decimal_input=Decimal(0.1)) response = await client.request_data_request_data_put(data=data) # Then assert isinstance(response, schemas.RequestDataResponse) @@ -205,7 +205,7 @@ async def test_request_data_path_request_data(respx_mock: MockRouter): mock_path = f"/request-data/{path_parameter}" respx_mock.post(mock_path).mock(return_value=Response(json=mocked_response, status_code=200)) # When - data = schemas.RequestDataRequest(my_input="test") + data = schemas.RequestDataRequest(my_input="test", my_decimal_input=Decimal(0.1)) response = await client.request_data_path_request_data(path_parameter, data=data) # Then assert isinstance(response, schemas.RequestDataAndParameterResponse) diff --git a/tests/test_client/http.py b/tests/test_client/http.py index 8bd9d0e..7aa1130 100644 --- a/tests/test_client/http.py +++ b/tests/test_client/http.py @@ -1,12 +1,21 @@ +from __future__ import annotations + +import json import types import typing +from decimal import Decimal from urllib.parse import parse_qs, urlencode, urlparse, urlunparse -import httpx # noqa +import httpx from tests.test_client import config as c # noqa +def json_serializer(obj): + if isinstance(obj, Decimal): + return str(obj) + + class APIException(Exception): """Could not match API response to return type of this function""" @@ -120,12 +129,14 @@ def get(url: str, headers: typing.Optional[dict] = None) -> httpx.Response: def post(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response: """Issue an HTTP POST request""" - return client.post(parse_url(url), json=data, headers=headers) + json_data = json.dumps(data, default=json_serializer) + return client.post(parse_url(url), json=json_data, headers=headers) def put(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response: """Issue an HTTP PUT request""" - return client.put(parse_url(url), json=data, headers=headers) + json_data = json.dumps(data, default=json_serializer) + return client.put(parse_url(url), json=json_data, headers=headers) def delete(url: str, headers: typing.Optional[dict] = None) -> httpx.Response: diff --git a/tests/test_client/schemas.py b/tests/test_client/schemas.py index e05ee6c..3059c2e 100644 --- a/tests/test_client/schemas.py +++ b/tests/test_client/schemas.py @@ -1,11 +1,11 @@ from __future__ import annotations -import inspect # noqa -import typing # noqa +import inspect +import typing from decimal import Decimal # noqa from enum import Enum # noqa -import pydantic # noqa +import pydantic class AnotherModel(pydantic.BaseModel): @@ -58,6 +58,7 @@ class RequestDataAndParameterResponse(pydantic.BaseModel): class RequestDataRequest(pydantic.BaseModel): my_input: str + my_decimal_input: Decimal class RequestDataResponse(pydantic.BaseModel): diff --git a/tests/test_generated_client.py b/tests/test_generated_client.py index 1091bef..c954448 100644 --- a/tests/test_generated_client.py +++ b/tests/test_generated_client.py @@ -152,7 +152,7 @@ def test_request_data_request_data_post(respx_mock: MockRouter): mock_path = "/request-data" respx_mock.post(mock_path).mock(return_value=Response(json=mocked_response, status_code=200)) # When - data = schemas.RequestDataRequest(my_input="test") + data = schemas.RequestDataRequest(my_input="test", my_decimal_input=Decimal(0.1)) response = client.request_data_request_data_post(data=data) # Then assert isinstance(response, schemas.RequestDataResponse) @@ -169,7 +169,7 @@ def test_request_data_request_data_put(respx_mock: MockRouter): mock_path = "/request-data" respx_mock.put(mock_path).mock(return_value=Response(json=mocked_response, status_code=200)) # When - data = schemas.RequestDataRequest(my_input="test") + data = schemas.RequestDataRequest(my_input="test", my_decimal_input=Decimal(0.1)) response = client.request_data_request_data_put(data=data) # Then assert isinstance(response, schemas.RequestDataResponse) @@ -187,7 +187,7 @@ def test_request_data_path_request_data(respx_mock: MockRouter): mock_path = f"/request-data/{path_parameter}" respx_mock.post(mock_path).mock(return_value=Response(json=mocked_response, status_code=200)) # When - data = schemas.RequestDataRequest(my_input="test") + data = schemas.RequestDataRequest(my_input="test", my_decimal_input=Decimal(0.1)) response = client.request_data_path_request_data(path_parameter, data=data) # Then assert isinstance(response, schemas.RequestDataAndParameterResponse) From 86cc2345b4694293fd08488219e0ce267ec6067c Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Thu, 26 Oct 2023 10:01:04 +1300 Subject: [PATCH 2/2] Changelog --- CHANGELOG.md | 1 + docs/CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8166967..3d6809c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Improved support for Async clients which prevents a weird bug when running more than one event loop. Based on the suggestions from [this httpx issue](https://github.com/encode/httpcore/discussions/659). - We now use [`ruff format`](https://astral.sh/blog/the-ruff-formatter) for coding formatting (not the client output). +- `Decimal` support now extends to Decimal input values. ## 0.7.1 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index aadaba3..3d6809c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,8 @@ ## 0.8.0 - Improved support for Async clients which prevents a weird bug when running more than one event loop. Based on the suggestions from [this httpx issue](https://github.com/encode/httpcore/discussions/659). +- We now use [`ruff format`](https://astral.sh/blog/the-ruff-formatter) for coding formatting (not the client output). +- `Decimal` support now extends to Decimal input values. ## 0.7.1