diff --git a/CHANGELOG.md b/CHANGELOG.md index 1489a0e..75829ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [4.0.1] - 2026-11-03 + +### Added + +- `add_project` and `update_project` accept the `table` view style. + +### Fixed + +- Projects with a view style this version doesn't know, such as `table`, load instead of failing with `ParseError`. `Project.view_style` is now a `str`. + +## [4.0.0] - 2026-03-25 ### Added - `TodoistAPIAsync` now performs true async HTTP I/O with `httpx.AsyncClient`. - Support for Python 3.14. +- `add_project` and `update_project` accept the `table` view style. ### Removed @@ -25,6 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Breaking**: Authentication helpers now accept optional `httpx.Client` / `httpx.AsyncClient` instances instead of `session: requests.Session`. - **Breaking**: `update_section` now accepts only keyword arguments after `section_id`; any one of `name`, `order`, or `collapsed` can be updated in the same call. +### Fixed + +- Loading a project no longer fails when its `view_style` is one the SDK doesn't know yet; `Project.view_style` is now typed as `str` and keeps the value as sent. + ## [3.2.1] - 2026-01-22 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index b651de3..c57b9fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "todoist_api_python" -version = "4.0.0" +version = "4.0.1" description = "Official Python SDK for the Todoist API." authors = [{ name = "Doist Developers", email = "dev@doist.com" }] requires-python = ">=3.10,<4" diff --git a/tests/test_api_projects.py b/tests/test_api_projects.py index 212d430..13480cd 100644 --- a/tests/test_api_projects.py +++ b/tests/test_api_projects.py @@ -160,12 +160,13 @@ async def test_add_project_minimal( @pytest.mark.asyncio +@pytest.mark.parametrize("view_style", ["board", "table"]) async def test_add_project_full( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, respx_mock: respx.MockRouter, default_project_response: dict[str, Any], - default_project: Project, + view_style: str, ) -> None: project_name = "A Project" args: dict[str, Any] = { @@ -173,8 +174,9 @@ async def test_add_project_full( "parent_id": "3Ty8VQXxpwv28PK3", "color": "red", "is_favorite": True, - "view_style": "board", + "view_style": view_style, } + project_response = default_project_response | {"view_style": view_style} mock_route( respx_mock, @@ -182,19 +184,19 @@ async def test_add_project_full( url=f"{DEFAULT_API_URL}/projects", request_headers=api_headers(), request_json={"name": project_name} | args, - response_json=default_project_response, + response_json=project_response, response_status=200, ) new_project = todoist_api.add_project(name=project_name, **args) assert len(respx_mock.calls) == 1 - assert new_project == default_project + assert new_project == Project.from_dict(project_response) new_project = await todoist_api_async.add_project(name=project_name, **args) assert len(respx_mock.calls) == 2 - assert new_project == default_project + assert new_project == Project.from_dict(project_response) @pytest.mark.asyncio diff --git a/tests/test_models.py b/tests/test_models.py index 3ec24ba..ea6baf4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from tests.data.test_defaults import ( DEFAULT_ATTACHMENT_RESPONSE, DEFAULT_COLLABORATOR_RESPONSE, @@ -76,6 +78,16 @@ def test_project_from_dict() -> None: assert project.updated_at == parse_datetime(str(sample_data["updated_at"])) +@pytest.mark.parametrize("view_style", ["table", "some_future_view_style"]) +def test_project_from_dict_with_unknown_view_style(view_style: str) -> None: + sample_data = dict(DEFAULT_PROJECT_RESPONSE) + sample_data["view_style"] = view_style + + project = Project.from_dict(sample_data) + + assert project.view_style == view_style + + def test_project_url() -> None: inbox = Project.from_dict(dict(DEFAULT_PROJECT_RESPONSE)) assert inbox.url == "https://app.todoist.com/app/inbox" diff --git a/todoist_api_python/api.py b/todoist_api_python/api.py index 20b59cc..5d00ece 100644 --- a/todoist_api_python/api.py +++ b/todoist_api_python/api.py @@ -740,7 +740,7 @@ def add_project( :param parent_id: The ID of the parent project. Set to null for root projects. :param color: The color of the project icon. :param is_favorite: Whether the project is a favorite. - :param view_style: A string value (either 'list' or 'board', default is 'list'). + :param view_style: 'list', 'board', 'calendar' or 'table' (default is 'list'). :return: The newly created project. :raises httpx.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. @@ -788,7 +788,7 @@ def update_project( :param description: Description for the project (up to 1024 characters). :param color: The color of the project icon. :param is_favorite: Whether the project is a favorite. - :param view_style: A string value (either 'list' or 'board'). + :param view_style: 'list', 'board', 'calendar' or 'table'. :param order: Position of the project among projects with the same parent. :param collapsed: Whether the project's sub-projects are collapsed. :return: the updated Project. diff --git a/todoist_api_python/api_async.py b/todoist_api_python/api_async.py index 18150bb..30a2f39 100644 --- a/todoist_api_python/api_async.py +++ b/todoist_api_python/api_async.py @@ -760,7 +760,7 @@ async def add_project( :param parent_id: The ID of the parent project. Set to null for root projects. :param color: The color of the project icon. :param is_favorite: Whether the project is a favorite. - :param view_style: A string value (either 'list' or 'board', default is 'list'). + :param view_style: 'list', 'board', 'calendar' or 'table' (default is 'list'). :return: The newly created project. :raises httpx.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. @@ -808,7 +808,7 @@ async def update_project( :param description: Description for the project (up to 1024 characters). :param color: The color of the project icon. :param is_favorite: Whether the project is a favorite. - :param view_style: A string value (either 'list' or 'board'). + :param view_style: 'list', 'board', 'calendar' or 'table'. :param order: Position of the project among projects with the same parent. :param collapsed: Whether the project's sub-projects are collapsed. :return: the updated Project. diff --git a/todoist_api_python/models.py b/todoist_api_python/models.py index 5185a3c..cf5f4ce 100644 --- a/todoist_api_python/models.py +++ b/todoist_api_python/models.py @@ -9,7 +9,7 @@ from todoist_api_python._core.endpoints import INBOX_URL, get_project_url, get_task_url -ViewStyle = Literal["list", "board", "calendar"] +ViewStyle = Literal["list", "board", "calendar", "table"] DurationUnit = Literal["minute", "day"] ReminderType = Literal["relative", "absolute"] ReminderService = Literal["email", "push"] @@ -35,7 +35,8 @@ class _(JSONPyWizard.Meta): # noqa:N801 is_favorite: bool is_archived: bool can_assign_tasks: bool - view_style: ViewStyle + # Not `ViewStyle`: the API can add view styles, which a Literal would fail to load. + view_style: str created_at: ApiDate updated_at: ApiDate diff --git a/todoist_api_python/types.py b/todoist_api_python/types.py index 02c0d7c..a01db09 100644 --- a/todoist_api_python/types.py +++ b/todoist_api_python/types.py @@ -35,7 +35,7 @@ ] ViewStyle = Annotated[ str, - Predicate(lambda value: value in ("list", "board", "calendar")), + Predicate(lambda value: value in ("list", "board", "calendar", "table")), ] __all__ = ["ColorString", "LanguageCode", "ViewStyle"] diff --git a/uv.lock b/uv.lock index 1fdceb7..2f8b07a 100644 --- a/uv.lock +++ b/uv.lock @@ -824,7 +824,7 @@ wheels = [ [[package]] name = "todoist-api-python" -version = "3.2.1" +version = "4.0.1" source = { editable = "." } dependencies = [ { name = "annotated-types" },