thinwrap-notifications
thinwrap-notifications for Python — unified facade for email, SMS, push, and chat across 35 providers. One facade per channel; switch vendor by changing the config class. Zero runtime deps (stdlib urllib), synchronous, stateless, BYO transport.
Python constraints
- Runtime dependencies
- Zero runtime dependencies
- BYO HTTP client
- Transport protocol: any object with send(HttpRequest) -> HttpResponse, passed as transport=
- Provenance
- PEP 740 attestations (PyPI Trusted Publishing)
- Minimum runtime
- Python 3.10+
- Truly zero dependencies — the standard library urllib.request is the only HTTP surface, and AWS SigV4 (SES, SNS, Bedrock) / JWT signing (FCM RS256, APNs ES256) are implemented in pure Python on hashlib and hmac, with no cryptography package.
- Synchronous by design. For async or pooled HTTP, inject an httpx or requests based Transport; the library never imports either.
- The default transport never follows redirects — a 3xx surfaces as an error rather than silently re-sending auth headers to another host.
30-second install
pip install thinwrap-notifications2-minute end-to-end example
One facade per channel; the config class selects the provider. A provider that doesn't serve a channel is rejected when the facade is built (Email(TwilioConfig(…)) raisesValueError), before anything is sent.
import os
from thinwrap.notifications import Email, EmailSendInput, SendgridConfig, ConnectorError
e = Email(SendgridConfig(api_key=os.environ["SENDGRID_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 err:
print(err.provider_code.value, err.provider_message)Switch vendor by changing the config class — swap toResendConfig, PostmarkConfig,SesConfig, and so on. The send() call and theEmailSendResult stay identical. Same pattern forSms, Push, and Chat. (from is a Python keyword, so the sender field isfrom_.)
Supported providers
| Provider | Channel | Auth | README |
|---|---|---|---|
sendgrid SendGrid | bearer | docs | |
ses AWS SES | aws-sig-v4 | docs | |
mailgun Mailgun | basic | docs | |
postmark Postmark | header | docs | |
resend Resend | bearer | docs | |
brevo Brevo (Sendinblue) | header | docs | |
mailersend MailerSend | bearer | docs | |
mailtrap Mailtrap | bearer | docs | |
sparkpost SparkPost | header | docs | |
scaleway Scaleway | header | docs | |
twilio Twilio | sms | basic | docs |
vonage Vonage | sms | query | docs |
plivo Plivo | sms | basic | docs |
sns AWS SNS | sms | aws-sig-v4 | docs |
messagebird MessageBird (Bird) | sms | header | docs |
infobip Infobip | sms | header | docs |
telnyx Telnyx | sms | bearer | docs |
sinch Sinch | sms | bearer | docs |
textmagic Textmagic | sms | header | docs |
d7networks D7 Networks | sms | bearer | docs |
fcm Firebase Cloud Messaging | push | rs256-jwt | docs |
apns Apple Push Notifications | push | es256-jwt | docs |
expo Expo Push | push | bearer | docs |
one-signal OneSignal | push | header | docs |
pusher-beams Pusher Beams | push | bearer | docs |
wonderpush WonderPush | push | bearer | docs |
slack Slack | chat | webhook-url | docs |
discord Discord | chat | webhook-url | docs |
ms-teams Microsoft Teams | chat | webhook-url | docs |
telegram Telegram | chat | webhook-url | docs |
whatsapp-business WhatsApp Business | chat | bearer | docs |
line LINE | chat | bearer | docs |
google-chat Google Chat | chat | webhook-url | docs |
rocket-chat Rocket.Chat | chat | webhook-url | docs |
mattermost Mattermost | chat | webhook-url | docs |
Migration paths
Zero-dependency signing
No vendor SDKs and no third-party dependencies — AWS SigV4 (SES/SNS) and JWT signing (FCM RS256, APNs ES256) are implemented in pure Python onhashlib and hmac, so there is nocryptography package to install. The wrapper is stateless: FCM/APNs sign a fresh token on every send unless you supply a token_cache on the config.
Passthrough escape hatch
Only fields ≥90% of a channel's providers support are first-class on the input. Provider-specific fields flow through Passthrough(body deep-merged, headers/queryshallow-merged; consumer values win):
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
))Bring your own transport
Pass any object with send(HttpRequest) -> HttpResponse astransport=. The default stdlib transport never follows redirects, and repr(HttpRequest) hides header values and the body so a logging transport doesn't log credentials. There is deliberately no retry_after_seconds attribute — the rawRetry-After header rides in cause and retry is consumer policy.
Cross-language siblings
The TypeScript (/packages/notifications-ts), PHP (/packages/notifications-php), and Go (/packages/notifications-go) variants ship the same facade shape and provider set — cross-language parity is a design goal, not a hard release gate.