Skip to content
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