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
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
amix marked this conversation as resolved.
Resolved

### Removed

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
12 changes: 7 additions & 5 deletions tests/test_api_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,41 +160,43 @@ 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] = {
"description": "A project description",
"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,
method="POST",
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
Expand Down
12 changes: 12 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import pytest

from tests.data.test_defaults import (
DEFAULT_ATTACHMENT_RESPONSE,
DEFAULT_COLLABORATOR_RESPONSE,
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions todoist_api_python/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions todoist_api_python/api_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions todoist_api_python/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion todoist_api_python/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading