feat: support the reworked bulk contact import (TPL-2105) - #8
Merged
Conversation
Adds the per-contact bulk create shape, the new bulk create response fields, the two bulk topic endpoints, and the duplicate-create 409. All additive: `emails` becomes optional on bulk_create() so it can host the alternative `contacts` shape, and the four new result fields default when the API omits them, so the result also parses a pre-TPL-2105 body. ContactAlreadyExistsError subclasses ConflictError, so existing `except ConflictError` handlers keep catching duplicates unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI runs `ruff format --check` alongside `ruff check`; three call sites in the new code fit on one line under the 100-char limit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tracks the API changes on
lettr-apivojta/tpl-2105-contact-import-bulk(PR #412). Mirrors what shipped inlettr-php2.5.0 andlettr-node(#19).Everything is additive — code written against 1.4.0 keeps working and sends the exact same payloads.
What's in here
Per-contact bulk create.
bulk_create()accepts a second shape where each contact carries its own properties, lists and topic subscriptions:emailsbecame optional so it can host the alternative; exactly one of the two must be given, and an empty call raisesValueErrorrather than round-tripping to the API. New types:BulkContactRow,TopicSubscription(with.opt_in()/.opt_out()),TopicSubscriptionState.TopicSubscriptionStateis deliberately separate from a topic'sdefault_subscription— one is what a request should do with a topic, the other is how the topic behaves for a contact that says nothing. Anopt_outon an auto-subscribe topic suppresses the auto-subscription in the same request instead of needing a second call.Bulk create reports what happened per row.
BulkContactImportResultgainsupdated,error_count,errors,contacts, plushas_errors/contact_ids/id_for(email). Ids come back in submission order, so chaining intobulk_attach_lists()/bulk_subscribe_topics()needs no lookup.Two things worth knowing, both covered by tests:
errors; the rest of the batch still commits and the call still returns 201. Nothing raises — callers must checkresult.has_errors.already_existedandupdatedoverlap by design. They answer different questions, so they don't sum to the row count: a contact that already existed and got attached to a list is counted in both.The four new fields default when the API omits them, so the result also parses a pre-TPL-2105 body.
Bulk topic subscribe/unsubscribe —
bulk_subscribe_topics()/bulk_unsubscribe_topics(), mirroring thebulk_attach_lists()/bulk_detach_lists()pair. Both process everycontact_ids×topic_idscombination (up to 1000 × 50). The unsubscribe is a DELETE with a body;httpxhandles that, as it already does forbulk_detach_lists().Duplicate creates are now a 409, not a 500.
create()raisesContactAlreadyExistsError, carrying the colliding.email. It subclassesConflictError, so existingexcept ConflictError/except LettrErrorhandlers keep working; a 409 with any other error code stays a plainConflictError.send_errorfor this endpoint should be corrected.Notes
[Unreleased]; perRELEASING.mdthe version bump happens at release time, so no version files are touched here.Test plan
pytest— 148 pass, including 10 new cases: the per-contact shape, the legacy flat payload going out byte-identical, defaulting when the API omits the new fields, partial success, theValueErrorguard, the typed 409 (and a 409 with another code not being specialised), and both topic endpoints.ruff check src testsmypy src(strict)🤖 Generated with Claude Code