Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/data/test_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class PaginatedItems(TypedDict):
"is_favorite": False,
"is_inbox_project": True,
"can_assign_tasks": False,
"is_archived": False,
"view_style": "list",
"created_at": "2023-02-01T00:00:00.000000Z",
"updated_at": "2025-04-03T03:14:15.926536Z",
Expand Down
64 changes: 64 additions & 0 deletions tests/test_api_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,70 @@ async def test_update_project(
assert response == Project.from_dict(updated_project_dict)


@pytest.mark.asyncio
async def test_archive_project(
todoist_api: TodoistAPI,
todoist_api_async: TodoistAPIAsync,
requests_mock: responses.RequestsMock,
default_project: Project,
) -> None:
project_id = default_project.id
endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/archive"

archived_project_dict = default_project.to_dict()
archived_project_dict["is_archived"] = True

requests_mock.add(
method=responses.POST,
url=endpoint,
json=archived_project_dict,
status=200,
match=[auth_matcher()],
)

project = todoist_api.archive_project(project_id)

assert len(requests_mock.calls) == 1
assert project == Project.from_dict(archived_project_dict)

project = await todoist_api_async.archive_project(project_id)

assert len(requests_mock.calls) == 2
assert project == Project.from_dict(archived_project_dict)


@pytest.mark.asyncio
async def test_unarchive_project(
todoist_api: TodoistAPI,
todoist_api_async: TodoistAPIAsync,
requests_mock: responses.RequestsMock,
default_project: Project,
) -> None:
project_id = default_project.id
endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/unarchive"

unarchived_project_dict = default_project.to_dict()
unarchived_project_dict["is_archived"] = False

requests_mock.add(
method=responses.POST,
url=endpoint,
json=unarchived_project_dict,
status=200,
match=[auth_matcher()],
)

project = todoist_api.unarchive_project(project_id)

assert len(requests_mock.calls) == 1
assert project == Project.from_dict(unarchived_project_dict)

project = await todoist_api_async.unarchive_project(project_id)

assert len(requests_mock.calls) == 2
assert project == Project.from_dict(unarchived_project_dict)


@pytest.mark.asyncio
async def test_delete_project(
todoist_api: TodoistAPI,
Expand Down
2 changes: 2 additions & 0 deletions todoist_api_python/_core/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
TASKS_COMPLETED_BY_DUE_DATE_PATH = f"{TASKS_COMPLETED_PATH}/by_due_date"
TASKS_COMPLETED_BY_COMPLETION_DATE_PATH = f"{TASKS_COMPLETED_PATH}/by_completion_date"
PROJECTS_PATH = "projects"
PROJECT_ARCHIVE_PATH_SUFFIX = "archive"
PROJECT_UNARCHIVE_PATH_SUFFIX = "unarchive"
COLLABORATORS_PATH = "collaborators"
SECTIONS_PATH = "sections"
COMMENTS_PATH = "comments"
Expand Down
37 changes: 37 additions & 0 deletions todoist_api_python/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
COLLABORATORS_PATH,
COMMENTS_PATH,
LABELS_PATH,
PROJECT_ARCHIVE_PATH_SUFFIX,
PROJECT_UNARCHIVE_PATH_SUFFIX,
PROJECTS_PATH,
SECTIONS_PATH,
SHARED_LABELS_PATH,
Expand Down Expand Up @@ -712,6 +714,41 @@ def update_project(
)
return Project.from_dict(project_data)

def archive_project(self, project_id: str) -> Project:
"""
Archive a project.

For personal projects, archives it only for the user.
For workspace projects, archives it for all members.

:param project_id: The ID of the project to archive.
:return: The archived project object.
:raises requests.exceptions.HTTPError: If the API request fails.
:raises TypeError: If the API response is not a valid Project dictionary.
"""
endpoint = get_api_url(
f"{PROJECTS_PATH}/{project_id}/{PROJECT_ARCHIVE_PATH_SUFFIX}"
)
project_data: dict[str, Any] = post(self._session, endpoint, self._token)
return Project.from_dict(project_data)

def unarchive_project(self, project_id: str) -> Project:
"""
Unarchive a project.

Restores a previously archived project.

:param project_id: The ID of the project to unarchive.
:return: The unarchived project object.
:raises requests.exceptions.HTTPError: If the API request fails.
:raises TypeError: If the API response is not a valid Project dictionary.
"""
endpoint = get_api_url(
f"{PROJECTS_PATH}/{project_id}/{PROJECT_UNARCHIVE_PATH_SUFFIX}"
)
project_data: dict[str, Any] = post(self._session, endpoint, self._token)
return Project.from_dict(project_data)

def delete_project(self, project_id: str) -> bool:
"""
Delete a project.
Expand Down
28 changes: 28 additions & 0 deletions todoist_api_python/api_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ async def get_tasks(
ids=ids,
limit=limit,
)

return generate_async(paginator)

async def filter_tasks(
Expand Down Expand Up @@ -531,6 +532,33 @@ async def update_project(
)
)

async def archive_project(self, project_id: str) -> Project:
"""
Archive a project.

For personal projects, archives it only for the user.
For workspace projects, archives it for all members.

:param project_id: The ID of the project to archive.
:return: The archived project object.
:raises requests.exceptions.HTTPError: If the API request fails.
:raises TypeError: If the API response is not a valid Project dictionary.
"""
return await run_async(lambda: self._api.archive_project(project_id))

async def unarchive_project(self, project_id: str) -> Project:
"""
Unarchive a project.

Restores a previously archived project.

:param project_id: The ID of the project to unarchive.
:return: The unarchived project object.
:raises requests.exceptions.HTTPError: If the API request fails.
:raises TypeError: If the API response is not a valid Project dictionary.
"""
return await run_async(lambda: self._api.unarchive_project(project_id))

async def delete_project(self, project_id: str) -> bool:
"""
Delete a project.
Expand Down
1 change: 1 addition & 0 deletions todoist_api_python/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class _(JSONPyWizard.Meta): # noqa:N801
is_collapsed: Annotated[bool, Alias(load=("collapsed", "is_collapsed"))]
is_shared: Annotated[bool, Alias(load=("shared", "is_shared"))]
is_favorite: bool
is_archived: bool
can_assign_tasks: bool
view_style: ViewStyle
created_at: ApiDate
Expand Down