Metadata-Version: 2.5
Name: disparacom
Version: 1.1.0
Summary: Official Python SDK for the Disparacom e-mail marketing and automation API
Project-URL: Documentation, https://disparacom.web.app/docs/
Project-URL: Homepage, https://disparacom.web.app
Project-URL: Changelog, https://disparacom.web.app/docs/#changelog
Author: Disparacom
License: MIT
License-File: LICENSE
Keywords: automation,disparacom,email,marketing,resend,transactional
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# disparacom — SDK Python oficial

Cliente Python para a **API Disparacom** (e-mail marketing e automação).
Seu sistema manda contatos e eventos; o Disparacom cuida de templates, fluxos,
entrega e compliance (unsubscribe / LGPD).

- **Zero dependências** — só a biblioteca padrão. Nada de conflito de versão de `requests`/`httpx`.
- **Python 3.9+**, com type hints completos (`py.typed`).
- Erros tipados, retry automático com backoff, e cliente **async** incluso.

Documentação completa da API: **<https://disparacom.web.app/docs/>**

---

## Instalação

```bash
pip install disparacom
```

Enquanto o pacote não estiver no PyPI, instale direto do repositório:

```bash
pip install "git+ssh://git@bitbucket.org/<workspace>/disparacom.git#subdirectory=sdk/python"
```

## Começando

```python
from disparacom import Disparacom

client = Disparacom(api_key="SUA_CHAVE")   # ou exporte DISPARACOM_API_KEY

# 1. No cadastro — cria o contato e dispara os fluxos de onboarding.
client.contacts.upsert(
    "ana@exemplo.com",
    name="Ana",
    attributes={"plano": "pro"},
)

# 2. Em qualquer comportamento relevante — inicia e para fluxos sozinho.
client.events.track(
    "ana@exemplo.com",
    "cart_abandoned",
    attributes={"checkoutUrl": "https://loja/c/abc", "cartValue": "189,90"},
)
```

São essas **duas chamadas**. Toda a inteligência de e-mail (quando mandar, o que
mandar, quando parar) fica configurada no painel do Disparacom, não no seu código.

### Regra de ouro das variáveis

Os templates renderizam `{{variavel}}` a partir dos **atributos do contato**. Como
`events.track()` mescla `attributes` no contato, sempre mande no evento os dados
que o e-mail vai precisar:

```python
client.events.track(
    "ana@exemplo.com",
    "cart_abandoned",
    attributes={"checkoutUrl": "https://loja/c/abc"},   # vira {{checkoutUrl}}
)
```

### Primeiro evento de um contato novo

`events.track()` exige que o contato já exista. Quando não dá pra garantir isso,
use o atalho que faz as duas chamadas na ordem certa:

```python
client.identify_and_track(
    "ana@exemplo.com",
    "trial_started",
    name="Ana",
    attributes={"trialEndsAt": "2026-09-01"},
)
```

## Configuração

```python
client = Disparacom(
    api_key="...",                 # ou $DISPARACOM_API_KEY
    base_url="http://localhost:5001/disparacom/us-central1/api",  # ou $DISPARACOM_BASE_URL
    timeout=30.0,                  # segundos por requisição
    max_retries=2,                 # em erro de rede e 408/429/5xx
    backoff_factor=0.5,            # base do backoff exponencial (com jitter)
)
```

Retries usam backoff exponencial com jitter e respeitam o header `Retry-After`.
Só respostas transitórias são repetidas — 4xx de validação sobem na hora.

> **Sobre `send()` e retry:** o envio transacional não é idempotente do lado da
> API. O retry só acontece em 408/429/5xx e erro de rede, mas se você precisa da
> garantia de "no máximo um envio", construa esse cliente com `max_retries=0`.

## Envio transacional

Para um e-mail imediato, sem fluxo (recibo, confirmação, código):

```python
client.send("ana@exemplo.com", "tpl_recibo", variables={"orderId": "1234"})
```

O contato precisa existir (o token de unsubscribe nasce com ele). Limite de
**5 e-mails por contato a cada 24h**.

## Fluxos de automação

Steps são montados com helpers que numeram o `index` pra você e já barram
localmente os erros que a API rejeitaria:

```python
from disparacom import send_email, wait, check_event, condition

flow = client.flows.create(
    name="Carrinho abandonado",
    trigger="event",
    trigger_event="cart_abandoned",
    stop_condition="purchase_completed",       # para o fluxo quando a compra sai
    steps=[
        send_email("tpl_lembrete"),
        wait(2, "hours"),
        send_email("tpl_desconto", condition=condition("plano", "eq", "pro")),
        wait(1, "days"),
        check_event("purchase_completed"),     # para aqui também, por garantia
        send_email("tpl_ultima_chamada"),
    ],
)

client.flows.activate(flow["id"])   # fluxo SEMPRE nasce pausado
```

Regras que os helpers verificam antes de gastar uma requisição:

| Regra | Onde |
|---|---|
| Pelo menos 1 `send_email` | local + API |
| Nunca dois `send_email` seguidos sem `wait` | local + API |
| Espera mínima de 10 minutos | local + API |
| Fluxo com `wait` precisa de `stop_condition` ou `check_event` | API (`InvalidFlowError`) |

## Campanhas (envio em massa)

```python
from disparacom import segment_attribute, segment_inactive_days

campaign = client.campaigns.create(
    name="Black Friday",
    template_id="tpl_bf",
    segment=segment_attribute("plano", "eq", "pro"),   # ou segment_inactive_days(60)
)                                                      # sem segment = toda a base ativa

client.campaigns.send(campaign["id"])                  # dispara agora
# client.campaigns.send(campaign["id"], scheduled_at=datetime(2026, 11, 27, 9, 0))

progress = client.campaigns.get(campaign["id"])
print(progress["sent"], "/", progress["total"])
```

O disparo é assíncrono: a API marca a campanha como `sending` e um job entrega em
lotes, pulando quem deu unsubscribe e respeitando o limite diário por contato.

## Tratamento de erros

Todo erro herda de `DisparacomError`, então dá pra pegar o caso específico ou a
família inteira:

```python
from disparacom import (
    ContactUnsubscribedError, DailyLimitReachedError,
    NotFoundError, RateLimitError, DisparacomError,
)

try:
    client.send("ana@exemplo.com", "tpl_recibo")
except ContactUnsubscribedError:
    pass                       # opt-out: não insista, é LGPD
except DailyLimitReachedError:
    schedule_for_tomorrow()    # já levou 5 e-mails nas últimas 24h
except NotFoundError:
    client.contacts.upsert("ana@exemplo.com")
except DisparacomError as exc:
    log.warning("disparacom falhou: %s (code=%s)", exc, getattr(exc, "code", None))
```

| Exceção | HTTP | Quando |
|---|---|---|
| `BadRequestError` | 400 | campo obrigatório ausente ou inválido |
| `AuthenticationError` | 401 | `x-api-key` ausente/inválida, projeto inativo |
| `PermissionDeniedError` | 403 | recurso é de outro projeto |
| `NotFoundError` | 404 | contato/template/fluxo/campanha inexistente |
| `ConflictError` | 409 | template em uso, fluxo ativo, campanha enviando |
| `InvalidFlowError` | 422 | fluxo reprovado na validação (`.errors` lista os motivos) |
| `ContactUnsubscribedError` | 422 | contato com opt-out |
| `RateLimitError` | 429 | 1000 req/h do projeto |
| `DailyLimitReachedError` | 429 | 5 e-mails/contato/24h |
| `SendFailedError` | 502 | provedor de envio recusou |
| `InternalServerError` | 5xx | falha da API |
| `APITimeoutError` / `APIConnectionError` | — | rede |

## Cliente assíncrono

Mesma superfície, tudo `await`:

```python
from disparacom import AsyncDisparacom

async with AsyncDisparacom(api_key="...") as client:
    await client.contacts.upsert("ana@exemplo.com")
    await client.send("ana@exemplo.com", "tpl_recibo")
```

As chamadas rodam em worker thread (`asyncio.to_thread`), então o SDK continua
sem dependências e nunca trava seu event loop.

## Timestamps

A API devolve timestamps do Firestore (`{"_seconds": ..., "_nanoseconds": ...}`):

```python
from disparacom import parse_timestamp

log = client.logs.list(limit=1)[0]
print(parse_timestamp(log["sentAt"]))   # datetime aware em UTC
```

## Endpoints ainda não cobertos

Nada trava: `client.request()` fala com qualquer rota, com autenticação, retry e
erros tipados iguais aos do resto do SDK.

```python
client.request("POST", "/ai/generate-integration", json_body={"language": "python"})
```

## Desenvolvimento

```bash
cd sdk/python
PYTHONPATH=src:tests python3 -m unittest discover -s tests -v
```

109 testes, sem rede — o transporte HTTP é injetável e os testes usam um fake
in-memory (`tests/fake_transport.py`).

Lint e tipos (opcionais, `pip install -e ".[dev]"`):

```bash
ruff check src tests && mypy
```

## Licença

MIT — veja [LICENSE](./LICENSE).
