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-notifications

2-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

35 providers ship with thinwrap-notifications@1.2.0.
ProviderChannelAuthREADME
sendgrid SendGridemailbearerdocs
ses AWS SESemailaws-sig-v4docs
mailgun Mailgunemailbasicdocs
postmark Postmarkemailheaderdocs
resend Resendemailbearerdocs
brevo Brevo (Sendinblue)emailheaderdocs
mailersend MailerSendemailbearerdocs
mailtrap Mailtrapemailbearerdocs
sparkpost SparkPostemailheaderdocs
scaleway Scalewayemailheaderdocs
twilio Twiliosmsbasicdocs
vonage Vonagesmsquerydocs
plivo Plivosmsbasicdocs
sns AWS SNSsmsaws-sig-v4docs
messagebird MessageBird (Bird)smsheaderdocs
infobip Infobipsmsheaderdocs
telnyx Telnyxsmsbearerdocs
sinch Sinchsmsbearerdocs
textmagic Textmagicsmsheaderdocs
d7networks D7 Networkssmsbearerdocs
fcm Firebase Cloud Messagingpushrs256-jwtdocs
apns Apple Push Notificationspushes256-jwtdocs
expo Expo Pushpushbearerdocs
one-signal OneSignalpushheaderdocs
pusher-beams Pusher Beamspushbearerdocs
wonderpush WonderPushpushbearerdocs
slack Slackchatwebhook-urldocs
discord Discordchatwebhook-urldocs
ms-teams Microsoft Teamschatwebhook-urldocs
telegram Telegramchatwebhook-urldocs
whatsapp-business WhatsApp Businesschatbearerdocs
line LINEchatbearerdocs
google-chat Google Chatchatwebhook-urldocs
rocket-chat Rocket.Chatchatwebhook-urldocs
mattermost Mattermostchatwebhook-urldocs

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.

Read more