Skip to content

Commit 6812770

Browse files
committed
refactor: Centralize and strenghen response handling
No more blind type casting and type witnesses.
1 parent 70708cf commit 6812770

5 files changed

Lines changed: 233 additions & 153 deletions

File tree

tests/test_http_requests.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any
3+
from typing import TYPE_CHECKING
44

55
import httpx
66
import pytest
77

88
from tests.data.test_defaults import DEFAULT_REQUEST_ID, DEFAULT_TOKEN
99
from tests.utils.test_utils import api_headers, mock_route
10-
from todoist_api_python._core.http_requests import delete, get, post
10+
from todoist_api_python._core.http_requests import (
11+
delete,
12+
get,
13+
post,
14+
response_json_dict,
15+
)
1116

1217
if TYPE_CHECKING:
1318
import respx
@@ -30,7 +35,7 @@ def test_get_with_params(respx_mock: respx.MockRouter) -> None:
3035
)
3136

3237
with httpx.Client() as client:
33-
response: dict[str, Any] = get(
38+
response = get(
3439
client=client,
3540
url=EXAMPLE_URL,
3641
token=DEFAULT_TOKEN,
@@ -39,7 +44,7 @@ def test_get_with_params(respx_mock: respx.MockRouter) -> None:
3944
)
4045

4146
assert len(respx_mock.calls) == 1
42-
assert response == EXAMPLE_RESPONSE
47+
assert response.json() == EXAMPLE_RESPONSE
4348

4449

4550
def test_get_raise_for_status(respx_mock: respx.MockRouter) -> None:
@@ -69,7 +74,7 @@ def test_post_with_data(respx_mock: respx.MockRouter) -> None:
6974
)
7075

7176
with httpx.Client() as client:
72-
response: dict[str, Any] = post(
77+
response = post(
7378
client=client,
7479
url=EXAMPLE_URL,
7580
token=DEFAULT_TOKEN,
@@ -78,7 +83,7 @@ def test_post_with_data(respx_mock: respx.MockRouter) -> None:
7883
)
7984

8085
assert len(respx_mock.calls) == 1
81-
assert response == EXAMPLE_RESPONSE
86+
assert response.json() == EXAMPLE_RESPONSE
8287

8388

8489
def test_post_with_empty_data(respx_mock: respx.MockRouter) -> None:
@@ -93,7 +98,7 @@ def test_post_with_empty_data(respx_mock: respx.MockRouter) -> None:
9398
)
9499

95100
with httpx.Client() as client:
96-
response: dict[str, Any] = post(
101+
response = post(
97102
client=client,
98103
url=EXAMPLE_URL,
99104
token=DEFAULT_TOKEN,
@@ -102,7 +107,7 @@ def test_post_with_empty_data(respx_mock: respx.MockRouter) -> None:
102107
)
103108

104109
assert len(respx_mock.calls) == 1
105-
assert response == EXAMPLE_RESPONSE
110+
assert response.json() == EXAMPLE_RESPONSE
106111

107112

108113
def test_post_return_ok_when_no_response_body(respx_mock: respx.MockRouter) -> None:
@@ -114,9 +119,9 @@ def test_post_return_ok_when_no_response_body(respx_mock: respx.MockRouter) -> N
114119
)
115120

116121
with httpx.Client() as client:
117-
result: bool = post(client=client, url=EXAMPLE_URL, token=DEFAULT_TOKEN)
122+
response = post(client=client, url=EXAMPLE_URL, token=DEFAULT_TOKEN)
118123

119-
assert result is True
124+
assert response.is_success is True
120125

121126

122127
def test_post_raise_for_status(respx_mock: respx.MockRouter) -> None:
@@ -142,7 +147,7 @@ def test_delete_with_params(respx_mock: respx.MockRouter) -> None:
142147
)
143148

144149
with httpx.Client() as client:
145-
result = delete(
150+
response = delete(
146151
client=client,
147152
url=EXAMPLE_URL,
148153
token=DEFAULT_TOKEN,
@@ -151,7 +156,7 @@ def test_delete_with_params(respx_mock: respx.MockRouter) -> None:
151156
)
152157

153158
assert len(respx_mock.calls) == 1
154-
assert result is True
159+
assert response.is_success is True
155160

156161

157162
def test_delete_raise_for_status(respx_mock: respx.MockRouter) -> None:
@@ -164,3 +169,16 @@ def test_delete_raise_for_status(respx_mock: respx.MockRouter) -> None:
164169

165170
with httpx.Client() as client, pytest.raises(httpx.HTTPStatusError):
166171
delete(client=client, url=EXAMPLE_URL, token=DEFAULT_TOKEN)
172+
173+
174+
def test_response_json_dict_returns_dict() -> None:
175+
response = httpx.Response(status_code=200, json={"result": "ok"})
176+
177+
assert response_json_dict(response) == {"result": "ok"}
178+
179+
180+
def test_response_json_dict_raises_for_non_dict() -> None:
181+
response = httpx.Response(status_code=200, json=["not", "a", "dict"])
182+
183+
with pytest.raises(TypeError, match="JSON object"):
184+
response_json_dict(response)
Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Any, TypeVar, cast
3+
from typing import Any
44

55
import httpx
66

@@ -15,29 +15,14 @@
1515
# are forcefully terminated after this time, so there is no point waiting longer.
1616
TIMEOUT = httpx.Timeout(connect=10.0, read=60.0, write=60.0, pool=10.0)
1717

18-
T = TypeVar("T")
19-
20-
21-
def _parse_response(
22-
response: httpx.Response,
23-
_result_type: type[T] | None = None,
24-
) -> T:
25-
response.raise_for_status()
26-
27-
if response.status_code == httpx.codes.NO_CONTENT:
28-
return cast("T", response.is_success)
29-
30-
return cast("T", response.json())
31-
3218

3319
def get(
3420
client: httpx.Client,
3521
url: str,
3622
token: str | None = None,
3723
request_id: str | None = None,
3824
params: dict[str, Any] | None = None,
39-
result_type: type[T] | None = None,
40-
) -> T:
25+
) -> httpx.Response:
4126
headers = create_headers(token=token, request_id=request_id)
4227

4328
response = client.get(
@@ -46,8 +31,8 @@ def get(
4631
headers=headers,
4732
timeout=TIMEOUT,
4833
)
49-
50-
return _parse_response(response, result_type)
34+
response.raise_for_status()
35+
return response
5136

5237

5338
async def get_async(
@@ -56,8 +41,7 @@ async def get_async(
5641
token: str | None = None,
5742
request_id: str | None = None,
5843
params: dict[str, Any] | None = None,
59-
result_type: type[T] | None = None,
60-
) -> T:
44+
) -> httpx.Response:
6145
headers = create_headers(token=token, request_id=request_id)
6246

6347
response = await client.get(
@@ -66,8 +50,8 @@ async def get_async(
6650
headers=headers,
6751
timeout=TIMEOUT,
6852
)
69-
70-
return _parse_response(response, result_type)
53+
response.raise_for_status()
54+
return response
7155

7256

7357
def post(
@@ -78,19 +62,18 @@ def post(
7862
*,
7963
params: dict[str, Any] | None = None,
8064
data: dict[str, Any] | None = None,
81-
result_type: type[T] | None = None,
82-
) -> T:
65+
) -> httpx.Response:
8366
headers = create_headers(token=token, request_id=request_id)
8467

8568
response = client.post(
8669
url,
8770
headers=headers,
88-
json=data if data is not None else None,
71+
json=data,
8972
params=params,
9073
timeout=TIMEOUT,
9174
)
92-
93-
return _parse_response(response, result_type)
75+
response.raise_for_status()
76+
return response
9477

9578

9679
async def post_async(
@@ -101,19 +84,18 @@ async def post_async(
10184
*,
10285
params: dict[str, Any] | None = None,
10386
data: dict[str, Any] | None = None,
104-
result_type: type[T] | None = None,
105-
) -> T:
87+
) -> httpx.Response:
10688
headers = create_headers(token=token, request_id=request_id)
10789

10890
response = await client.post(
10991
url,
11092
headers=headers,
111-
json=data if data is not None else None,
93+
json=data,
11294
params=params,
11395
timeout=TIMEOUT,
11496
)
115-
116-
return _parse_response(response, result_type)
97+
response.raise_for_status()
98+
return response
11799

118100

119101
def delete(
@@ -122,13 +104,12 @@ def delete(
122104
token: str | None = None,
123105
request_id: str | None = None,
124106
params: dict[str, Any] | None = None,
125-
) -> bool:
107+
) -> httpx.Response:
126108
headers = create_headers(token=token, request_id=request_id)
127109

128110
response = client.delete(url, params=params, headers=headers, timeout=TIMEOUT)
129-
130111
response.raise_for_status()
131-
return response.is_success
112+
return response
132113

133114

134115
async def delete_async(
@@ -137,10 +118,18 @@ async def delete_async(
137118
token: str | None = None,
138119
request_id: str | None = None,
139120
params: dict[str, Any] | None = None,
140-
) -> bool:
121+
) -> httpx.Response:
141122
headers = create_headers(token=token, request_id=request_id)
142123

143124
response = await client.delete(url, params=params, headers=headers, timeout=TIMEOUT)
144-
145125
response.raise_for_status()
146-
return response.is_success
126+
return response
127+
128+
129+
def response_json_dict(response: httpx.Response) -> dict[str, Any]:
130+
data = response.json()
131+
if not isinstance(data, dict):
132+
raise TypeError(
133+
f"Expected response to be a JSON object, got {type(data).__name__}."
134+
)
135+
return data

0 commit comments

Comments
 (0)