Skip to content

feat!: Lettermint Python SDK 3.0 - #33

Open
bjarn wants to merge 8 commits into
mainfrom
feat/v3
Open

bjarn wants to merge 8 commits into
mainfrom
feat/v3

Conversation

@bjarn

@bjarn bjarn commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A new major version, following the Lettermint SDK design agreed for all languages. The reference is the Node.js SDK 3.0 (lettermint/lettermint-node#36). Sending holds no state on the client, types are generated from the published API spec, and every SDK passes the same conformance suite.

What changes

  • One client with sendingToken and/or teamToken. A single token string is detected by prefix: lm_team_ is a team token, lm_ plus letters and digits is a sending token, and anything else is a config error. Each part uses its own token and never falls back to the other.
  • Stateless sending: emails.send(message, options), emails.sendBatch(…) and an immutable per-message compose() builder. The idempotency key is a per-call option. Attachments are objects.
  • Typed errors with the HTTP status and API error code, plus timeout, connection, unexpected-response and redirect errors.
  • Transport: redirects are never followed, so the token never reaches another host. The timeout covers the whole request, nothing is retried automatically, and tokens never appear in logs or debug output.
  • Generated types from the published API spec (lettermint/lettermint#2582, pinned to 0b4ecbdd23) using its names. Enums are open, so new API values never break decoding.
  • Team API: same layout in every SDK, with typed queries and cursor iteration.
  • Webhooks: verify(rawBody, headers) requires both X-Lettermint-Signature and X-Lettermint-Delivery, accepts any matching v1, uses a ±300 s tolerance, and keys the HMAC on the secret as-is. Reason codes are the same in every SDK.
  • UPGRADE.md has before/after for every changed call, the full type rename table, and a ready-to-copy coding-agent instruction. The previous major no longer receives updates.

Specific to this SDK

  • Lettermint and AsyncLettermint have identical surfaces. The sync layer is generated from the async one with scripts/unasync.py, and CI checks that it is up to date.
  • Python 3.10+, since 3.9 is end-of-life. Adds anyio (already pulled in by httpx).
  • Includes the earlier fixes: typing_extensions is declared for every Python version; webhook verification accepts bytes; a non-ASCII signature header no longer crashes.
  • No httpx exception travels with an SDK error, so the token never leaks through .request.
  • Release workflow: the version is now set in src/lettermint/_version.py, and the built wheel and sdist are checked against the tag.
  • Note: the "Update Changelog" job is blocked by branch protection on main (GH006). That is a repo setting, not code.

Verification

  • 216 tests pass on Python 3.10–3.14, on asyncio and trio. ruff and strict mypy are clean.
  • Conformance: 42/42 with --strict. 2.7.0 fails 8.

Release

Publish a GitHub release v3.0.0.

Docs for all SDK majors: lettermint/lettermint#2594 (stacked on lettermint/lettermint#2590). Merge it after the releases.

🤖 Generated with Claude Code

bjarn added 8 commits October 3, 2026 13:48
types.py imports typing_extensions unconditionally, but the dependency was
only declared for Python < 3.11. Installs on 3.11+ only worked when another
package happened to pull it in transitively.
Bytes payloads (e.g. Django's request.body) were interpolated into an
f-string, producing "t.b'...'" and always failing verification. The HMAC is
now computed over the raw bytes. Non-ASCII signature header values no longer
raise TypeError from hmac.compare_digest and fail as InvalidSignatureError.
Non-UTF-8 bodies raise JsonDecodeError.
…enerator

src/lettermint/_generated is written by the private lettermint/sdk-generator
(emit python, next naming profile, spec 80d8ab2a2d).
scripts/generate.py regenerates it, or with --check verifies it; without a
generator checkout it verifies the generated headers only.
… tokens

One client per mode, Lettermint and AsyncLettermint, with identical
surfaces: sending_token for emails.*, team_token for every other part, no
fallback between them, and a token string classified by its prefix. Sending
stores no message state: emails.send/send_batch take the message and a
per-call idempotency_key, and emails.compose() returns an immutable builder.

The async layer is written once; scripts/unasync.py generates the sync layer
from it, and the I/O-free core (request preparation, decoding, error mapping,
query serialization, tag validation) is shared. Requests never follow
redirects or retry, the timeout covers the whole request including the body,
and every httpx exception is translated into an SDK exception with no
chained request. Tokens never appear in repr, vars or pickles.

Typed exceptions (APIError and subclasses, APITimeoutError,
APIConnectionError, UnexpectedResponseError, RedirectError,
LettermintConfigError, LettermintValidationError), typed nested query
objects, iterate() generators that follow next_cursor, and a Webhook verifier
that requires both signature headers, accepts any v1 and reports a reason.

BREAKING CHANGE: Lettermint.email()/api(), ApiClient, AsyncApiClient, the
endpoint classes, MessageTag, the 2.x exceptions and type names are removed;
Python 3.10 or newer is required. See UPGRADE.md.
UPGRADE.md has a before/after for every changed call, the full type rename
table, the removed classes and types, and a ready-to-copy instruction for
upgrading with a coding agent. The wheel ships UPGRADE.md inside the package.
The spec at lettermint/lettermint@0b4ecbdd23 (main, the #2582 merge) is
identical to the previous pin; only the generated file headers change.
@bjarn
bjarn requested a review from a team as a code owner October 3, 2026 22:51
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.

1 participant