Metadata-Version: 2.5
Name: sending-sdk
Version: 0.4.0
Summary: SDK Python ufficiale per l'API di Sending.dev (email + WhatsApp + Telegram)
Project-URL: Homepage, https://sending.dev
Project-URL: Documentation, https://sending.dev/api-reference
Project-URL: Repository, https://github.com/bluworl/sending
Project-URL: Issues, https://github.com/bluworl/sending/issues
Author: Sending.dev
License: MIT
Keywords: api,email,sdk,sending,telegram,whatsapp
Requires-Python: >=3.10
Requires-Dist: httpx>=0.24
Requires-Dist: pydantic[email]>=2
Provides-Extra: dev
Requires-Dist: datamodel-code-generator>=0.25; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Description-Content-Type: text/markdown

# sending-sdk

SDK Python ufficiale per l'[API di Sending.dev](https://sending.dev) (email + WhatsApp + Telegram).

I modelli Pydantic sono generati dallo spec OpenAPI; il client e' scritto a mano per la migliore DX.

## Installazione

```bash
pip install sending-sdk
```

Si importa come `sending`:

```python
from sending import Sending
```

Richiede Python >= 3.10.

## Uso

```python
from sending import Sending

client = Sending(api_key="sk_...")

# Email transazionale (accetta un dict oppure un modello Pydantic)
client.emails.send({
    "to": "cliente@esempio.com",
    "from": "you@tuodominio.com",
    "subject": "Benvenuto",
    "html": "<p>Ciao!</p>",
    "idempotencyKey": "welcome-001",
})

# Messaggio multi-canale
client.messages.send({
    "channel": "whatsapp",
    "to": "+393331234567",
    "text": "Ciao!",
    "idempotencyKey": "wa-001",
})
```

### Invio batch

Fino a 100 email in una richiesta, che ai fini del rate limit ne vale **una**. L'esito e'
parziale: `results` e' allineato per indice alla lista passata, quindi un elemento fallito
non impedisce gli altri, e un duplicato idempotente torna `accepted` con `duplicate: true`.

```python
esito = client.emails.send_batch([
    {"from": "Acme <ciao@acme.dev>", "to": "sara@acme.io", "subject": "Ciao",
     "html": "<p>hey</p>", "idempotencyKey": "b-001"},
    {"from": "Acme <ciao@acme.dev>", "to": "capo@acme.io", "subject": "Ciao",
     "html": "<p>hey</p>", "idempotencyKey": "b-002"},
])
for r in esito["results"]:
    if r["status"] == "failed":
        print(f"elemento {r['index']}: {r['error']['code']}")
```

### Copie visibili e nascoste

`cc` e `bcc` valgono su `emails.send`, `emails.send_batch`, `messages.send` (canale email) e
sugli invii dalle inbox agente. Massimo 50 destinatari per messaggio, e **ogni destinatario
consuma una unita' di quota**.

```python
client.emails.send({
    "from": "Acme <ciao@acme.dev>",
    "to": "sara@acme.io",
    "cc": ["capo@acme.io"],
    "bcc": ["archivio@acme.io"],
    "subject": "Preventivo",
    "html": "<p>In allegato.</p>",
    "idempotencyKey": "prev-001",
})
```

Come context manager (chiude la connessione httpx):

```python
with Sending(api_key="sk_...") as client:
    page = client.contacts.list(q="mario", page=1, page_size=50)
    print(page["total"], page["data"])
```

### Contatti, liste e tag

`contacts.create`, `lists.create` e `tags.create` sono idempotenti: agganciano quello che esiste gia' invece di sollevare un errore, quindi non serve cercare prima di scrivere e un retry non crea doppioni. La risposta porta sempre `created`.

```python
with Sending(api_key="sk_...") as client:
    contatto = client.contacts.create({
        "attributes": {"firstName": "Mario"},
        "identities": [
            {"channel": "email", "address": "mario@esempio.com", "consent": "opted_in"}
        ],
    })
    print(contatto["id"], contatto["created"])  # created False = esisteva gia'

    client.contacts.get(contatto["id"])
    client.contacts.delete(contatto["id"])
```

Gli attributi si fondono (`None` cancella la chiave) e il consenso viene toccato solo se lo dichiari, cosi un payload che tace non azzera un opt-in raccolto altrove.

### Client asincrono

Stessa API, su `httpx.AsyncClient`:

```python
import asyncio
from sending import AsyncSending

async def main():
    async with AsyncSending(api_key="sk_...") as client:
        await client.emails.send({
            "to": "cliente@esempio.com",
            "from": "you@tuodominio.com",
            "subject": "Benvenuto",
            "html": "<p>Ciao!</p>",
            "idempotencyKey": "welcome-001",
        })
        page = await client.contacts.list(q="mario", page=1, page_size=50)
        print(page["total"])

asyncio.run(main())
```

### Allegati

Un allegato si carica una volta e si riusa su piu' invii: passa `attachments=[{"id": ...}]` a `emails.send`, `messages.send`, `campaigns.create` e agli invii dalle inbox agente.

```python
# File di qualsiasi dimensione: create + PUT firmato + confirm, in un metodo.
doc = client.attachments.upload("fattura.pdf", pdf_bytes, content_type="application/pdf")

client.emails.send({
    "to": "cliente@esempio.com",
    "from": "you@tuodominio.com",
    "subject": "La tua fattura",
    "html": "<p>In allegato la fattura.</p>",
    "attachments": [{"id": doc["id"]}],
    "idempotencyKey": "fattura-001",
})
```

Per i file piccoli c'e' `upload_bytes`, che li manda in base64 in una sola chiamata. Sul client async `upload` si attende come tutto il resto (`await client.attachments.upload(...)`).

In alternativa all'id puoi passare il file inline (`{"filename": ..., "content": ...}` in base64) o una URL pubblica (`{"filename": ..., "url": ...}`). Con `contentId` l'allegato diventa inline e lo referenzi nell'HTML come `cid:`.

Limiti: 20 allegati per messaggio, 10 MB l'uno, 25 MB complessivi. Le estensioni eseguibili sono rifiutate.

### Modelli tipizzati (opzionali)

```python
from sending import models

body = models.SendEmailInput(
    to="a@b.com", **{"from": "you@dominio.com"},
    subject="Ciao", html="<p>Hi</p>", idempotencyKey="welcome-001",
)
client.emails.send(body)  # serializzato con gli alias corretti (es. "from")
```

### Gestione errori

```python
from sending import SendingError

try:
    client.emails.send({...})
except SendingError as err:
    print(err.status, err.code, err.body)
```

## Risorse disponibili

`emails` · `messages` · `events` · `attachments` · `campaigns` · `contacts` · `lists` ·
`tags` · `custom_fields` · `segments` · `domains` · `inboxes` · `email_rules` ·
`automations` · `webhooks` · `utm` · `integrations` · `usage`.

## Sviluppo

```bash
pip install -e ".[dev]"
python scripts/gen.py   # rigenera sending/models.py da openapi.json
pytest
```

Lo `openapi.json` viene copiato qui da `pnpm openapi:generate` (package `@sending/openapi`).
