Skip to content

Added move_task #198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 30, 2025
Merged
38 changes: 38 additions & 0 deletions tests/test_api_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,44 @@ async def test_uncomplete_task(
assert response is True


@pytest.mark.asyncio
async def test_move_task(
todoist_api: TodoistAPI,
todoist_api_async: TodoistAPIAsync,
requests_mock: responses.RequestsMock,
) -> None:
task_id = "6X7rM8997g3RQmvh"
endpoint = f"{DEFAULT_API_URL}/tasks/{task_id}/move"

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

response = todoist_api.move_task(task_id, project_id="123")

assert len(requests_mock.calls) == 1
assert response is True

response = await todoist_api_async.move_task(task_id, section_id="456")

assert len(requests_mock.calls) == 2
assert response is True

response = await todoist_api_async.move_task(task_id, parent_id="789")

assert len(requests_mock.calls) == 3
assert response is True

with pytest.raises(
ValueError,
match="Either `project_id`, `section_id`, or `parent_id` must be provided.",
):
response = await todoist_api_async.move_task(task_id)


@pytest.mark.asyncio
async def test_delete_task(
todoist_api: TodoistAPI,
Expand Down
39 changes: 39 additions & 0 deletions todoist_api_python/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,45 @@ def uncomplete_task(self, task_id: str) -> bool:
endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/reopen")
return post(self._session, endpoint, self._token)

def move_task(
self,
task_id: str,
project_id: str | None = None,
section_id: str | None = None,
parent_id: str | None = None,
) -> bool:
"""
Move a task to a different project, section, or parent task.

`project_id` takes predence, followed by
`section_id` (which also updates `project_id`),
and then `parent_id` (which also updates `section_id` and `project_id`).

:param task_id: The ID of the task to move.
:param project_id: The ID of the project to move the task to.
:param section_id: The ID of the section to move the task to.
:param parent_id: The ID of the parent to move the task to.
:return: True if the task was moved successfully,
False otherwise (possibly raise `HTTPError` instead).
:raises requests.exceptions.HTTPError: If the API request fails.
:raises ValueError: If neither `project_id`, `section_id`,
nor `parent_id` is provided.
"""
if project_id is None and section_id is None and parent_id is None:
raise ValueError(
"Either `project_id`, `section_id`, or `parent_id` must be provided."
)

data: dict[str, Any] = {}
if project_id is not None:
data["project_id"] = project_id
if section_id is not None:
data["section_id"] = section_id
if parent_id is not None:
data["parent_id"] = parent_id
endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/move")
return post(self._session, endpoint, self._token, data=data)

def delete_task(self, task_id: str) -> bool:
"""
Delete a task.
Expand Down
33 changes: 33 additions & 0 deletions todoist_api_python/api_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,39 @@ async def uncomplete_task(self, task_id: str) -> bool:
"""
return await run_async(lambda: self._api.uncomplete_task(task_id))

async def move_task(
self,
task_id: str,
project_id: str | None = None,
section_id: str | None = None,
parent_id: str | None = None,
) -> bool:
"""
Move a task to a different project, section, or parent task.

`project_id` takes predence, followed by
`section_id` (which also updates `project_id`),
and then `parent_id` (which also updates `section_id` and `project_id`).

:param task_id: The ID of the task to move.
:param project_id: The ID of the project to move the task to.
:param section_id: The ID of the section to move the task to.
:param parent_id: The ID of the parent to move the task to.
:return: True if the task was moved successfully,
False otherwise (possibly raise `HTTPError` instead).
:raises requests.exceptions.HTTPError: If the API request fails.
:raises ValueError: If neither `project_id`, `section_id`,
nor `parent_id` is provided.
"""
return await run_async(
lambda: self._api.move_task(
task_id,
project_id=project_id,
section_id=section_id,
parent_id=parent_id,
)
)

async def delete_task(self, task_id: str) -> bool:
"""
Delete a task.
Expand Down