diff --git a/CHANGELOG.md b/CHANGELOG.md index e04ede2..c552438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `TodoistAPIAsync` now performs true async HTTP I/O with `httpx.AsyncClient`. +- `TodoistAPIAsync` now performs true async HTTP I/O with `httpx2.AsyncClient`. - Support for Python 3.14. ### Removed @@ -18,11 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Breaking**: `TodoistAPI` now accepts an optional `client: httpx.Client` instead of `session: requests.Session`. -- **Breaking**: `TodoistAPIAsync` now accepts an optional `client: httpx.AsyncClient` instead of `session: requests.Session`. +- **Breaking**: Replaced the `httpx` runtime dependency with `httpx2`; caller-supplied clients and caught HTTP exceptions must now come from `httpx2`. +- **Breaking**: `TodoistAPI` now accepts an optional `client: httpx2.Client` instead of `session: requests.Session`. +- **Breaking**: `TodoistAPIAsync` now accepts an optional `client: httpx2.AsyncClient` instead of `session: requests.Session`. - **Breaking**: Async paginated return types now use `AsyncIterator[...]` instead of `AsyncGenerator[...]`. -- **Breaking**: API errors now raise `httpx.HTTPStatusError` instead of `requests.exceptions.HTTPError`. -- **Breaking**: Authentication helpers now accept optional `httpx.Client` / `httpx.AsyncClient` instances instead of `session: requests.Session`. +- **Breaking**: API errors now raise `httpx2.HTTPStatusError` instead of `requests.exceptions.HTTPError`. +- **Breaking**: Authentication helpers now accept optional `httpx2.Client` / `httpx2.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. - **Breaking**: `add_label` and `update_label` now accept `order` instead of `item_order` for label ordering. (#247) diff --git a/README.md b/README.md index bbb43d6..abc44ce 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,11 @@ For more detailed reference documentation, have a look at the [SDK documentation ## Migrating from 3.x -Version `4.x` introduces a breaking HTTP stack migration from `requests` to `httpx`. +Version `4.x` introduces a breaking HTTP stack migration from `requests` to `httpx2`. -- `TodoistAPI(..., session=...)` is now `TodoistAPI(..., client=...)` with `httpx.Client`. -- `TodoistAPIAsync(..., session=...)` is now `TodoistAPIAsync(..., client=...)` with `httpx.AsyncClient`. -- Error handling should catch `httpx.HTTPStatusError` instead of `requests.exceptions.HTTPError`. +- `TodoistAPI(..., session=...)` is now `TodoistAPI(..., client=...)` with `httpx2.Client`. +- `TodoistAPIAsync(..., session=...)` is now `TodoistAPIAsync(..., client=...)` with `httpx2.AsyncClient`. +- Error handling should catch `httpx2.HTTPStatusError` instead of `requests.exceptions.HTTPError`. ## Development diff --git a/docs/index.md b/docs/index.md index 382a485..fea1829 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,7 +37,7 @@ for comments in comments_iter: ### Async usage Use `TodoistAPIAsync` with `async with` (or call `await api.close()` manually) -to ensure the underlying `httpx.AsyncClient` is closed. +to ensure the underlying `httpx2.AsyncClient` is closed. ## Quick start diff --git a/pyproject.toml b/pyproject.toml index 907ae13..4cd3bd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ classifiers = [ ] dependencies = [ - "httpx>=0.28.1,<1", + "httpx2>=2.0.0,<3", "dataclass-wizard>=0.35.4,<1.0", "annotated-types", ] @@ -28,6 +28,7 @@ dev = [ "pre-commit>=4.0.0,<5", "pytest>=9.0.2,<10", "pytest-asyncio>=1.3,<1.4", + "pytest-httpx2>=1.0.0,<2", "tox>=4.15.1,<5", "tox-uv>=1.25.0,<2", "mypy~=2.1", diff --git a/tests/conftest.py b/tests/conftest.py index 882cea1..e15d724 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,13 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator + import respx + + +@pytest.fixture +def respx_mock(httpx2_mock: respx.MockRouter) -> respx.MockRouter: + return httpx2_mock + @pytest.fixture def todoist_api() -> Iterator[TodoistAPI]: diff --git a/tests/test_http_requests.py b/tests/test_http_requests.py index 33984ad..722c62f 100644 --- a/tests/test_http_requests.py +++ b/tests/test_http_requests.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -import httpx +import httpx2 import pytest from tests.data.test_defaults import DEFAULT_REQUEST_ID, DEFAULT_TOKEN @@ -34,7 +34,7 @@ def test_get_with_params(respx_mock: respx.MockRouter) -> None: response_status=200, ) - with httpx.Client() as client: + with httpx2.Client() as client: response = get( client=client, url=EXAMPLE_URL, @@ -56,7 +56,7 @@ def test_get_raise_for_status(respx_mock: respx.MockRouter) -> None: response_status=500, ) - with httpx.Client() as client, pytest.raises(httpx.HTTPStatusError) as error_info: + with httpx2.Client() as client, pytest.raises(httpx2.HTTPStatusError) as error_info: get(client, EXAMPLE_URL, DEFAULT_TOKEN) assert error_info.value.response.content == b'""' @@ -73,7 +73,7 @@ def test_post_with_data(respx_mock: respx.MockRouter) -> None: response_status=200, ) - with httpx.Client() as client: + with httpx2.Client() as client: response = post( client=client, url=EXAMPLE_URL, @@ -97,7 +97,7 @@ def test_post_with_empty_data(respx_mock: respx.MockRouter) -> None: response_status=200, ) - with httpx.Client() as client: + with httpx2.Client() as client: response = post( client=client, url=EXAMPLE_URL, @@ -118,7 +118,7 @@ def test_post_return_ok_when_no_response_body(respx_mock: respx.MockRouter) -> N response_status=204, ) - with httpx.Client() as client: + with httpx2.Client() as client: response = post(client=client, url=EXAMPLE_URL, token=DEFAULT_TOKEN) assert response.is_success is True @@ -132,7 +132,7 @@ def test_post_raise_for_status(respx_mock: respx.MockRouter) -> None: response_status=500, ) - with httpx.Client() as client, pytest.raises(httpx.HTTPStatusError): + with httpx2.Client() as client, pytest.raises(httpx2.HTTPStatusError): post(client=client, url=EXAMPLE_URL, token=DEFAULT_TOKEN) @@ -146,7 +146,7 @@ def test_delete_with_params(respx_mock: respx.MockRouter) -> None: response_status=204, ) - with httpx.Client() as client: + with httpx2.Client() as client: response = delete( client=client, url=EXAMPLE_URL, @@ -167,18 +167,18 @@ def test_delete_raise_for_status(respx_mock: respx.MockRouter) -> None: response_status=500, ) - with httpx.Client() as client, pytest.raises(httpx.HTTPStatusError): + with httpx2.Client() as client, pytest.raises(httpx2.HTTPStatusError): delete(client=client, url=EXAMPLE_URL, token=DEFAULT_TOKEN) def test_response_json_dict_returns_dict() -> None: - response = httpx.Response(status_code=200, json={"result": "ok"}) + response = httpx2.Response(status_code=200, json={"result": "ok"}) assert response_json_dict(response) == {"result": "ok"} def test_response_json_dict_raises_for_non_dict() -> None: - response = httpx.Response(status_code=200, json=["not", "a", "dict"]) + response = httpx2.Response(status_code=200, json=["not", "a", "dict"]) with pytest.raises(TypeError, match="JSON object"): response_json_dict(response) diff --git a/todoist_api_python/_core/http_requests.py b/todoist_api_python/_core/http_requests.py index 4608152..f6d21df 100644 --- a/todoist_api_python/_core/http_requests.py +++ b/todoist_api_python/_core/http_requests.py @@ -2,7 +2,7 @@ from typing import Any -import httpx +import httpx2 from todoist_api_python._core.http_headers import create_headers @@ -13,16 +13,16 @@ # # 60 seconds for reading aligns with Todoist's own internal timeout. All requests # are forcefully terminated after this time, so there is no point waiting longer. -TIMEOUT = httpx.Timeout(connect=10.0, read=60.0, write=60.0, pool=10.0) +TIMEOUT = httpx2.Timeout(connect=10.0, read=60.0, write=60.0, pool=10.0) def get( - client: httpx.Client, + client: httpx2.Client, url: str, token: str | None = None, request_id: str | None = None, params: dict[str, Any] | None = None, -) -> httpx.Response: +) -> httpx2.Response: headers = create_headers(token=token, request_id=request_id) response = client.get( @@ -36,12 +36,12 @@ def get( async def get_async( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, url: str, token: str | None = None, request_id: str | None = None, params: dict[str, Any] | None = None, -) -> httpx.Response: +) -> httpx2.Response: headers = create_headers(token=token, request_id=request_id) response = await client.get( @@ -55,14 +55,14 @@ async def get_async( def post( - client: httpx.Client, + client: httpx2.Client, url: str, token: str | None = None, request_id: str | None = None, *, params: dict[str, Any] | None = None, data: dict[str, Any] | None = None, -) -> httpx.Response: +) -> httpx2.Response: headers = create_headers(token=token, request_id=request_id) response = client.post( @@ -77,14 +77,14 @@ def post( async def post_async( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, url: str, token: str | None = None, request_id: str | None = None, *, params: dict[str, Any] | None = None, data: dict[str, Any] | None = None, -) -> httpx.Response: +) -> httpx2.Response: headers = create_headers(token=token, request_id=request_id) response = await client.post( @@ -99,12 +99,12 @@ async def post_async( def delete( - client: httpx.Client, + client: httpx2.Client, url: str, token: str | None = None, request_id: str | None = None, params: dict[str, Any] | None = None, -) -> httpx.Response: +) -> httpx2.Response: headers = create_headers(token=token, request_id=request_id) response = client.delete(url, params=params, headers=headers, timeout=TIMEOUT) @@ -113,12 +113,12 @@ def delete( async def delete_async( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, url: str, token: str | None = None, request_id: str | None = None, params: dict[str, Any] | None = None, -) -> httpx.Response: +) -> httpx2.Response: headers = create_headers(token=token, request_id=request_id) response = await client.delete(url, params=params, headers=headers, timeout=TIMEOUT) @@ -126,7 +126,7 @@ async def delete_async( return response -def response_json_dict(response: httpx.Response) -> dict[str, Any]: +def response_json_dict(response: httpx2.Response) -> dict[str, Any]: data = response.json() if not isinstance(data, dict): raise TypeError( diff --git a/todoist_api_python/api.py b/todoist_api_python/api.py index 0fe0cd0..f74ce59 100644 --- a/todoist_api_python/api.py +++ b/todoist_api_python/api.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar from weakref import finalize -import httpx +import httpx2 from annotated_types import Ge, Le, MaxLen, MinLen from todoist_api_python._core.endpoints import ( @@ -82,18 +82,18 @@ def __init__( self, token: str, request_id_fn: Callable[[], str] | None = default_request_id_fn, - client: httpx.Client | None = None, + client: httpx2.Client | None = None, ) -> None: """ Initialize the TodoistAPI client. :param token: Authentication token for the Todoist API. :param request_id_fn: Generator of request IDs for the `X-Request-ID` header. - :param client: An optional pre-configured `httpx.Client` object. + :param client: An optional pre-configured `httpx2.Client` object. """ self._token = token self._request_id_fn = request_id_fn - self._client = client or httpx.Client() + self._client = client or httpx2.Client() self._finalizer = finalize(self, self._client.close) def __enter__(self) -> Self: @@ -113,7 +113,7 @@ def __exit__( exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: - """Exit the runtime context and close the underlying httpx client.""" + """Exit the runtime context and close the underlying httpx2 client.""" self._finalizer() def get_task(self, task_id: str) -> Task: @@ -122,7 +122,7 @@ def get_task(self, task_id: str) -> Task: :param task_id: The ID of the task to retrieve. :return: The requested task. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Task dictionary. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") @@ -159,7 +159,7 @@ def get_tasks( :param ids: A list of the IDs of the tasks to retrieve. :param limit: Maximum number of tasks per page. :return: An iterable of lists of tasks. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_PATH) @@ -201,7 +201,7 @@ def filter_tasks( :param lang: Language for task content (e.g., 'en'). :param limit: Maximum number of tasks per page. :return: An iterable of lists of tasks. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_FILTER_PATH) @@ -264,7 +264,7 @@ def add_task( :param deadline_date: The deadline date as a date object. :param deadline_lang: Language for parsing the deadline date. :return: The newly created task. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Task dictionary. """ endpoint = get_api_url(TASKS_PATH) @@ -324,7 +324,7 @@ def add_task_quick( :param reminder: Optional reminder date in free form text. :param auto_reminder: Whether to add default reminder if date with time is set. :return: A result object containing the parsed task data and metadata. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response cannot be parsed into a QuickAddResult. """ endpoint = get_api_url(TASKS_QUICK_ADD_PATH) @@ -391,7 +391,7 @@ def update_task( :param deadline_date: The deadline date as a date object. :param deadline_lang: Language for parsing the deadline date. :return: the updated Task. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") @@ -438,7 +438,7 @@ def complete_task(self, task_id: str) -> bool: :param task_id: The ID of the task to close. :return: True if the task was closed successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/close") response = post( @@ -458,7 +458,7 @@ def uncomplete_task(self, task_id: str) -> bool: :param task_id: The ID of the task to reopen. :return: True if the task was uncompleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/reopen") response = post( @@ -489,7 +489,7 @@ def move_task( :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 httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises ValueError: If neither `project_id`, `section_id`, nor `parent_id` is provided. """ @@ -520,7 +520,7 @@ def delete_task(self, task_id: str) -> bool: :param task_id: The ID of the task to delete. :return: True if the task was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") response = delete( @@ -564,7 +564,7 @@ def get_completed_tasks_by_due_date( :param filter_lang: Language for the filter query (e.g., 'en'). :param limit: Maximum number of tasks per page (default 50). :return: An iterable of lists of completed tasks. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_COMPLETED_BY_DUE_DATE_PATH) @@ -618,7 +618,7 @@ def get_completed_tasks_by_completion_date( :param filter_lang: Language for the filter query (e.g., 'en'). :param limit: Maximum number of tasks per page (default 50). :return: An iterable of lists of completed tasks. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_COMPLETED_BY_COMPLETION_DATE_PATH) @@ -648,7 +648,7 @@ def get_project(self, project_id: str) -> Project: :param project_id: The ID of the project to retrieve. :return: The requested project. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") @@ -674,7 +674,7 @@ def get_projects( :param limit: Maximum number of projects per page. :return: An iterable of lists of projects. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(PROJECTS_PATH) @@ -705,7 +705,7 @@ def search_projects( :param query: Query string for project names. :param limit: Maximum number of projects per page. :return: An iterable of lists of projects. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{PROJECTS_SEARCH_PATH_SUFFIX}") @@ -742,7 +742,7 @@ def add_project( :param is_favorite: Whether the project is a favorite. :param view_style: A string value (either 'list' or 'board', default is 'list'). :return: The newly created project. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url(PROJECTS_PATH) @@ -792,7 +792,7 @@ def update_project( :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. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") @@ -825,7 +825,7 @@ def archive_project(self, project_id: str) -> Project: :param project_id: The ID of the project to archive. :return: The archived project object. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url( @@ -848,7 +848,7 @@ def unarchive_project(self, project_id: str) -> Project: :param project_id: The ID of the project to unarchive. :return: The unarchived project object. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url( @@ -872,7 +872,7 @@ def delete_project(self, project_id: str) -> bool: :param project_id: The ID of the project to delete. :return: True if the project was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") response = delete( @@ -898,7 +898,7 @@ def get_collaborators( :param project_id: The ID of the project. :param limit: Maximum number of collaborators per page. :return: An iterable of lists of collaborators. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}/{COLLABORATORS_PATH}") @@ -919,7 +919,7 @@ def get_section(self, section_id: str) -> Section: :param section_id: The ID of the section to retrieve. :return: The requested section. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Section dictionary. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") @@ -950,7 +950,7 @@ def get_sections( :param project_id: Filter sections by project ID. :param limit: Maximum number of sections per page. :return: An iterable of lists of sections. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(SECTIONS_PATH) @@ -985,7 +985,7 @@ def search_sections( :param project_id: If set, search sections within the given project only. :param limit: Maximum number of sections per page. :return: An iterable of lists of sections. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{SECTIONS_SEARCH_PATH_SUFFIX}") @@ -1016,7 +1016,7 @@ def add_section( :param project_id: The ID of the project to add the section to. :param order: The order of the section among all sections in the project. :return: The newly created section. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Section dictionary. """ endpoint = get_api_url(SECTIONS_PATH) @@ -1049,7 +1049,7 @@ def update_section( :param order: Position of the section among sections in the project. :param collapsed: Whether the section's tasks are collapsed. :return: the updated Section. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") @@ -1078,7 +1078,7 @@ def delete_section(self, section_id: str) -> bool: :param section_id: The ID of the section to delete. :return: True if the section was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") response = delete( @@ -1095,7 +1095,7 @@ def get_comment(self, comment_id: str) -> Comment: :param comment_id: The ID of the comment to retrieve. :return: The requested comment. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Comment dictionary. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") @@ -1129,7 +1129,7 @@ def get_comments( :param limit: Maximum number of comments per page. :return: An iterable of lists of comments. :raises ValueError: If neither `project_id` nor `task_id` is provided. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ if project_id is None and task_id is None: @@ -1175,7 +1175,7 @@ def add_comment( :param uids_to_notify: A list of user IDs to notify. :return: The newly created comment. :raises ValueError: If neither `project_id` nor `task_id` is provided. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Comment dictionary. """ if project_id is None and task_id is None: @@ -1212,7 +1212,7 @@ def update_comment( :param comment_id: The ID of the comment to update. :param content: The new text content for the comment. :return: the updated Comment. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") response = post( @@ -1232,7 +1232,7 @@ def delete_comment(self, comment_id: str) -> bool: :param comment_id: The ID of the comment to delete. :return: True if the comment was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") response = delete( @@ -1249,7 +1249,7 @@ def get_label(self, label_id: str) -> Label: :param label_id: The ID of the label to retrieve. :return: The requested label. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Label dictionary. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") @@ -1278,7 +1278,7 @@ def get_labels( :param limit: Maximum number of labels per page. :return: An iterable of lists of personal labels. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(LABELS_PATH) @@ -1311,7 +1311,7 @@ def search_labels( :param query: Query string for label names. :param limit: Maximum number of labels per page. :return: An iterable of lists of labels. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{LABELS_PATH}/{LABELS_SEARCH_PATH_SUFFIX}") @@ -1344,7 +1344,7 @@ def add_label( :param order: Label's order in the label list. :param is_favorite: Whether the label is a favorite. :return: The newly created label. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Label dictionary. """ endpoint = get_api_url(LABELS_PATH) @@ -1386,7 +1386,7 @@ def update_label( :param order: Label's order in the label list. :param is_favorite: Whether the label is a favorite. :return: the updated Label. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") @@ -1416,7 +1416,7 @@ def delete_label(self, label_id: str) -> bool: :param label_id: The ID of the label to delete. :return: True if the label was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") response = delete( @@ -1447,7 +1447,7 @@ def get_shared_labels( :param omit_personal: Optional boolean flag to omit personal label names. :param limit: Maximum number of labels per page. :return: An iterable of lists of shared label names (strings). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(SHARED_LABELS_PATH) @@ -1476,7 +1476,7 @@ def rename_shared_label( :param new_name: The new name for the shared label. :return: True if the rename was successful, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(SHARED_LABELS_RENAME_PATH) response = post( @@ -1497,7 +1497,7 @@ def remove_shared_label(self, name: Annotated[str, MaxLen(60)]) -> bool: :param name: The name of the shared label to remove. :return: True if the removal was successful, - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(SHARED_LABELS_REMOVE_PATH) data = {"name": name} @@ -1518,7 +1518,7 @@ def get_reminder(self, reminder_id: str) -> Reminder: :param reminder_id: The ID of the reminder to retrieve. :return: The requested reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{REMINDERS_PATH}/{reminder_id}") response = get( @@ -1546,7 +1546,7 @@ def get_reminders( :param task_id: Optional task ID to filter reminders by. :param limit: Maximum number of reminders per page. :return: An iterable of lists of reminders. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(REMINDERS_PATH) @@ -1594,7 +1594,7 @@ def add_reminder( :param due_timezone: Timezone for the due date. :param service: The notification service ("email" or "push"). :return: The newly created reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(REMINDERS_PATH) @@ -1651,7 +1651,7 @@ def update_reminder( :param due_timezone: Timezone for the due date. :param service: The notification service ("email" or "push"). :return: The updated reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{REMINDERS_PATH}/{reminder_id}") @@ -1686,7 +1686,7 @@ def delete_reminder(self, reminder_id: str) -> bool: :param reminder_id: The ID of the reminder to delete. :return: True if the reminder was deleted successfully. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{REMINDERS_PATH}/{reminder_id}") response = delete( @@ -1705,7 +1705,7 @@ def get_location_reminder(self, location_reminder_id: str) -> LocationReminder: :param location_reminder_id: The ID of the location reminder to retrieve. :return: The requested location reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LOCATION_REMINDERS_PATH}/{location_reminder_id}") response = get( @@ -1733,7 +1733,7 @@ def get_location_reminders( :param task_id: Optional task ID to filter location reminders by. :param limit: Maximum number of location reminders per page. :return: An iterable of lists of location reminders. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(LOCATION_REMINDERS_PATH) @@ -1772,7 +1772,7 @@ def add_location_reminder( :param loc_trigger: When to trigger ("on_enter" or "on_leave"). :param radius: The radius in meters (default: 100). :return: The newly created location reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(LOCATION_REMINDERS_PATH) @@ -1817,7 +1817,7 @@ def update_location_reminder( :param loc_trigger: When to trigger ("on_enter" or "on_leave"). :param radius: The radius in meters. :return: The updated location reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LOCATION_REMINDERS_PATH}/{location_reminder_id}") @@ -1845,7 +1845,7 @@ def delete_location_reminder(self, location_reminder_id: str) -> bool: :param location_reminder_id: The ID of the location reminder to delete. :return: True if the location reminder was deleted successfully. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LOCATION_REMINDERS_PATH}/{location_reminder_id}") response = delete( @@ -1869,7 +1869,7 @@ class ResultsPaginator(Iterator[list[T]]): requesting new pages as needed when iterating. """ - _client: httpx.Client + _client: httpx2.Client _url: str _results_field: str _results_inst: Callable[[Any], T] @@ -1878,7 +1878,7 @@ class ResultsPaginator(Iterator[list[T]]): def __init__( self, - client: httpx.Client, + client: httpx2.Client, url: str, results_field: str, results_inst: Callable[[Any], T], @@ -1889,7 +1889,7 @@ def __init__( """ Initialize the ResultsPaginator. - :param client: The httpx client to use for API calls. + :param client: The httpx2 client to use for API calls. :param url: The API endpoint URL to fetch results from. :param results_field: The key in the API response that contains the results. :param results_inst: A callable that converts result items to objects of type T. @@ -1910,7 +1910,7 @@ def __next__(self) -> list[T]: Fetch and return the next page of results from the Todoist API. :return: A list of results. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ if self._cursor is None: diff --git a/todoist_api_python/api_async.py b/todoist_api_python/api_async.py index c7d5e3c..fd086eb 100644 --- a/todoist_api_python/api_async.py +++ b/todoist_api_python/api_async.py @@ -5,7 +5,7 @@ from collections.abc import AsyncIterator, Callable from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar -import httpx +import httpx2 from annotated_types import Ge, Le, MaxLen, MinLen from todoist_api_python._core.endpoints import ( @@ -77,7 +77,7 @@ class TodoistAPIAsync: Manages an HTTP client and handles authentication. Prefer using this class as an async context manager to ensure the underlying - `httpx.AsyncClient` is always closed. If you do not use `async with`, call + `httpx2.AsyncClient` is always closed. If you do not use `async with`, call `await close()` explicitly. """ @@ -85,19 +85,19 @@ def __init__( self, token: str, request_id_fn: Callable[[], str] | None = default_request_id_fn, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> None: """ Initialize the TodoistAPIAsync client. :param token: Authentication token for the Todoist API. :param request_id_fn: Generator of request IDs for the `X-Request-ID` header. - :param client: An optional pre-configured `httpx.AsyncClient` object, to be + :param client: An optional pre-configured `httpx2.AsyncClient` object, to be fully managed by `TodoistAPIAsync`. """ self._token = token self._request_id_fn = request_id_fn - self._client = client or httpx.AsyncClient() + self._client = client or httpx2.AsyncClient() async def __aenter__(self) -> Self: """ @@ -116,11 +116,11 @@ async def __aexit__( exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: - """Exit the async runtime context and close the underlying httpx client.""" + """Exit the async runtime context and close the underlying httpx2 client.""" await self.close() async def close(self) -> None: - """Close the underlying `httpx.AsyncClient`.""" + """Close the underlying `httpx2.AsyncClient`.""" await self._client.aclose() def __del__(self) -> None: @@ -142,7 +142,7 @@ async def get_task(self, task_id: str) -> Task: :param task_id: The ID of the task to retrieve. :return: The requested task. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Task dictionary. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") @@ -179,7 +179,7 @@ async def get_tasks( :param ids: A list of the IDs of the tasks to retrieve. :param limit: Maximum number of tasks per page. :return: An iterable of lists of tasks. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_PATH) @@ -221,7 +221,7 @@ async def filter_tasks( :param lang: Language for task content (e.g., 'en'). :param limit: Maximum number of tasks per page. :return: An iterable of lists of tasks. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_FILTER_PATH) @@ -284,7 +284,7 @@ async def add_task( :param deadline_date: The deadline date as a date object. :param deadline_lang: Language for parsing the deadline date. :return: The newly created task. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Task dictionary. """ endpoint = get_api_url(TASKS_PATH) @@ -344,7 +344,7 @@ async def add_task_quick( :param reminder: Optional reminder date in free form text. :param auto_reminder: Whether to add default reminder if date with time is set. :return: A result object containing the parsed task data and metadata. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response cannot be parsed into a QuickAddResult. """ endpoint = get_api_url(TASKS_QUICK_ADD_PATH) @@ -411,7 +411,7 @@ async def update_task( :param deadline_date: The deadline date as a date object. :param deadline_lang: Language for parsing the deadline date. :return: the updated Task. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") @@ -458,7 +458,7 @@ async def complete_task(self, task_id: str) -> bool: :param task_id: The ID of the task to close. :return: True if the task was closed successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/close") response = await post_async( @@ -478,7 +478,7 @@ async def uncomplete_task(self, task_id: str) -> bool: :param task_id: The ID of the task to reopen. :return: True if the task was uncompleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/reopen") response = await post_async( @@ -509,7 +509,7 @@ async def move_task( :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 httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises ValueError: If neither `project_id`, `section_id`, nor `parent_id` is provided. """ @@ -540,7 +540,7 @@ async def delete_task(self, task_id: str) -> bool: :param task_id: The ID of the task to delete. :return: True if the task was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") response = await delete_async( @@ -584,7 +584,7 @@ async def get_completed_tasks_by_due_date( :param filter_lang: Language for the filter query (e.g., 'en'). :param limit: Maximum number of tasks per page (default 50). :return: An iterable of lists of completed tasks. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_COMPLETED_BY_DUE_DATE_PATH) @@ -638,7 +638,7 @@ async def get_completed_tasks_by_completion_date( :param filter_lang: Language for the filter query (e.g., 'en'). :param limit: Maximum number of tasks per page (default 50). :return: An iterable of lists of completed tasks. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_COMPLETED_BY_COMPLETION_DATE_PATH) @@ -668,7 +668,7 @@ async def get_project(self, project_id: str) -> Project: :param project_id: The ID of the project to retrieve. :return: The requested project. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") @@ -694,7 +694,7 @@ async def get_projects( :param limit: Maximum number of projects per page. :return: An iterable of lists of projects. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(PROJECTS_PATH) @@ -725,7 +725,7 @@ async def search_projects( :param query: Query string for project names. :param limit: Maximum number of projects per page. :return: An iterable of lists of projects. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{PROJECTS_SEARCH_PATH_SUFFIX}") @@ -762,7 +762,7 @@ async def add_project( :param is_favorite: Whether the project is a favorite. :param view_style: A string value (either 'list' or 'board', default is 'list'). :return: The newly created project. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url(PROJECTS_PATH) @@ -812,7 +812,7 @@ async def update_project( :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. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") @@ -845,7 +845,7 @@ async def archive_project(self, project_id: str) -> Project: :param project_id: The ID of the project to archive. :return: The archived project object. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url( @@ -868,7 +868,7 @@ async def unarchive_project(self, project_id: str) -> Project: :param project_id: The ID of the project to unarchive. :return: The unarchived project object. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url( @@ -892,7 +892,7 @@ async def delete_project(self, project_id: str) -> bool: :param project_id: The ID of the project to delete. :return: True if the project was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") response = await delete_async( @@ -918,7 +918,7 @@ async def get_collaborators( :param project_id: The ID of the project. :param limit: Maximum number of collaborators per page. :return: An iterable of lists of collaborators. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}/{COLLABORATORS_PATH}") @@ -939,7 +939,7 @@ async def get_section(self, section_id: str) -> Section: :param section_id: The ID of the section to retrieve. :return: The requested section. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Section dictionary. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") @@ -970,7 +970,7 @@ async def get_sections( :param project_id: Filter sections by project ID. :param limit: Maximum number of sections per page. :return: An iterable of lists of sections. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(SECTIONS_PATH) @@ -1005,7 +1005,7 @@ async def search_sections( :param project_id: If set, search sections within the given project only. :param limit: Maximum number of sections per page. :return: An iterable of lists of sections. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{SECTIONS_SEARCH_PATH_SUFFIX}") @@ -1036,7 +1036,7 @@ async def add_section( :param project_id: The ID of the project to add the section to. :param order: The order of the section among all sections in the project. :return: The newly created section. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Section dictionary. """ endpoint = get_api_url(SECTIONS_PATH) @@ -1069,7 +1069,7 @@ async def update_section( :param order: Position of the section among sections in the project. :param collapsed: Whether the section's tasks are collapsed. :return: the updated Section. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") @@ -1098,7 +1098,7 @@ async def delete_section(self, section_id: str) -> bool: :param section_id: The ID of the section to delete. :return: True if the section was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") response = await delete_async( @@ -1115,7 +1115,7 @@ async def get_comment(self, comment_id: str) -> Comment: :param comment_id: The ID of the comment to retrieve. :return: The requested comment. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Comment dictionary. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") @@ -1149,7 +1149,7 @@ async def get_comments( :param limit: Maximum number of comments per page. :return: An iterable of lists of comments. :raises ValueError: If neither `project_id` nor `task_id` is provided. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ if project_id is None and task_id is None: @@ -1195,7 +1195,7 @@ async def add_comment( :param uids_to_notify: A list of user IDs to notify. :return: The newly created comment. :raises ValueError: If neither `project_id` nor `task_id` is provided. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Comment dictionary. """ if project_id is None and task_id is None: @@ -1232,7 +1232,7 @@ async def update_comment( :param comment_id: The ID of the comment to update. :param content: The new text content for the comment. :return: the updated Comment. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") response = await post_async( @@ -1252,7 +1252,7 @@ async def delete_comment(self, comment_id: str) -> bool: :param comment_id: The ID of the comment to delete. :return: True if the comment was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") response = await delete_async( @@ -1269,7 +1269,7 @@ async def get_label(self, label_id: str) -> Label: :param label_id: The ID of the label to retrieve. :return: The requested label. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Label dictionary. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") @@ -1298,7 +1298,7 @@ async def get_labels( :param limit: Maximum number of labels per page. :return: An iterable of lists of personal labels. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(LABELS_PATH) @@ -1331,7 +1331,7 @@ async def search_labels( :param query: Query string for label names. :param limit: Maximum number of labels per page. :return: An iterable of lists of labels. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{LABELS_PATH}/{LABELS_SEARCH_PATH_SUFFIX}") @@ -1364,7 +1364,7 @@ async def add_label( :param order: Label's order in the label list. :param is_favorite: Whether the label is a favorite. :return: The newly created label. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response is not a valid Label dictionary. """ endpoint = get_api_url(LABELS_PATH) @@ -1406,7 +1406,7 @@ async def update_label( :param order: Label's order in the label list. :param is_favorite: Whether the label is a favorite. :return: the updated Label. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") @@ -1436,7 +1436,7 @@ async def delete_label(self, label_id: str) -> bool: :param label_id: The ID of the label to delete. :return: True if the label was deleted successfully, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") response = await delete_async( @@ -1467,7 +1467,7 @@ async def get_shared_labels( :param omit_personal: Optional boolean flag to omit personal label names. :param limit: Maximum number of labels per page. :return: An iterable of lists of shared label names (strings). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(SHARED_LABELS_PATH) @@ -1496,7 +1496,7 @@ async def rename_shared_label( :param new_name: The new name for the shared label. :return: True if the rename was successful, False otherwise (possibly raise `HTTPError` instead). - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(SHARED_LABELS_RENAME_PATH) response = await post_async( @@ -1517,7 +1517,7 @@ async def remove_shared_label(self, name: Annotated[str, MaxLen(60)]) -> bool: :param name: The name of the shared label to remove. :return: True if the removal was successful, - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(SHARED_LABELS_REMOVE_PATH) data = {"name": name} @@ -1538,7 +1538,7 @@ async def get_reminder(self, reminder_id: str) -> Reminder: :param reminder_id: The ID of the reminder to retrieve. :return: The requested reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{REMINDERS_PATH}/{reminder_id}") response = await get_async( @@ -1566,7 +1566,7 @@ async def get_reminders( :param task_id: Optional task ID to filter reminders by. :param limit: Maximum number of reminders per page. :return: An async iterable of lists of reminders. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(REMINDERS_PATH) @@ -1614,7 +1614,7 @@ async def add_reminder( :param due_timezone: Timezone for the due date. :param service: The notification service ("email" or "push"). :return: The newly created reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(REMINDERS_PATH) @@ -1671,7 +1671,7 @@ async def update_reminder( :param due_timezone: Timezone for the due date. :param service: The notification service ("email" or "push"). :return: The updated reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{REMINDERS_PATH}/{reminder_id}") @@ -1706,7 +1706,7 @@ async def delete_reminder(self, reminder_id: str) -> bool: :param reminder_id: The ID of the reminder to delete. :return: True if the reminder was deleted successfully. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{REMINDERS_PATH}/{reminder_id}") response = await delete_async( @@ -1727,7 +1727,7 @@ async def get_location_reminder( :param location_reminder_id: The ID of the location reminder to retrieve. :return: The requested location reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LOCATION_REMINDERS_PATH}/{location_reminder_id}") response = await get_async( @@ -1755,7 +1755,7 @@ async def get_location_reminders( :param task_id: Optional task ID to filter location reminders by. :param limit: Maximum number of location reminders per page. :return: An async iterable of lists of location reminders. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(LOCATION_REMINDERS_PATH) @@ -1794,7 +1794,7 @@ async def add_location_reminder( :param loc_trigger: When to trigger ("on_enter" or "on_leave"). :param radius: The radius in meters (default: 100). :return: The newly created location reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(LOCATION_REMINDERS_PATH) @@ -1839,7 +1839,7 @@ async def update_location_reminder( :param loc_trigger: When to trigger ("on_enter" or "on_leave"). :param radius: The radius in meters. :return: The updated location reminder. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LOCATION_REMINDERS_PATH}/{location_reminder_id}") @@ -1867,7 +1867,7 @@ async def delete_location_reminder(self, location_reminder_id: str) -> bool: :param location_reminder_id: The ID of the location reminder to delete. :return: True if the location reminder was deleted successfully. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. """ endpoint = get_api_url(f"{LOCATION_REMINDERS_PATH}/{location_reminder_id}") response = await delete_async( @@ -1891,7 +1891,7 @@ class AsyncResultsPaginator(AsyncIterator[list[T]]): requesting new pages as needed when iterating. """ - _client: httpx.AsyncClient + _client: httpx2.AsyncClient _url: str _results_field: str _results_inst: Callable[[Any], T] @@ -1900,7 +1900,7 @@ class AsyncResultsPaginator(AsyncIterator[list[T]]): def __init__( self, - client: httpx.AsyncClient, + client: httpx2.AsyncClient, url: str, results_field: str, results_inst: Callable[[Any], T], @@ -1911,7 +1911,7 @@ def __init__( """ Initialize the ResultsPaginator. - :param client: The httpx client to use for API calls. + :param client: The httpx2 client to use for API calls. :param url: The API endpoint URL to fetch results from. :param results_field: The key in the API response that contains the results. :param results_inst: A callable that converts result items to objects of type T. @@ -1932,7 +1932,7 @@ async def __anext__(self) -> list[T]: Fetch and return the next page of results from the Todoist API. :return: A list of results. - :raises httpx.HTTPStatusError: If the API request fails. + :raises httpx2.HTTPStatusError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ if self._cursor is None: diff --git a/todoist_api_python/authentication.py b/todoist_api_python/authentication.py index 951dabe..aea84b3 100644 --- a/todoist_api_python/authentication.py +++ b/todoist_api_python/authentication.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Literal from urllib.parse import urlencode -import httpx +import httpx2 if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator @@ -63,7 +63,7 @@ def get_auth_token( client_id: str, client_secret: str, code: str, - client: httpx.Client | None = None, + client: httpx2.Client | None = None, ) -> AuthResult: """Get access token using provided client ID, client secret, and auth code.""" endpoint = _get_access_token_url() @@ -84,7 +84,7 @@ async def get_auth_token_async( client_id: str, client_secret: str, code: str, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> AuthResult: """Get access token asynchronously.""" endpoint = _get_access_token_url() @@ -105,7 +105,7 @@ def revoke_auth_token( client_id: str, client_secret: str, token: str, - client: httpx.Client | None = None, + client: httpx2.Client | None = None, ) -> bool: """Revoke an access token.""" endpoint = _get_access_tokens_url() @@ -121,7 +121,7 @@ async def revoke_auth_token_async( client_id: str, client_secret: str, token: str, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> bool: """Revoke an access token asynchronously.""" endpoint = _get_access_tokens_url() @@ -136,24 +136,24 @@ async def revoke_auth_token_async( @contextmanager -def _managed_client(client: httpx.Client | None) -> Iterator[httpx.Client]: +def _managed_client(client: httpx2.Client | None) -> Iterator[httpx2.Client]: if client is not None: yield client return - with httpx.Client() as default_client: + with httpx2.Client() as default_client: yield default_client @asynccontextmanager async def _managed_async_client( - client: httpx.AsyncClient | None, -) -> AsyncIterator[httpx.AsyncClient]: + client: httpx2.AsyncClient | None, +) -> AsyncIterator[httpx2.AsyncClient]: if client is not None: yield client return - async with httpx.AsyncClient() as default_client: + async with httpx2.AsyncClient() as default_client: yield default_client diff --git a/uv.lock b/uv.lock index f95ba94..75a6e08 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,8 @@ revision = 3 requires-python = ">=3.10, <4" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'emscripten'", + "(python_full_version < '3.15' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] [[package]] @@ -301,6 +302,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11", marker = "python_full_version < '3.12' or python_full_version >= '3.15' or sys_platform != 'emscripten'" }, + { name = "truststore", marker = "python_full_version < '3.12' or python_full_version >= '3.15' or sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -316,6 +330,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "identify" version = "2.6.9" @@ -327,11 +367,11 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -841,6 +881,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-httpx2" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "respx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/d1/70e7edb50679ddd34ba5e13ac1d6e1223bc4e24d0a5fc30587f8fbe884c4/pytest_httpx2-1.0.0.tar.gz", hash = "sha256:d897a14c1341d3f3014e9432c16fed366598aba06a2ba9c08460a51799cb7128", size = 2882, upload-time = "2026-05-20T08:26:41.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/e1/5e38e8de42fba9667846a43113469c7481d01db4b6511e42645385bace8f/pytest_httpx2-1.0.0-py3-none-any.whl", hash = "sha256:467dc7f6946854ffc20e1678703266b093037d8f3fb9f1fb523a2c432c64cff0", size = 4504, upload-time = "2026-05-20T08:26:40.219Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -977,7 +1029,7 @@ source = { editable = "." } dependencies = [ { name = "annotated-types" }, { name = "dataclass-wizard" }, - { name = "httpx" }, + { name = "httpx2" }, ] [package.dev-dependencies] @@ -986,6 +1038,7 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-httpx2" }, { name = "respx" }, { name = "ruff" }, { name = "tox" }, @@ -1001,7 +1054,7 @@ docs = [ requires-dist = [ { name = "annotated-types" }, { name = "dataclass-wizard", specifier = ">=0.35.4,<1.0" }, - { name = "httpx", specifier = ">=0.28.1,<1" }, + { name = "httpx2", specifier = ">=2.0.0,<3" }, ] [package.metadata.requires-dev] @@ -1010,6 +1063,7 @@ dev = [ { name = "pre-commit", specifier = ">=4.0.0,<5" }, { name = "pytest", specifier = ">=9.0.2,<10" }, { name = "pytest-asyncio", specifier = ">=1.3,<1.4" }, + { name = "pytest-httpx2", specifier = ">=1.0.0,<2" }, { name = "respx", specifier = ">=0.23.1,<0.24" }, { name = "ruff", specifier = ">=0.16.4,<0.17" }, { name = "tox", specifier = ">=4.15.1,<5" }, @@ -1096,6 +1150,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a7/f5c29e0e6faaccefcab607f672b176927144e9412c8183d21301ea2a6f6c/tox_uv-1.25.0-py3-none-any.whl", hash = "sha256:50cfe7795dcd49b2160d7d65b5ece8717f38cfedc242c852a40ec0a71e159bf7", size = 16431, upload-time = "2025-02-21T16:37:49.657Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"