Skip to content

Commit 3f40abf

Browse files
committed
Migrate sync SDK and tests to httpx
Switch TodoistAPI and authentication sync paths from requests\nto httpx.Client and align sync error semantics with\nhttpx.HTTPStatusError.\n\nUpdate sync-focused tests to use the respx-backed request\nmocking setup.
1 parent 0245bf5 commit 3f40abf

19 files changed

Lines changed: 899 additions & 678 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "todoist_api_python"
33
version = "3.2.1"
44
description = "Official Python SDK for the Todoist API."
55
authors = [{ name = "Doist Developers", email = "dev@doist.com" }]
6-
requires-python = "~=3.9"
6+
requires-python = ">=3.9,<4"
77
readme = "README.md"
88
license = "MIT"
99
keywords = ["todoist", "rest", "sync", "api", "python"]

tests/conftest.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
PaginatedItems,
2525
PaginatedResults,
2626
)
27-
from tests.utils.http_mock import RequestsMock
2827
from todoist_api_python.api import TodoistAPI
2928
from todoist_api_python.api_async import TodoistAPIAsync
3029
from todoist_api_python.models import (
@@ -40,26 +39,15 @@
4039
if TYPE_CHECKING:
4140
from collections.abc import AsyncIterator, Iterator
4241

43-
import respx
44-
45-
46-
@pytest.fixture
47-
def requests_mock(respx_mock: respx.MockRouter) -> Iterator[RequestsMock]:
48-
mock = RequestsMock(respx_mock)
49-
yield mock
50-
mock.assert_all_called()
51-
5242

5343
@pytest.fixture
54-
def todoist_api(respx_mock: respx.MockRouter) -> Iterator[TodoistAPI]:
44+
def todoist_api() -> Iterator[TodoistAPI]:
5545
with TodoistAPI(DEFAULT_TOKEN) as api:
5646
yield api
5747

5848

5949
@pytest_asyncio.fixture
60-
async def todoist_api_async(
61-
respx_mock: respx.MockRouter,
62-
) -> AsyncIterator[TodoistAPIAsync]:
50+
async def todoist_api_async() -> AsyncIterator[TodoistAPIAsync]:
6351
async with TodoistAPIAsync(DEFAULT_TOKEN) as api:
6452
yield api
6553

tests/test_api_async_client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import warnings
5+
6+
from tests.data.test_defaults import DEFAULT_TOKEN
7+
from todoist_api_python.api_async import TodoistAPIAsync
8+
9+
10+
def test_warns_if_async_client_is_not_closed() -> None:
11+
api = TodoistAPIAsync(DEFAULT_TOKEN)
12+
13+
with warnings.catch_warnings(record=True) as caught:
14+
warnings.simplefilter("always", ResourceWarning)
15+
api.__del__()
16+
17+
assert any(item.category is ResourceWarning for item in caught)
18+
19+
asyncio.run(api.close())

tests/test_api_comments.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
auth_matcher,
1313
data_matcher,
1414
enumerate_async,
15-
param_matcher,
15+
mock_route,
1616
request_id_matcher,
1717
)
1818
from todoist_api_python.models import Attachment
1919

2020
if TYPE_CHECKING:
21-
from tests.utils.http_mock import RequestsMock
21+
import respx
22+
2223
from todoist_api_python.api import TodoistAPI
2324
from todoist_api_python.api_async import TodoistAPIAsync
2425

@@ -29,37 +30,38 @@
2930
async def test_get_comment(
3031
todoist_api: TodoistAPI,
3132
todoist_api_async: TodoistAPIAsync,
32-
requests_mock: RequestsMock,
33+
respx_mock: respx.MockRouter,
3334
default_comment_response: dict[str, Any],
3435
default_comment: Comment,
3536
) -> None:
3637
comment_id = "6X7rM8997g3RQmvh"
3738
endpoint = f"{DEFAULT_API_URL}/comments/{comment_id}"
3839

39-
requests_mock.add(
40+
mock_route(
41+
respx_mock,
4042
method="GET",
4143
url=endpoint,
4244
json=default_comment_response,
4345
status=200,
44-
match=[auth_matcher(), request_id_matcher()],
46+
matchers=[auth_matcher(), request_id_matcher()],
4547
)
4648

4749
comment = todoist_api.get_comment(comment_id)
4850

49-
assert len(requests_mock.calls) == 1
51+
assert len(respx_mock.calls) == 1
5052
assert comment == default_comment
5153

5254
comment = await todoist_api_async.get_comment(comment_id)
5355

54-
assert len(requests_mock.calls) == 2
56+
assert len(respx_mock.calls) == 2
5557
assert comment == default_comment
5658

5759

5860
@pytest.mark.asyncio
5961
async def test_get_comments(
6062
todoist_api: TodoistAPI,
6163
todoist_api_async: TodoistAPIAsync,
62-
requests_mock: RequestsMock,
64+
respx_mock: respx.MockRouter,
6365
default_comments_response: list[PaginatedResults],
6466
default_comments_list: list[list[Comment]],
6567
) -> None:
@@ -68,15 +70,16 @@ async def test_get_comments(
6870

6971
cursor: str | None = None
7072
for page in default_comments_response:
71-
requests_mock.add(
73+
mock_route(
74+
respx_mock,
7275
method="GET",
7376
url=endpoint,
7477
json=page,
7578
status=200,
76-
match=[
79+
params={"task_id": task_id} | ({"cursor": cursor} if cursor else {}),
80+
matchers=[
7781
auth_matcher(),
7882
request_id_matcher(),
79-
param_matcher({"task_id": task_id}, cursor),
8083
],
8184
)
8285
cursor = page["next_cursor"]
@@ -86,14 +89,14 @@ async def test_get_comments(
8689
comments_iter = todoist_api.get_comments(task_id=task_id)
8790

8891
for i, comments in enumerate(comments_iter):
89-
assert len(requests_mock.calls) == count + 1
92+
assert len(respx_mock.calls) == count + 1
9093
assert comments == default_comments_list[i]
9194
count += 1
9295

9396
comments_async_iter = await todoist_api_async.get_comments(task_id=task_id)
9497

9598
async for i, comments in enumerate_async(comments_async_iter):
96-
assert len(requests_mock.calls) == count + 1
99+
assert len(respx_mock.calls) == count + 1
97100
assert comments == default_comments_list[i]
98101
count += 1
99102

@@ -102,7 +105,7 @@ async def test_get_comments(
102105
async def test_add_comment(
103106
todoist_api: TodoistAPI,
104107
todoist_api_async: TodoistAPIAsync,
105-
requests_mock: RequestsMock,
108+
respx_mock: respx.MockRouter,
106109
default_comment_response: dict[str, Any],
107110
default_comment: Comment,
108111
) -> None:
@@ -115,12 +118,13 @@ async def test_add_comment(
115118
file_name="File.pdf",
116119
)
117120

118-
requests_mock.add(
121+
mock_route(
122+
respx_mock,
119123
method="POST",
120124
url=f"{DEFAULT_API_URL}/comments",
121125
json=default_comment_response,
122126
status=200,
123-
match=[
127+
matchers=[
124128
auth_matcher(),
125129
request_id_matcher(),
126130
data_matcher(
@@ -139,7 +143,7 @@ async def test_add_comment(
139143
attachment=attachment,
140144
)
141145

142-
assert len(requests_mock.calls) == 1
146+
assert len(respx_mock.calls) == 1
143147
assert new_comment == default_comment
144148

145149
new_comment = await todoist_api_async.add_comment(
@@ -148,65 +152,67 @@ async def test_add_comment(
148152
attachment=attachment,
149153
)
150154

151-
assert len(requests_mock.calls) == 2
155+
assert len(respx_mock.calls) == 2
152156
assert new_comment == default_comment
153157

154158

155159
@pytest.mark.asyncio
156160
async def test_update_comment(
157161
todoist_api: TodoistAPI,
158162
todoist_api_async: TodoistAPIAsync,
159-
requests_mock: RequestsMock,
163+
respx_mock: respx.MockRouter,
160164
default_comment: Comment,
161165
) -> None:
162166
args = {
163167
"content": "An updated comment",
164168
}
165169
updated_comment_dict = default_comment.to_dict() | args
166170

167-
requests_mock.add(
171+
mock_route(
172+
respx_mock,
168173
method="POST",
169174
url=f"{DEFAULT_API_URL}/comments/{default_comment.id}",
170175
json=updated_comment_dict,
171176
status=200,
172-
match=[auth_matcher(), request_id_matcher(), data_matcher(args)],
177+
matchers=[auth_matcher(), request_id_matcher(), data_matcher(args)],
173178
)
174179

175180
response = todoist_api.update_comment(comment_id=default_comment.id, **args)
176181

177-
assert len(requests_mock.calls) == 1
182+
assert len(respx_mock.calls) == 1
178183
assert response == Comment.from_dict(updated_comment_dict)
179184

180185
response = await todoist_api_async.update_comment(
181186
comment_id=default_comment.id, **args
182187
)
183188

184-
assert len(requests_mock.calls) == 2
189+
assert len(respx_mock.calls) == 2
185190
assert response == Comment.from_dict(updated_comment_dict)
186191

187192

188193
@pytest.mark.asyncio
189194
async def test_delete_comment(
190195
todoist_api: TodoistAPI,
191196
todoist_api_async: TodoistAPIAsync,
192-
requests_mock: RequestsMock,
197+
respx_mock: respx.MockRouter,
193198
) -> None:
194199
comment_id = "6X7rM8997g3RQmvh"
195200
endpoint = f"{DEFAULT_API_URL}/comments/{comment_id}"
196201

197-
requests_mock.add(
202+
mock_route(
203+
respx_mock,
198204
method="DELETE",
199205
url=endpoint,
200206
status=204,
201-
match=[auth_matcher(), request_id_matcher()],
207+
matchers=[auth_matcher(), request_id_matcher()],
202208
)
203209

204210
response = todoist_api.delete_comment(comment_id)
205211

206-
assert len(requests_mock.calls) == 1
212+
assert len(respx_mock.calls) == 1
207213
assert response is True
208214

209215
response = await todoist_api_async.delete_comment(comment_id)
210216

211-
assert len(requests_mock.calls) == 2
217+
assert len(respx_mock.calls) == 2
212218
assert response is True

tests/test_api_completed_tasks.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
from tests.utils.test_utils import (
1616
auth_matcher,
1717
enumerate_async,
18-
param_matcher,
18+
mock_route,
1919
request_id_matcher,
2020
)
2121
from todoist_api_python._core.utils import format_datetime
2222

2323
if TYPE_CHECKING:
24-
from tests.utils.http_mock import RequestsMock
24+
import respx
25+
2526
from todoist_api_python.api import TodoistAPI
2627
from todoist_api_python.api_async import TodoistAPIAsync
2728
from todoist_api_python.models import Task
@@ -31,7 +32,7 @@
3132
async def test_get_completed_tasks_by_due_date(
3233
todoist_api: TodoistAPI,
3334
todoist_api_async: TodoistAPIAsync,
34-
requests_mock: RequestsMock,
35+
respx_mock: respx.MockRouter,
3536
default_completed_tasks_response: list[PaginatedItems],
3637
default_completed_tasks_list: list[list[Task]],
3738
) -> None:
@@ -51,12 +52,17 @@ async def test_get_completed_tasks_by_due_date(
5152

5253
cursor: str | None = None
5354
for page in default_completed_tasks_response:
54-
requests_mock.add(
55+
mock_route(
56+
respx_mock,
5557
method="GET",
5658
url=endpoint,
5759
json=page,
5860
status=200,
59-
match=[auth_matcher(), request_id_matcher(), param_matcher(params, cursor)],
61+
params=params | ({"cursor": cursor} if cursor else {}),
62+
matchers=[
63+
auth_matcher(),
64+
request_id_matcher(),
65+
],
6066
)
6167
cursor = page["next_cursor"]
6268

@@ -70,7 +76,7 @@ async def test_get_completed_tasks_by_due_date(
7076
)
7177

7278
for i, tasks in enumerate(tasks_iter):
73-
assert len(requests_mock.calls) == count + 1
79+
assert len(respx_mock.calls) == count + 1
7480
assert tasks == default_completed_tasks_list[i]
7581
count += 1
7682

@@ -82,7 +88,7 @@ async def test_get_completed_tasks_by_due_date(
8288
)
8389

8490
async for i, tasks in enumerate_async(tasks_async_iter):
85-
assert len(requests_mock.calls) == count + 1
91+
assert len(respx_mock.calls) == count + 1
8692
assert tasks == default_completed_tasks_list[i]
8793
count += 1
8894

@@ -91,7 +97,7 @@ async def test_get_completed_tasks_by_due_date(
9197
async def test_get_completed_tasks_by_completion_date(
9298
todoist_api: TodoistAPI,
9399
todoist_api_async: TodoistAPIAsync,
94-
requests_mock: RequestsMock,
100+
respx_mock: respx.MockRouter,
95101
default_completed_tasks_response: list[PaginatedItems],
96102
default_completed_tasks_list: list[list[Task]],
97103
) -> None:
@@ -111,12 +117,17 @@ async def test_get_completed_tasks_by_completion_date(
111117

112118
cursor: str | None = None
113119
for page in default_completed_tasks_response:
114-
requests_mock.add(
120+
mock_route(
121+
respx_mock,
115122
method="GET",
116123
url=endpoint,
117124
json=page,
118125
status=200,
119-
match=[auth_matcher(), request_id_matcher(), param_matcher(params, cursor)],
126+
params=params | ({"cursor": cursor} if cursor else {}),
127+
matchers=[
128+
auth_matcher(),
129+
request_id_matcher(),
130+
],
120131
)
121132
cursor = page["next_cursor"]
122133

@@ -130,7 +141,7 @@ async def test_get_completed_tasks_by_completion_date(
130141
)
131142

132143
for i, tasks in enumerate(tasks_iter):
133-
assert len(requests_mock.calls) == count + 1
144+
assert len(respx_mock.calls) == count + 1
134145
assert tasks == default_completed_tasks_list[i]
135146
count += 1
136147

@@ -142,6 +153,6 @@ async def test_get_completed_tasks_by_completion_date(
142153
)
143154

144155
async for i, tasks in enumerate_async(tasks_async_iter):
145-
assert len(requests_mock.calls) == count + 1
156+
assert len(respx_mock.calls) == count + 1
146157
assert tasks == default_completed_tasks_list[i]
147158
count += 1

0 commit comments

Comments
 (0)