Skip to content

Migrate SDK HTTP stack from requests to httpx#221

Open
goncalossilva wants to merge 5 commits intomainfrom
goncalossilva/httpx
Open

Migrate SDK HTTP stack from requests to httpx#221
goncalossilva wants to merge 5 commits intomainfrom
goncalossilva/httpx

Conversation

@goncalossilva
Copy link
Member

@goncalossilva goncalossilva commented Feb 13, 2026

Summary

This PR migrates the SDK HTTP stack from requests to httpx across async AND sync clients.

What changed

  • Replaced runtime HTTP client usage with httpx:
    • TodoistAPI(..., client: httpx.Client | None = None)
    • TodoistAPIAsync(..., client: httpx.AsyncClient | None = None)
  • Removed executor-based async request wrappers from API I/O paths (true async HTTP calls now: closes Running Synchronous Code as Async Using Lambdas (Code Smell) #162).
  • Migrated auth helpers to httpx (get_auth_token, revoke_auth_token, async variants).
  • Updated low-level HTTP helpers (_core/http_requests.py) to httpx.
  • Migrated tests from responses/requests-style mocks to respx.
  • Added async-client lifecycle coverage and explicit async close guidance.
  • Added shared constrained type aliases for API annotations.
  • Updated docs/changelog for the migration.

Breaking changes

  • session constructor arg is replaced by client (no compatibility alias):
    • TodoistAPI(session=...) -> TodoistAPI(client=...)
    • TodoistAPIAsync(session=...) -> TodoistAPIAsync(client=...)
  • Authentication helpers now accept optional httpx.Client / httpx.AsyncClient instances instead of session: requests.Session.
  • Exceptions to catch are now httpx.HTTPStatusError.

Next steps

Major version bump and release 4.0.0.

@goncalossilva goncalossilva force-pushed the goncalossilva/httpx branch 6 times, most recently from 6167b48 to f9dc425 Compare February 13, 2026 18:18
Move the async client to native httpx.AsyncClient I/O and remove\nexecutor-based async wrappers from API flows.\n\nAlso add respx-backed test infrastructure and migrate API tests\nthat exercise async behavior through the new transport stack.
Switch TodoistAPI and authentication sync paths from requests\nto httpx.Client and align sync error semantics with\nhttpx.HTTPStatusError.\n\nUpdate sync-focused tests to use the respx-backed request\nmocking setup.
Update docs and changelog to describe the requests-to-httpx\ntransition, constructor client arguments, and new HTTP error\nhandling guidance.
Use declarative respx lookups (params__eq, headers__contains, json__eq) via mock_route request_* arguments and response_* payload arguments.

Set API fixtures to deterministic request IDs so request-header assertions remain fully declarative and consistent across sync and async tests.
@goncalossilva goncalossilva force-pushed the goncalossilva/httpx branch 2 times, most recently from 930cfc7 to 26fde1d Compare February 13, 2026 18:36
Add a pure kwargs_without_none helper and reuse it across TodoistAPI and TodoistAPIAsync to reduce duplicated None-filtering logic in request params/data preparation.
@goncalossilva goncalossilva requested a review from lefcha February 13, 2026 18:59
@goncalossilva goncalossilva marked this pull request as ready for review February 13, 2026 19:00
@goncalossilva goncalossilva requested a review from a team as a code owner February 13, 2026 19:00
Copilot AI review requested due to automatic review settings February 13, 2026 19:00
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR successfully migrates the Todoist Python SDK's HTTP stack from requests to httpx, enabling true async HTTP I/O and removing the problematic executor-based async wrappers that blocked the event loop. The migration is comprehensive, covering both sync and async clients, authentication helpers, tests, and documentation.

Changes:

  • Replaced requests with httpx for HTTP operations (sync: httpx.Client, async: httpx.AsyncClient)
  • Removed executor-based async wrappers (run_async, generate_async) in favor of native async HTTP calls
  • Migrated test mocking from responses to respx

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
uv.lock Added httpx dependencies (httpx, httpcore, h11, anyio); removed requests and types-requests; replaced responses with respx
pyproject.toml Updated dependencies: httpx>=0.28.1 instead of requests; respx instead of responses; removed types-requests
todoist_api_python/types.py New shared type aliases module for ColorString, LanguageCode, ViewStyle (extracted from api.py)
todoist_api_python/_core/type_aliases.py Re-exports type aliases from types.py for backwards compatibility
todoist_api_python/_core/utils.py Removed run_async and generate_async functions; added kwargs_without_none helper
todoist_api_python/_core/http_requests.py Complete rewrite using httpx.Client/AsyncClient; added true async variants (get_async, post_async, delete_async); uses httpx.Timeout instead of tuple
todoist_api_python/_core/http_headers.py Removed Content-Type header creation (httpx handles this automatically with json= parameter)
todoist_api_python/api.py Updated to use httpx.Client; replaced manual dict building with kwargs_without_none; removed noqa PLR0912 comments
todoist_api_python/api_async.py Complete rewrite with true async HTTP; added proper client lifecycle management (aenter, aexit, close, del with ResourceWarning); uses AsyncIterator instead of AsyncGenerator; implements AsyncResultsPaginator
todoist_api_python/authentication.py Migrated to httpx with context managers for client lifecycle; added async variants; uses _managed_client and _managed_async_client helpers
tests/* Migrated all tests from responses to respx; updated mocking utilities; added async client lifecycle test; fixtures now use context managers
docs/* Added async usage guidance; documented breaking changes; updated authentication examples
README.md Added migration guide from 3.x to 4.x; documented async client lifecycle requirements
CHANGELOG.md Documented all breaking changes for version 4.0.0

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Collaborator

@lefcha lefcha Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this file? I don't see it being used anywhere...

- **Breaking**: `TodoistAPIAsync` now accepts an optional `client: httpx.AsyncClient` instead of `session: requests.Session`.
- **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`.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also mention that all AsyncGenerator became AsyncIterator in the breaking changes, in case people have typed variables with the former?


def _parse_response(
response: httpx.Response,
_result_type: type[T] | None = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of _result_type and the result_type we pass around in other functions?

version = "3.2.1"
description = "Official Python SDK for the Todoist API."
authors = [{ name = "Doist Developers", email = "dev@doist.com" }]
requires-python = "~=3.9"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably can fix this, too. I get this when running uv:

warning: The requires-python specifier (~=3.9) in todoist-api-python uses the tilde specifier (~=) without a patch version. This will be interpreted as >=3.9, <4. Did you mean ~=3.9.0 to constrain the version as >=3.9.0, <3.10? We recommend only using the tilde specifier with a patch version to avoid ambiguity.

Copy link
Collaborator

@lefcha lefcha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. I only had some comments about minor things (see inline comments).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Running Synchronous Code as Async Using Lambdas (Code Smell)

2 participants