thinwrap-notifications (Python)
Unified, SDK-free, zero-runtime-dependency Python wrapper for sending
email, SMS, push, and chat across 35 providers. Switch vendor by changing the
config type; the input and output shapes stay identical. Synchronous, stateless,
and standard-library-only — bring your own HTTP transport if you want
httpx/requests.
pip install thinwrap-notifications
Requires Python ≥ 3.10. No runtime dependencies — AWS SigV4 (SES/SNS) and JWT signing (FCM RS256, APNs ES256) are implemented in pure Python on the stdlib.
Two-minute send
from thinwrap.notifications import Email, EmailSendInput, SendgridConfig, ConnectorError
e = Email(SendgridConfig(api_key="...", from_="hello@acme.com"))
try:
res = e.send(EmailSendInput(
to="user@example.com",
subject="Welcome",
text="Thanks for signing up!",
html="<p>Thanks for signing up!</p>",
))
print(res.status.value, res.provider_message_id)
except ConnectorError as e:
print(e.provider_code.value, e.provider_message)
fromis a Python keyword, so the sender field isfrom_(config default and per-send override).
Channels & providers
The config type selects the provider. A provider that doesn't serve a channel
raises ValueError at facade construction (e.g. Email(TwilioConfig(...))).
| Facade | Method | Providers |
|---|---|---|
Email |
send |
SES, Resend, Mailgun, SendGrid, Postmark, MailerSend, Mailtrap, Brevo, SparkPost, Scaleway |
Sms |
send |
Vonage, Twilio, Plivo, SNS, Sinch, Telnyx, Infobip, MessageBird, TextMagic, D7 Networks |
Push |
send |
FCM, Expo, APNs, OneSignal, Pusher Beams, WonderPush |
Chat |
send |
Telegram, Slack, WhatsApp Business, Discord, MS Teams, Google Chat, Mattermost, Rocket.Chat, LINE |
Per-provider details (endpoint, auth, error mapping, passthrough keys) live in
docs/providers/ — one page per provider.
Switching providers
# Same send() call, same EmailSendResult — only the config changes.
e = Email(ResendConfig(api_key="...", from_="hi@acme.com"))
# or PostmarkConfig(...), SesConfig(...), ...
The normalized surface
Every send returns a frozen <Channel>SendResult:
@dataclass(frozen=True)
class EmailSendResult:
success: bool
status: SendStatus # sent | queued | rejected | suppressed | unknown
provider_message_id: str
raw: Any # decoded vendor body, verbatim
Only fields ≥90% of a channel's providers support are first-class on the input
(the ≥90% baseline-coverage rule). Provider-specific fields flow through the
Passthrough escape hatch:
from thinwrap.notifications import Sms, SmsSendInput, TwilioConfig, Passthrough
s = Sms(TwilioConfig(account_sid="...", auth_token="...", from_="+15550001111"))
s.send(SmsSendInput(
to="+15552223333",
body="hi",
passthrough=Passthrough(body={"mediaUrl": ["https://cdn/img.png"]}), # Twilio MMS
))
passthrough.body is deep-merged into the request body, headers/query
shallow-merged; consumer values win on collision.
Errors
A failed send raises ConnectorError: a vendor error response, a transport
failure, or request data the connector rejects before sending (a missing
recipient, an invalid region, a header the vendor forbids, a header value
with a line break, a value that cannot be encoded) all arrive this way, as
invalid_request, which is never worth retrying.
Misusing the API itself raises a native exception when the facade is built: a
missing or non-config argument is a TypeError, and a config for another
channel (e.g. Sms(SendgridConfig(...))) is a ValueError.
try:
...
except ConnectorError as e:
# e.provider_code — ProviderCode: invalid_recipient | rate_limited | auth_failed |
# provider_unavailable | invalid_request | unknown
# e.status_code — int | None (None for transport failures)
# e.provider_message
# e.cause — {"raw": <vendor body>, "retryAfter": <hdr>, "retryAfterSeconds": <n>}
...
There is deliberately no retry_after_seconds attribute — the raw
Retry-After header and its parsed seconds ride in cause. Retry is consumer
policy.
Bring your own transport
from thinwrap.notifications import Email, SendgridConfig
e = Email(SendgridConfig(api_key="..."), transport=my_transport)
my_transport implements Transport (a send(HttpRequest) -> HttpResponse
method). The default stdlib urllib transport never follows redirects.
repr(HttpRequest) hides header values, the URL path and the body, so a
transport that logs requests does not log credentials; the request still holds
them, and so do the local variables of the frames in a traceback, so an error
tracker that captures frame locals needs its own scrubbing. The
wrapper is stateless: no caching, retries, or telemetry. FCM/APNs sign a fresh
token on every send unless you supply a TokenCacheHook on the config. For
APNs the cache is required in production: Apple rejects a new provider token
more often than once per 20 minutes (429 TooManyProviderTokenUpdates).
Relationship to the siblings
One of four thinwrap notifications libraries sharing a normalized surface and byte-identical provider ids / error codes:
- TypeScript —
@thinwrap/notifications - PHP —
thinwrap/notifications - Go —
thinwrap/notifications-go - Python — this package
Security
Report vulnerabilities privately — please do not open a public issue. Preferred: a private security advisory on this repository. Alternatively, email security@thinwrap.dev. Include the affected versions and a minimal reproduction if you have one.
A vulnerability in a provider's own API or service belongs to that vendor rather than to this wrapper — please report those upstream.
License
MIT © Dmitry Polyanovsky & contributors
Metadata
Release files for thinwrap-notifications 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| thinwrap_notifications-1.2.0.tar.gz | 120.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| thinwrap_notifications-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 229.3 kB
Release files / thinwrap_notifications-1.2.0.tar.gz
| Download URL | thinwrap_notifications-1.2.0.tar.gz |
|---|---|
| Size | 120.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
efffc75c827cb0a4b1937a3866918648e7e9217fc0685e0729436161f312d1b9
|
|
BLAKE2b-256 checksum How to use checksums |
d005944160a71ab085eb93cc2c727eca451c985f4da16418065a2a57ff3da556
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / thinwrap_notifications-1.2.0-py3-none-any.whl
| Download URL | thinwrap_notifications-1.2.0-py3-none-any.whl |
|---|---|
| Size | 109.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d45df49c822de2ea56f8026b9067650bbf1193107bfcbd720db5cf25a777946c
|
|
BLAKE2b-256 checksum How to use checksums |
ac01d1df5006cc22fd5160f5579664f02bced009bdd4b432e0ccc199429c5777
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logSecurity for thinwrap-notifications 1.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for thinwrap_notifications-1.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.