Metadata-Version: 2.4
Name: thinwrap-notifications
Version: 1.2.0
Summary: Lightweight, SDK-free, zero-dependency Python wrapper for sending email, SMS, push, and chat across 35 providers behind one normalized surface.
Author: Dmitry Polyanovsky
License-Expression: MIT
Project-URL: Homepage, https://thinwrap.dev
Project-URL: Repository, https://github.com/thinwrap/notifications-py
Keywords: notifications,email,sms,push,chat,sendgrid,twilio,ses,fcm,slack
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file

# 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`.

```bash
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

```python
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)
```

> `from` is a Python keyword, so the sender field is `from_` (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/`](docs/providers/) — one page per provider.

## Switching providers

```python
# 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`:

```python
@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:

```python
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`.

```python
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

```python
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`](https://github.com/thinwrap/notifications-ts)
- PHP — [`thinwrap/notifications`](https://github.com/thinwrap/notifications-php)
- Go — [`thinwrap/notifications-go`](https://github.com/thinwrap/notifications-go)
- Python — this package

## Security

Report vulnerabilities **privately** — please do not open a public issue. Preferred: a
[private security advisory](https://github.com/thinwrap/notifications-py/security/advisories/new)
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
