Skip to content

Commit 8403c89

Browse files
authored
Implement get_completed_items (#90)
1 parent d2d3908 commit 8403c89

File tree

8 files changed

+307
-2
lines changed

8 files changed

+307
-2
lines changed

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
DEFAULT_COLLABORATORS_RESPONSE,
1010
DEFAULT_COMMENT_RESPONSE,
1111
DEFAULT_COMMENTS_RESPONSE,
12+
DEFAULT_COMPLETED_ITEMS_RESPONSE,
1213
DEFAULT_LABEL_RESPONSE,
1314
DEFAULT_LABELS_RESPONSE,
1415
DEFAULT_PROJECT_RESPONSE,
@@ -25,6 +26,7 @@
2526
AuthResult,
2627
Collaborator,
2728
Comment,
29+
CompletedItems,
2830
Label,
2931
Project,
3032
QuickAddResult,
@@ -177,3 +179,13 @@ def default_auth_response() -> Dict[str, Any]:
177179
@pytest.fixture()
178180
def default_auth_result() -> AuthResult:
179181
return AuthResult.from_dict(DEFAULT_AUTH_RESPONSE)
182+
183+
184+
@pytest.fixture()
185+
def default_completed_items_response() -> dict[str, Any]:
186+
return DEFAULT_COMPLETED_ITEMS_RESPONSE
187+
188+
189+
@pytest.fixture()
190+
def default_completed_items() -> CompletedItems:
191+
return CompletedItems.from_dict(DEFAULT_COMPLETED_ITEMS_RESPONSE)

tests/data/test_defaults.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
23
from typing import Any
34

45
REST_API_BASE_URL = "https://api.todoist.com/rest/v2"
@@ -148,3 +149,32 @@
148149
"access_token": "1234",
149150
"state": "somestate",
150151
}
152+
153+
DEFAULT_ITEM_RESPONSE = {
154+
"id": "2995104339",
155+
"user_id": "2671355",
156+
"project_id": "2203306141",
157+
"content": "Buy Milk",
158+
"description": "",
159+
"priority": 1,
160+
"due": DEFAULT_DUE_RESPONSE,
161+
"child_order": 1,
162+
"day_order": -1,
163+
"collapsed": False,
164+
"labels": ["Food", "Shopping"],
165+
"added_by_uid": "2671355",
166+
"assigned_by_uid": "2671355",
167+
"checked": False,
168+
"is_deleted": False,
169+
"added_at": "2014-09-26T08:25:05.000000Z",
170+
}
171+
172+
DEFAULT_ITEM_COMPLETED_INFO_RESPONSE = {"item_id": "2995104339", "completed_items": 12}
173+
174+
DEFAULT_COMPLETED_ITEMS_RESPONSE = {
175+
"items": [DEFAULT_ITEM_RESPONSE],
176+
"completed_info": [DEFAULT_ITEM_COMPLETED_INFO_RESPONSE],
177+
"total": 22,
178+
"next_cursor": "k85gVI5ZAs8AAAABFoOzAQ",
179+
"has_more": True,
180+
}

tests/test_api_items.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from typing import Any
2+
from urllib.parse import parse_qs, urlparse
3+
4+
import pytest
5+
import responses
6+
7+
from tests.data.test_defaults import SYNC_API_BASE_URL
8+
from tests.utils.test_utils import assert_auth_header
9+
from todoist_api_python.api import TodoistAPI
10+
from todoist_api_python.api_async import TodoistAPIAsync
11+
from todoist_api_python.endpoints import COMPLETED_ITEMS_ENDPOINT
12+
from todoist_api_python.models import CompletedItems
13+
14+
15+
@pytest.mark.asyncio
16+
async def test_get_completed_items(
17+
todoist_api: TodoistAPI,
18+
todoist_api_async: TodoistAPIAsync,
19+
requests_mock: responses.RequestsMock,
20+
default_completed_items_response: dict[str, Any],
21+
default_completed_items: CompletedItems,
22+
) -> None:
23+
project_id = "1234"
24+
section_id = "5678"
25+
item_id = "90ab"
26+
last_seen_id = "cdef"
27+
limit = 30
28+
cursor = "ghij"
29+
30+
def assert_query(url):
31+
queries = parse_qs(urlparse(url).query)
32+
assert queries.get("project_id") == [project_id]
33+
assert queries.get("section_id") == [section_id]
34+
assert queries.get("item_id") == [item_id]
35+
assert queries.get("last_seen_id") == [last_seen_id]
36+
assert queries.get("limit") == [str(limit)]
37+
assert queries.get("cursor") == [cursor]
38+
39+
expected_endpoint = f"{SYNC_API_BASE_URL}/{COMPLETED_ITEMS_ENDPOINT}"
40+
41+
requests_mock.add(
42+
responses.GET,
43+
expected_endpoint,
44+
json=default_completed_items_response,
45+
status=200,
46+
)
47+
48+
completed_items = todoist_api.get_completed_items(
49+
project_id, section_id, item_id, last_seen_id, limit, cursor
50+
)
51+
52+
assert len(requests_mock.calls) == 1
53+
assert_auth_header(requests_mock.calls[0].request)
54+
assert_query(requests_mock.calls[0].request.url)
55+
assert completed_items == default_completed_items
56+
57+
completed_items = await todoist_api_async.get_completed_items(
58+
project_id, section_id, item_id, last_seen_id, limit, cursor
59+
)
60+
61+
assert len(requests_mock.calls) == 2
62+
assert_auth_header(requests_mock.calls[1].request)
63+
assert_query(requests_mock.calls[1].request.url)
64+
assert completed_items == default_completed_items

tests/test_models.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
DEFAULT_ATTACHMENT_RESPONSE,
77
DEFAULT_COLLABORATOR_RESPONSE,
88
DEFAULT_COMMENT_RESPONSE,
9+
DEFAULT_COMPLETED_ITEMS_RESPONSE,
910
DEFAULT_DUE_RESPONSE,
11+
DEFAULT_ITEM_COMPLETED_INFO_RESPONSE,
12+
DEFAULT_ITEM_RESPONSE,
1013
DEFAULT_LABEL_RESPONSE,
1114
DEFAULT_PROJECT_RESPONSE,
1215
DEFAULT_SECTION_RESPONSE,
@@ -17,7 +20,10 @@
1720
AuthResult,
1821
Collaborator,
1922
Comment,
23+
CompletedItems,
2024
Due,
25+
Item,
26+
ItemCompletedInfo,
2127
Label,
2228
Project,
2329
QuickAddResult,
@@ -280,3 +286,87 @@ def test_auth_result_from_dict():
280286

281287
assert auth_result.access_token == token
282288
assert auth_result.state == state
289+
290+
291+
def test_item_from_dict():
292+
sample_data = dict(DEFAULT_ITEM_RESPONSE)
293+
sample_data.update(unexpected_data)
294+
295+
item = Item.from_dict(sample_data)
296+
297+
assert item.id == "2995104339"
298+
assert item.user_id == "2671355"
299+
assert item.project_id == "2203306141"
300+
assert item.content == "Buy Milk"
301+
assert item.description == ""
302+
assert item.priority == 1
303+
assert item.due.date == DEFAULT_DUE_RESPONSE["date"]
304+
assert item.due.is_recurring == DEFAULT_DUE_RESPONSE["is_recurring"]
305+
assert item.due.string == DEFAULT_DUE_RESPONSE["string"]
306+
assert item.due.datetime == DEFAULT_DUE_RESPONSE["datetime"]
307+
assert item.due.timezone == DEFAULT_DUE_RESPONSE["timezone"]
308+
assert item.parent_id is None
309+
assert item.child_order == 1
310+
assert item.section_id is None
311+
assert item.day_order == -1
312+
assert item.collapsed is False
313+
assert item.labels == ["Food", "Shopping"]
314+
assert item.added_by_uid == "2671355"
315+
assert item.assigned_by_uid == "2671355"
316+
assert item.responsible_uid is None
317+
assert item.checked is False
318+
assert item.is_deleted is False
319+
assert item.sync_id is None
320+
assert item.added_at == "2014-09-26T08:25:05.000000Z"
321+
322+
323+
def test_item_completed_info_from_dict():
324+
sample_data = dict(DEFAULT_ITEM_COMPLETED_INFO_RESPONSE)
325+
sample_data.update(unexpected_data)
326+
327+
info = ItemCompletedInfo.from_dict(sample_data)
328+
329+
assert info.item_id == "2995104339"
330+
assert info.completed_items == 12
331+
332+
333+
def test_completed_items_from_dict():
334+
sample_data = dict(DEFAULT_COMPLETED_ITEMS_RESPONSE)
335+
sample_data.update(unexpected_data)
336+
337+
completed_items = CompletedItems.from_dict(sample_data)
338+
339+
assert completed_items.total == 22
340+
assert completed_items.next_cursor == "k85gVI5ZAs8AAAABFoOzAQ"
341+
assert completed_items.has_more is True
342+
assert len(completed_items.items) == 1
343+
assert completed_items.items[0].id == "2995104339"
344+
assert completed_items.items[0].user_id == "2671355"
345+
assert completed_items.items[0].project_id == "2203306141"
346+
assert completed_items.items[0].content == "Buy Milk"
347+
assert completed_items.items[0].description == ""
348+
assert completed_items.items[0].priority == 1
349+
assert completed_items.items[0].due.date == DEFAULT_DUE_RESPONSE["date"]
350+
assert (
351+
completed_items.items[0].due.is_recurring
352+
== DEFAULT_DUE_RESPONSE["is_recurring"]
353+
)
354+
assert completed_items.items[0].due.string == DEFAULT_DUE_RESPONSE["string"]
355+
assert completed_items.items[0].due.datetime == DEFAULT_DUE_RESPONSE["datetime"]
356+
assert completed_items.items[0].due.timezone == DEFAULT_DUE_RESPONSE["timezone"]
357+
assert completed_items.items[0].parent_id is None
358+
assert completed_items.items[0].child_order == 1
359+
assert completed_items.items[0].section_id is None
360+
assert completed_items.items[0].day_order == -1
361+
assert completed_items.items[0].collapsed is False
362+
assert completed_items.items[0].labels == ["Food", "Shopping"]
363+
assert completed_items.items[0].added_by_uid == "2671355"
364+
assert completed_items.items[0].assigned_by_uid == "2671355"
365+
assert completed_items.items[0].responsible_uid is None
366+
assert completed_items.items[0].checked is False
367+
assert completed_items.items[0].is_deleted is False
368+
assert completed_items.items[0].sync_id is None
369+
assert completed_items.items[0].added_at == "2014-09-26T08:25:05.000000Z"
370+
assert len(completed_items.completed_info) == 1
371+
assert completed_items.completed_info[0].item_id == "2995104339"
372+
assert completed_items.completed_info[0].completed_items == 12

todoist_api_python/api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from todoist_api_python.endpoints import (
88
COLLABORATORS_ENDPOINT,
99
COMMENTS_ENDPOINT,
10+
COMPLETED_ITEMS_ENDPOINT,
1011
LABELS_ENDPOINT,
1112
PROJECTS_ENDPOINT,
1213
QUICK_ADD_ENDPOINT,
@@ -22,6 +23,7 @@
2223
from todoist_api_python.models import (
2324
Collaborator,
2425
Comment,
26+
CompletedItems,
2527
Label,
2628
Project,
2729
QuickAddResult,
@@ -207,3 +209,28 @@ def remove_shared_label(self, name: str) -> bool:
207209
endpoint = get_rest_url(SHARED_LABELS_REMOVE_ENDPOINT)
208210
data = {"name": name}
209211
return post(self._session, endpoint, self._token, data=data)
212+
213+
def get_completed_items(
214+
self,
215+
project_id: str | None = None,
216+
section_id: str | None = None,
217+
item_id: str | None = None,
218+
last_seen_id: str | None = None,
219+
limit: int | None = None,
220+
cursor: str | None = None,
221+
) -> CompletedItems:
222+
endpoint = get_sync_url(COMPLETED_ITEMS_ENDPOINT)
223+
completed_items = get(
224+
self._session,
225+
endpoint,
226+
self._token,
227+
{
228+
"project_id": project_id,
229+
"section_id": section_id,
230+
"item_id": item_id,
231+
"last_seen_id": last_seen_id,
232+
"limit": limit,
233+
"cursor": cursor,
234+
},
235+
)
236+
return CompletedItems.from_dict(completed_items)

todoist_api_python/api_async.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from todoist_api_python.models import (
77
Collaborator,
88
Comment,
9+
CompletedItems,
910
Label,
1011
Project,
1112
QuickAddResult,
@@ -120,3 +121,18 @@ async def rename_shared_label(self, name: str, new_name: str) -> bool:
120121

121122
async def remove_shared_label(self, name: str) -> bool:
122123
return await run_async(lambda: self._api.remove_shared_label(name))
124+
125+
async def get_completed_items(
126+
self,
127+
project_id: str | None = None,
128+
section_id: str | None = None,
129+
item_id: str | None = None,
130+
last_seen_id: str | None = None,
131+
limit: int | None = None,
132+
cursor: str | None = None,
133+
) -> CompletedItems:
134+
return await run_async(
135+
lambda: self._api.get_completed_items(
136+
project_id, section_id, item_id, last_seen_id, limit, cursor
137+
)
138+
)

todoist_api_python/endpoints.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
TOKEN_ENDPOINT = "oauth/access_token"
2525
REVOKE_TOKEN_ENDPOINT = "access_tokens/revoke"
2626

27+
COMPLETED_ITEMS_ENDPOINT = "archive/items"
28+
2729

2830
def get_rest_url(relative_path: str) -> str:
2931
return urljoin(REST_API, relative_path)

0 commit comments

Comments
 (0)