Metadata-Version: 2.5
Name: zapcore-sdk
Version: 0.1.3
Summary: SDK oficial da API REST v2 do zapCore (WhatsApp): tipado, zero dependência, retry que respeita 429/Retry-After, idempotência automática e verificação de webhook HMAC
Project-URL: Homepage, https://zapcore.app
Project-URL: Source, https://github.com/adeiltonpessini/zapcore/tree/main/sdks/python
Author: Adeilton Pessini
License-Expression: MIT
License-File: LICENSE
Keywords: hmac,sdk,webhook,whatsapp,whatsapp-api,zapcore
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Communications :: Chat
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# zapcore-sdk (Python)

SDK oficial da API REST v2 do [zapCore](https://zapcore.app) (WhatsApp).
Python ≥ 3.9, **zero dependência** (só biblioteca padrão), tipado (`TypedDict` + `py.typed`).

> **Ainda não publicado no PyPI.** Não rode `pip install zapcore-sdk`: o nome ainda não é nosso
> no registro. Por enquanto instale desta pasta, a partir de um clone do repositório:
>
> ```sh
> pip install ./sdks/python
> ```

## Início rápido

```python
from zapcore import Client, ZapcoreError

zap = Client("https://api.seudominio.com", token="TOKEN_DA_INSTANCIA")  # host sem /api/v2

zap.send_text("+55 (27) 99999-9999", "Olá!")                    # pontuação é limpa
zap.send_image("5527999999999", open("foto.jpg", "rb").read(), "Seu pedido")
zap.send_image("5527999999999", "https://site/foto.jpg")       # o SDK baixa
zap.send_buttons("5527999999999", "Confirma o horário?", ["Sim", "Remarcar"])
zap.send_list("5527999999999", "Escolha o serviço", "Ver opções",
              [{"title": "Cabelo", "rows": [{"RowId": "corte", "title": "Corte", "desc": "R$ 50"}]}])
zap.send_poll("120363000000000000@g.us", "Melhor dia?", ["Sexta", "Sábado"])

zap.status()["connected"]
zap.get_qr()["QRCode"]                       # data URL
zap.pair_phone("5527999999999")["LinkingCode"]
zap.create_group("Equipe", ["5527999990000"])
zap.check_numbers(["5527999999999"])[0]["IsInWhatsapp"]

for msg in zap.iter_history("5527999999999@s.whatsapp.net"):   # pagina sozinho
    print(msg["timestamp"], msg.get("text_content"))
```

Toda rota existe como método gerado: `zap.post_chat_send_carousel(...)`,
`zap.get_group_list(query={"limit": 50})`… (235 operações). Para `meta`, status e headers use
`zap.request("GET", "/api/v2/session/limits")`.

## Votar em enquete, GIF, link, fila assíncrona e busca de conversas

Métodos gerados (nome = `operationId` da spec, em snake_case):

```python
# Votar numa enquete recebida (poll_msg_id do histórico local; options exatas)
zap.post_chat_send_pollvote({"poll_msg_id": "3EB0...", "options": ["Sexta"]})
# GIF (mp4 até 16 MB; URL ou data URL) e link com cartão de prévia
zap.post_chat_send_gif({"Phone": "5527999999999", "Gif": "https://site/anim.mp4", "Caption": "Oi"})
zap.post_chat_send_link({"Phone": "5527999999999", "Url": "https://site/oferta", "Body": "Veja:"})

# Fila assíncrona: enfileirar (async=true na query), listar, consultar, cancelar
zap.post_chat_send_text({"Phone": "5527999999999", "Body": "oi"},
                        query={"async": True, "track_source": "crm", "track_id": "pedido-42"})
zap.get_chat_send_async(query={"limit": 50})
zap.get_chat_send_async_id(id)
zap.delete_chat_send_async_id(id)                                   # 409 se já saiu
zap.post_chat_send_async_cancel({"track_source": "crm", "track_id": "pedido-42"})  # em massa

# Busca de conversas com filtros ("counts" só vem na 1ª página: sem cursor e offset 0)
r = zap.get_chat_search(query={"type": "group", "unread": True, "limit": 50})
```

Os filtros da listagem da fila (`status`, `track_source`, `track_id`, `since`) são suportados
([API.md](../../API.md)), na query de `get_chat_send_async`.

## Erros, retry e idempotência

```python
try:
    zap.send_text("5527999999999", "oi", idempotency_key=pedido_id)
except ZapcoreError as e:
    if e.code == "rate_limited":           # case por code, nunca pela mensagem
        print("tente em", e.retry_after, "s")
    print(e.status, e.field, e.request_id)
```

`429` espera o `Retry-After` (até `max_retry_wait`, padrão 60 s); `502/503/504` e falha de
rede re-tentam só GET e envios. Envios levam `Idempotency-Key` automática, a mesma em todos
os retries (`auto_idempotency=False` desliga). Opções: `Client(..., max_retries=3, timeout=15,
headers={...}, transport=meu_transporte)`.

**Redirect nunca é seguido**: o transporte padrão não tem `HTTPRedirectHandler`, e um 3xx vira
`ZapcoreError(code="redirect_refused")`. Seguir levaria o header `token` para o `Location`.

## Webhook (HMAC)

> **Verificação ESTRITA por padrão.** Só passa entrega com `x-hmac-signature-v2` +
> `x-webhook-timestamp` dentro de 5 min (anti-replay). Entrega só com a v1 (`x-hmac-signature`,
> sem carimbo) é **recusada**: ela não expira, então quem capturar uma pode reenviá-la para
> sempre. Motor antigo que ainda não manda a v2? Ligue ``allow_v1=True``, conscientemente.

```python
from zapcore import parse_webhook, WebhookVerificationError

# Flask: request.get_data() é o corpo CRU, não use request.json
@app.post("/zapcore")
def webhook():
    try:
        ev = parse_webhook(request.get_data(), request.headers, os.environ["ZAPCORE_HMAC_SECRET"])
    except WebhookVerificationError:
        return "", 401
    print(ev["type"], ev.get("event"))
    return "", 200
```

`verify_webhook(...)` devolve `bool` e nunca levanta por header malicioso; `tolerance_seconds`
ajusta a janela. Corpo assinado que não é objeto JSON levanta `WebhookVerificationError`.

## Desenvolvimento

```sh
PYTHONPATH=src python -m unittest discover -s tests
```

`src/zapcore/_generated.py` é gerado por `node ../gerar.mjs`, não edite.

---

## English

Official Python SDK (≥ 3.9, stdlib only) for the zapCore WhatsApp REST API v2. One generated
method per operation (snake_case of the `operationId`) plus helpers (`send_text`, `send_image`,
`send_buttons`, `send_list`, `send_poll`, `get_qr`, `pair_phone`, `status`, `create_group`,
`check_numbers`, `history`/`iter_history`). Webhook verification is strict by default (v1-only deliveries need `allow_v1=True`) and redirects are never followed. Retries honor `429 Retry-After`, send routes get an
automatic idempotency key reused across retries, errors raise `ZapcoreError` with a stable
`code`, and `verify_webhook`/`parse_webhook` check the HMAC-SHA256 signature.
