diff --git a/tests/test_api_tasks.py b/tests/test_api_tasks.py index 29b586a..c813aea 100644 --- a/tests/test_api_tasks.py +++ b/tests/test_api_tasks.py @@ -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, diff --git a/todoist_api_python/api.py b/todoist_api_python/api.py index f946175..2e6e044 100644 --- a/todoist_api_python/api.py +++ b/todoist_api_python/api.py @@ -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. diff --git a/todoist_api_python/api_async.py b/todoist_api_python/api_async.py index ba78db5..3c45273 100644 --- a/todoist_api_python/api_async.py +++ b/todoist_api_python/api_async.py @@ -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.