Skip to content

Commit 2a35e9c

Browse files
Merge pull request #228 from Doist/goncalossilva/order-mapping
2 parents f5179f1 + 4b890a1 commit 2a35e9c

File tree

8 files changed

+228
-47
lines changed

8 files changed

+228
-47
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- **Breaking**: Async paginated return types now use `AsyncIterator[...]` instead of `AsyncGenerator[...]`.
1919
- **Breaking**: API errors now raise `httpx.HTTPStatusError` instead of `requests.exceptions.HTTPError`.
2020
- **Breaking**: Authentication helpers now accept optional `httpx.Client` / `httpx.AsyncClient` instances instead of `session: requests.Session`.
21+
- **Breaking**: `update_section` now accepts only keyword arguments after `section_id`; any one of `name`, `order`, or `collapsed` can be updated in the same call.
2122

2223
## [3.2.1] - 2026-01-22
2324

tests/data/test_defaults.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ class PaginatedItems(TypedDict):
5656
"workspace_id": None,
5757
"child_order": 1,
5858
"color": "red",
59-
"shared": False,
60-
"collapsed": False,
59+
"is_shared": False,
60+
"is_collapsed": False,
6161
"is_favorite": False,
62-
"is_inbox_project": True,
62+
"inbox_project": True,
6363
"can_assign_tasks": False,
6464
"is_archived": False,
6565
"view_style": "list",
@@ -69,12 +69,12 @@ class PaginatedItems(TypedDict):
6969

7070
DEFAULT_PROJECT_RESPONSE_2 = dict(DEFAULT_PROJECT_RESPONSE)
7171
DEFAULT_PROJECT_RESPONSE_2["id"] = "6X7rfFVPjhvv84XG"
72-
DEFAULT_PROJECT_RESPONSE_2["is_inbox_project"] = False
72+
DEFAULT_PROJECT_RESPONSE_2["inbox_project"] = False
7373

7474

7575
DEFAULT_PROJECT_RESPONSE_3 = dict(DEFAULT_PROJECT_RESPONSE)
7676
DEFAULT_PROJECT_RESPONSE_3["id"] = "6X7rfEVP8hvv25ZQ"
77-
DEFAULT_PROJECT_RESPONSE_3["is_inbox_project"] = False
77+
DEFAULT_PROJECT_RESPONSE_3["inbox_project"] = False
7878

7979
DEFAULT_PROJECTS_RESPONSE: list[PaginatedResults] = [
8080
{
@@ -99,8 +99,9 @@ class PaginatedItems(TypedDict):
9999
"due": DEFAULT_DUE_RESPONSE,
100100
"deadline": DEFAULT_DEADLINE_RESPONSE,
101101
"duration": DEFAULT_DURATION_RESPONSE,
102-
"collapsed": False,
102+
"is_collapsed": False,
103103
"child_order": 3,
104+
"day_order": -1,
104105
"responsible_uid": "2423523",
105106
"assigned_by_uid": "2971358",
106107
"completed_at": None,
@@ -179,8 +180,8 @@ class PaginatedItems(TypedDict):
179180
"id": "6X7rM8997g3RQmvh",
180181
"project_id": "4567",
181182
"name": "A Section",
182-
"collapsed": False,
183-
"order": 1,
183+
"is_collapsed": False,
184+
"section_order": 1,
184185
}
185186

186187
DEFAULT_SECTION_RESPONSE_2 = dict(DEFAULT_SECTION_RESPONSE)
@@ -219,18 +220,17 @@ class PaginatedItems(TypedDict):
219220
"content": "A comment",
220221
"posted_uid": "34567",
221222
"posted_at": "2019-09-22T07:00:00.000000Z",
222-
"task_id": "6X7rM8997g3RQmvh",
223-
"project_id": "6X7rfEVP8hvv25ZQ",
224-
"attachment": DEFAULT_ATTACHMENT_RESPONSE,
223+
"item_id": "6X7rM8997g3RQmvh",
224+
"file_attachment": DEFAULT_ATTACHMENT_RESPONSE,
225225
}
226226

227227
DEFAULT_COMMENT_RESPONSE_2 = dict(DEFAULT_COMMENT_RESPONSE)
228228
DEFAULT_COMMENT_RESPONSE_2["id"] = "6X7rfFVPjhvv84XG"
229-
DEFAULT_COMMENT_RESPONSE_2["attachment"] = None
229+
DEFAULT_COMMENT_RESPONSE_2["file_attachment"] = None
230230

231231
DEFAULT_COMMENT_RESPONSE_3 = dict(DEFAULT_COMMENT_RESPONSE)
232232
DEFAULT_COMMENT_RESPONSE_3["id"] = "6X7rfFVPjhvv65HG"
233-
DEFAULT_COMMENT_RESPONSE_3["attachment"] = None
233+
DEFAULT_COMMENT_RESPONSE_3["file_attachment"] = None
234234

235235
DEFAULT_COMMENTS_RESPONSE: list[PaginatedResults] = [
236236
{

tests/test_api_projects.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import json
34
from typing import TYPE_CHECKING, Any
45

56
import pytest
@@ -202,35 +203,83 @@ async def test_update_project(
202203
todoist_api_async: TodoistAPIAsync,
203204
respx_mock: respx.MockRouter,
204205
default_project: Project,
206+
default_project_response: dict[str, Any],
205207
) -> None:
206208
args: dict[str, Any] = {
207209
"name": "An updated project",
208210
"color": "red",
209211
"is_favorite": False,
210212
}
211-
updated_project_dict = default_project.to_dict() | args
213+
updated_project_response = dict(default_project_response) | args
212214

213215
mock_route(
214216
respx_mock,
215217
method="POST",
216218
url=f"{DEFAULT_API_URL}/projects/{default_project.id}",
217219
request_headers=api_headers(),
218220
request_json=args,
219-
response_json=updated_project_dict,
221+
response_json=updated_project_response,
222+
response_status=200,
223+
)
224+
225+
response = todoist_api.update_project(project_id=default_project.id, **args)
226+
227+
assert len(respx_mock.calls) == 1
228+
assert response == Project.from_dict(updated_project_response)
229+
230+
response = await todoist_api_async.update_project(
231+
project_id=default_project.id, **args
232+
)
233+
234+
assert len(respx_mock.calls) == 2
235+
assert response == Project.from_dict(updated_project_response)
236+
237+
238+
@pytest.mark.asyncio
239+
async def test_update_project_payload_mapping(
240+
todoist_api: TodoistAPI,
241+
todoist_api_async: TodoistAPIAsync,
242+
respx_mock: respx.MockRouter,
243+
default_project: Project,
244+
default_project_response: dict[str, Any],
245+
) -> None:
246+
args: dict[str, Any] = {
247+
"order": 3,
248+
"collapsed": True,
249+
}
250+
expected_payload: dict[str, Any] = {
251+
"child_order": 3,
252+
"is_collapsed": True,
253+
}
254+
updated_project_response = dict(default_project_response) | {
255+
"child_order": args["order"],
256+
"is_collapsed": args["collapsed"],
257+
}
258+
259+
mock_route(
260+
respx_mock,
261+
method="POST",
262+
url=f"{DEFAULT_API_URL}/projects/{default_project.id}",
263+
request_headers=api_headers(),
264+
response_json=updated_project_response,
220265
response_status=200,
221266
)
222267

223268
response = todoist_api.update_project(project_id=default_project.id, **args)
224269

225270
assert len(respx_mock.calls) == 1
226-
assert response == Project.from_dict(updated_project_dict)
271+
assert response == Project.from_dict(updated_project_response)
272+
actual_payload = json.loads(respx_mock.calls[0].request.content)
273+
assert actual_payload == expected_payload
227274

228275
response = await todoist_api_async.update_project(
229276
project_id=default_project.id, **args
230277
)
231278

232279
assert len(respx_mock.calls) == 2
233-
assert response == Project.from_dict(updated_project_dict)
280+
assert response == Project.from_dict(updated_project_response)
281+
actual_payload = json.loads(respx_mock.calls[1].request.content)
282+
assert actual_payload == expected_payload
234283

235284

236285
@pytest.mark.asyncio

tests/test_api_sections.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import json
34
from typing import TYPE_CHECKING, Any
45

56
import pytest
@@ -259,33 +260,81 @@ async def test_update_section(
259260
todoist_api_async: TodoistAPIAsync,
260261
respx_mock: respx.MockRouter,
261262
default_section: Section,
263+
default_section_response: dict[str, Any],
262264
) -> None:
263-
args = {
265+
args: dict[str, Any] = {
264266
"name": "An updated section",
265267
}
266-
updated_section_dict = default_section.to_dict() | args
268+
updated_section_response = dict(default_section_response) | args
267269

268270
mock_route(
269271
respx_mock,
270272
method="POST",
271273
url=f"{DEFAULT_API_URL}/sections/{default_section.id}",
272274
request_headers=api_headers(),
273275
request_json=args,
274-
response_json=updated_section_dict,
276+
response_json=updated_section_response,
277+
response_status=200,
278+
)
279+
280+
response = todoist_api.update_section(section_id=default_section.id, **args)
281+
282+
assert len(respx_mock.calls) == 1
283+
assert response == Section.from_dict(updated_section_response)
284+
285+
response = await todoist_api_async.update_section(
286+
section_id=default_section.id, **args
287+
)
288+
289+
assert len(respx_mock.calls) == 2
290+
assert response == Section.from_dict(updated_section_response)
291+
292+
293+
@pytest.mark.asyncio
294+
async def test_update_section_payload_mapping(
295+
todoist_api: TodoistAPI,
296+
todoist_api_async: TodoistAPIAsync,
297+
respx_mock: respx.MockRouter,
298+
default_section: Section,
299+
default_section_response: dict[str, Any],
300+
) -> None:
301+
args: dict[str, Any] = {
302+
"order": 2,
303+
"collapsed": False,
304+
}
305+
expected_payload: dict[str, Any] = {
306+
"section_order": 2,
307+
"is_collapsed": False,
308+
}
309+
updated_section_response = dict(default_section_response) | {
310+
"section_order": args["order"],
311+
"is_collapsed": args["collapsed"],
312+
}
313+
314+
mock_route(
315+
respx_mock,
316+
method="POST",
317+
url=f"{DEFAULT_API_URL}/sections/{default_section.id}",
318+
request_headers=api_headers(),
319+
response_json=updated_section_response,
275320
response_status=200,
276321
)
277322

278323
response = todoist_api.update_section(section_id=default_section.id, **args)
279324

280325
assert len(respx_mock.calls) == 1
281-
assert response == Section.from_dict(updated_section_dict)
326+
assert response == Section.from_dict(updated_section_response)
327+
actual_payload = json.loads(respx_mock.calls[0].request.content)
328+
assert actual_payload == expected_payload
282329

283330
response = await todoist_api_async.update_section(
284331
section_id=default_section.id, **args
285332
)
286333

287334
assert len(respx_mock.calls) == 2
288-
assert response == Section.from_dict(updated_section_dict)
335+
assert response == Section.from_dict(updated_section_response)
336+
actual_payload = json.loads(respx_mock.calls[1].request.content)
337+
assert actual_payload == expected_payload
289338

290339

291340
@pytest.mark.asyncio

tests/test_api_tasks.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import json
34
import sys
45
from datetime import datetime, timezone
56
from typing import TYPE_CHECKING, Any
@@ -353,35 +354,83 @@ async def test_update_task(
353354
todoist_api_async: TodoistAPIAsync,
354355
respx_mock: respx.MockRouter,
355356
default_task: Task,
357+
default_task_response: dict[str, Any],
356358
) -> None:
357359
args: dict[str, Any] = {
358360
"content": "Updated content",
359361
"description": "Updated description",
360362
"labels": ["label1", "label2"],
361363
"priority": 2,
362-
"order": 3,
363364
}
364-
updated_task_dict = default_task.to_dict() | args
365+
updated_task_response = dict(default_task_response) | args
365366

366367
mock_route(
367368
respx_mock,
368369
method="POST",
369370
url=f"{DEFAULT_API_URL}/tasks/{default_task.id}",
370371
request_headers=api_headers(),
371372
request_json=args,
372-
response_json=updated_task_dict,
373+
response_json=updated_task_response,
374+
response_status=200,
375+
)
376+
377+
response = todoist_api.update_task(task_id=default_task.id, **args)
378+
379+
assert len(respx_mock.calls) == 1
380+
assert response == Task.from_dict(updated_task_response)
381+
382+
response = await todoist_api_async.update_task(task_id=default_task.id, **args)
383+
384+
assert len(respx_mock.calls) == 2
385+
assert response == Task.from_dict(updated_task_response)
386+
387+
388+
@pytest.mark.asyncio
389+
async def test_update_task_payload_mapping(
390+
todoist_api: TodoistAPI,
391+
todoist_api_async: TodoistAPIAsync,
392+
respx_mock: respx.MockRouter,
393+
default_task: Task,
394+
default_task_response: dict[str, Any],
395+
) -> None:
396+
args: dict[str, Any] = {
397+
"order": 3,
398+
"day_order": 2,
399+
"collapsed": True,
400+
}
401+
expected_payload: dict[str, Any] = {
402+
"child_order": 3,
403+
"day_order": 2,
404+
"is_collapsed": True,
405+
}
406+
updated_task_response = dict(default_task_response) | {
407+
"child_order": args["order"],
408+
"day_order": args["day_order"],
409+
"is_collapsed": args["collapsed"],
410+
}
411+
412+
mock_route(
413+
respx_mock,
414+
method="POST",
415+
url=f"{DEFAULT_API_URL}/tasks/{default_task.id}",
416+
request_headers=api_headers(),
417+
response_json=updated_task_response,
373418
response_status=200,
374419
)
375420

376421
response = todoist_api.update_task(task_id=default_task.id, **args)
377422

378423
assert len(respx_mock.calls) == 1
379-
assert response == Task.from_dict(updated_task_dict)
424+
assert response == Task.from_dict(updated_task_response)
425+
actual_payload = json.loads(respx_mock.calls[0].request.content)
426+
assert actual_payload == expected_payload
380427

381428
response = await todoist_api_async.update_task(task_id=default_task.id, **args)
382429

383430
assert len(respx_mock.calls) == 2
384-
assert response == Task.from_dict(updated_task_dict)
431+
assert response == Task.from_dict(updated_task_response)
432+
actual_payload = json.loads(respx_mock.calls[1].request.content)
433+
assert actual_payload == expected_payload
385434

386435

387436
@pytest.mark.asyncio

0 commit comments

Comments
 (0)