Skip to content

Commit fbd8a70

Browse files
Merge pull request #187 from Doist/goncalossilva/archive
2 parents 0d79424 + ab7c22c commit fbd8a70

File tree

6 files changed

+133
-0
lines changed

6 files changed

+133
-0
lines changed

tests/data/test_defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class PaginatedItems(TypedDict):
5959
"is_favorite": False,
6060
"is_inbox_project": True,
6161
"can_assign_tasks": False,
62+
"is_archived": False,
6263
"view_style": "list",
6364
"created_at": "2023-02-01T00:00:00.000000Z",
6465
"updated_at": "2025-04-03T03:14:15.926536Z",

tests/test_api_projects.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,70 @@ async def test_update_project(
180180
assert response == Project.from_dict(updated_project_dict)
181181

182182

183+
@pytest.mark.asyncio
184+
async def test_archive_project(
185+
todoist_api: TodoistAPI,
186+
todoist_api_async: TodoistAPIAsync,
187+
requests_mock: responses.RequestsMock,
188+
default_project: Project,
189+
) -> None:
190+
project_id = default_project.id
191+
endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/archive"
192+
193+
archived_project_dict = default_project.to_dict()
194+
archived_project_dict["is_archived"] = True
195+
196+
requests_mock.add(
197+
method=responses.POST,
198+
url=endpoint,
199+
json=archived_project_dict,
200+
status=200,
201+
match=[auth_matcher()],
202+
)
203+
204+
project = todoist_api.archive_project(project_id)
205+
206+
assert len(requests_mock.calls) == 1
207+
assert project == Project.from_dict(archived_project_dict)
208+
209+
project = await todoist_api_async.archive_project(project_id)
210+
211+
assert len(requests_mock.calls) == 2
212+
assert project == Project.from_dict(archived_project_dict)
213+
214+
215+
@pytest.mark.asyncio
216+
async def test_unarchive_project(
217+
todoist_api: TodoistAPI,
218+
todoist_api_async: TodoistAPIAsync,
219+
requests_mock: responses.RequestsMock,
220+
default_project: Project,
221+
) -> None:
222+
project_id = default_project.id
223+
endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/unarchive"
224+
225+
unarchived_project_dict = default_project.to_dict()
226+
unarchived_project_dict["is_archived"] = False
227+
228+
requests_mock.add(
229+
method=responses.POST,
230+
url=endpoint,
231+
json=unarchived_project_dict,
232+
status=200,
233+
match=[auth_matcher()],
234+
)
235+
236+
project = todoist_api.unarchive_project(project_id)
237+
238+
assert len(requests_mock.calls) == 1
239+
assert project == Project.from_dict(unarchived_project_dict)
240+
241+
project = await todoist_api_async.unarchive_project(project_id)
242+
243+
assert len(requests_mock.calls) == 2
244+
assert project == Project.from_dict(unarchived_project_dict)
245+
246+
183247
@pytest.mark.asyncio
184248
async def test_delete_project(
185249
todoist_api: TodoistAPI,

todoist_api_python/_core/endpoints.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
TASKS_COMPLETED_BY_DUE_DATE_PATH = f"{TASKS_COMPLETED_PATH}/by_due_date"
1919
TASKS_COMPLETED_BY_COMPLETION_DATE_PATH = f"{TASKS_COMPLETED_PATH}/by_completion_date"
2020
PROJECTS_PATH = "projects"
21+
PROJECT_ARCHIVE_PATH_SUFFIX = "archive"
22+
PROJECT_UNARCHIVE_PATH_SUFFIX = "unarchive"
2123
COLLABORATORS_PATH = "collaborators"
2224
SECTIONS_PATH = "sections"
2325
COMMENTS_PATH = "comments"

todoist_api_python/api.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
COLLABORATORS_PATH,
1212
COMMENTS_PATH,
1313
LABELS_PATH,
14+
PROJECT_ARCHIVE_PATH_SUFFIX,
15+
PROJECT_UNARCHIVE_PATH_SUFFIX,
1416
PROJECTS_PATH,
1517
SECTIONS_PATH,
1618
SHARED_LABELS_PATH,
@@ -712,6 +714,41 @@ def update_project(
712714
)
713715
return Project.from_dict(project_data)
714716

717+
def archive_project(self, project_id: str) -> Project:
718+
"""
719+
Archive a project.
720+
721+
For personal projects, archives it only for the user.
722+
For workspace projects, archives it for all members.
723+
724+
:param project_id: The ID of the project to archive.
725+
:return: The archived project object.
726+
:raises requests.exceptions.HTTPError: If the API request fails.
727+
:raises TypeError: If the API response is not a valid Project dictionary.
728+
"""
729+
endpoint = get_api_url(
730+
f"{PROJECTS_PATH}/{project_id}/{PROJECT_ARCHIVE_PATH_SUFFIX}"
731+
)
732+
project_data: dict[str, Any] = post(self._session, endpoint, self._token)
733+
return Project.from_dict(project_data)
734+
735+
def unarchive_project(self, project_id: str) -> Project:
736+
"""
737+
Unarchive a project.
738+
739+
Restores a previously archived project.
740+
741+
:param project_id: The ID of the project to unarchive.
742+
:return: The unarchived project object.
743+
:raises requests.exceptions.HTTPError: If the API request fails.
744+
:raises TypeError: If the API response is not a valid Project dictionary.
745+
"""
746+
endpoint = get_api_url(
747+
f"{PROJECTS_PATH}/{project_id}/{PROJECT_UNARCHIVE_PATH_SUFFIX}"
748+
)
749+
project_data: dict[str, Any] = post(self._session, endpoint, self._token)
750+
return Project.from_dict(project_data)
751+
715752
def delete_project(self, project_id: str) -> bool:
716753
"""
717754
Delete a project.

todoist_api_python/api_async.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ async def get_tasks(
112112
ids=ids,
113113
limit=limit,
114114
)
115+
115116
return generate_async(paginator)
116117

117118
async def filter_tasks(
@@ -531,6 +532,33 @@ async def update_project(
531532
)
532533
)
533534

535+
async def archive_project(self, project_id: str) -> Project:
536+
"""
537+
Archive a project.
538+
539+
For personal projects, archives it only for the user.
540+
For workspace projects, archives it for all members.
541+
542+
:param project_id: The ID of the project to archive.
543+
:return: The archived project object.
544+
:raises requests.exceptions.HTTPError: If the API request fails.
545+
:raises TypeError: If the API response is not a valid Project dictionary.
546+
"""
547+
return await run_async(lambda: self._api.archive_project(project_id))
548+
549+
async def unarchive_project(self, project_id: str) -> Project:
550+
"""
551+
Unarchive a project.
552+
553+
Restores a previously archived project.
554+
555+
:param project_id: The ID of the project to unarchive.
556+
:return: The unarchived project object.
557+
:raises requests.exceptions.HTTPError: If the API request fails.
558+
:raises TypeError: If the API response is not a valid Project dictionary.
559+
"""
560+
return await run_async(lambda: self._api.unarchive_project(project_id))
561+
534562
async def delete_project(self, project_id: str) -> bool:
535563
"""
536564
Delete a project.

todoist_api_python/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class _(JSONPyWizard.Meta): # noqa:N801
3030
is_collapsed: Annotated[bool, Alias(load=("collapsed", "is_collapsed"))]
3131
is_shared: Annotated[bool, Alias(load=("shared", "is_shared"))]
3232
is_favorite: bool
33+
is_archived: bool
3334
can_assign_tasks: bool
3435
view_style: ViewStyle
3536
created_at: ApiDate

0 commit comments

Comments
 (0)