postmarker
Python client library for the Postmark transactional email API, with built-in Django and Tornado integrations.
Repository Health
Technical Analysis
Postmarker wraps the Postmark transactional email API behind a single PostmarkClient object, exposing every API area — emails, bounces, domains, messages, templates, sender signatures, server settings, stats, status, and triggers — as attached manager objects with consistent, Pythonic method names. It accepts Python’s own email.message.Message and MIMEMultipart instances directly, deconstructing multipart bodies and attachments automatically, which makes migrating existing smtplib-based sending code straightforward.
Beyond the core client, the package ships a drop-in Django EMAIL_BACKEND with pre-send/post-send/exception signals, a Tornado mixin for settings-driven sending, a spam-check helper, and a pytest plugin that mocks outgoing requests for testing code that sends email — all backed by a betamax-recorded test suite replaying real Postmark API responses.
What You Get
- A
PostmarkClientfacade that attaches one manager per Postmark API area (emails, bounces, domains, messages, templates, sender signatures, server, stats, status, triggers) with consistent method names. - A Django
EMAIL_BACKEND(postmarker.django.backend.EmailBackend) that routes Django’s normalsend_mail/EmailMessagecalls through Postmark’s batch send API, withpre_send/post_send/on_exceptionsignals. - A Tornado mixin (
postmarker.tornado.PostmarkMixin) that reads a Tornadosettingsobject and exposessend/send_batchhelpers. - Native support for Python’s
email.message.MessageandMIMEMultipartobjects as input, with automatic multipart/attachment deconstruction. - A pytest plugin (registered via
entry_points) providingpostmark_client/postmark_requestfixtures that mock outgoing HTTP calls for testing. - Automatic chunking/pagination for batch sends and list endpoints via
ModelManager._call_many(default chunk size 500).
Common Use Cases
- Configuring
EMAIL_BACKEND = "postmarker.django.backend.EmailBackend"so an existing Django app’s email calls route through Postmark with no call-site changes. - Mixing
PostmarkMixininto a TornadoRequestHandlerto send account/billing emails using the app’s existing settings object. - Passing existing
EmailMessage/MIMEMultipartobjects straight intopostmark.emails.send()when migrating offsmtplib. - Using the packaged pytest fixtures and betamax cassettes to test email-sending code paths without hitting the network.
Under The Hood
Architecture
PostmarkClient in core.py is the central facade; on init it iterates a _managers tuple (BounceManager, DomainsManager, EmailManager, MessageManager, SenderSignaturesManager, ServerManager, StatsManager, StatusManager, TemplateManager, TriggersManager) and attaches each manager instance to itself keyed by the manager’s name attribute — a self-registration pattern in _setup_managers. Each manager subclasses ModelManager (models/base.py), which proxies HTTP calls back through client.call, handles server vs. account token headers, and offers call_many/_call_many to auto-paginate collection endpoints in fixed-size chunks driven by a sizes() utility. Domain models subclass a thin Model value object backed by a _data dict with from_json/as_dict helpers. The Django and Tornado integrations sit outside the core and simply instantiate or reuse a PostmarkClient, translating framework-native message objects into Postmark’s JSON payload via helpers like deconstruct_multipart in emails.py. Because manager attachment is fully dynamic (setattr in a loop) rather than declared per-class, static type checkers and IDEs cannot see postmark.emails as an attribute without executing the code.
Tech Stack
A src/-layout package built with plain setuptools, with a single hard runtime dependency, requests>=2.20.0. The Django and Tornado integrations import those frameworks directly but are not declared as install dependencies, so using them without the framework present raises an ImportError at import time rather than failing gracefully. The test stack is comprehensive: pytest, pytest-django, pytest-tornado, betamax/betamax_serializers for HTTP cassette replay against recorded real Postmark responses, and coverage, matrixed across Python 3.6-3.10 plus PyPy 3.7/3.8 and Django 2.2/3.2/4.0 via tox.ini. CI runs on GitHub Actions with a dedicated commit-message linter (commitsar), pre-commit/pylint jobs, and a release workflow that publishes to PyPI on GitHub release. Documentation is built with Sphinx, one page per API resource, and hosted on Read the Docs.
Code Quality
The test suite is extensive — dozens of files under test/, including a models/ subpackage mirroring the source layout and per-endpoint betamax cassette fixtures that replay real recorded API responses rather than hand-rolled mocks. The library exposes its own testing infrastructure to consumers as a first-class pytest plugin. Error handling at the API boundary is explicit: check_response() catches requests.HTTPError, parses the Postmark JSON error body, and re-raises a domain-specific ClientError carrying the Postmark error code via raise ... from, preserving the exception chain. There are no type hints anywhere in the source and no static type checker configured. Style is enforced through black, isort, pylint, and other pre-commit hooks run in CI.
API Design
The manager-attribute pattern — postmark.emails.send(...), postmark.bounces.all(), postmark.templates.get(...) — mirrors Postmark’s own API documentation closely, so users familiar with Postmark’s docs can largely guess method names. PostmarkClient.from_config() lets any dict-like settings object construct a client via a postmark_-prefixed key convention, reducing framework glue code. Accepting native email.message.Message/MIMEMultipart objects directly, rather than requiring a bespoke payload dict, is a genuinely convenient design choice for teams migrating from smtplib. Documentation is organized one page per API resource plus dedicated Django/Tornado/testing/webhooks guides. The main friction points are the lack of type stubs for IDE autocompletion of dynamically attached manager attributes, and an assert server_token, "..." in __init__ that fails as a bare AssertionError rather than a typed exception.