Metadata-Version: 2.4
Name: catalisa-decision
Version: 0.1.0
Summary: Catalisa Decision Python SDK — run credit decisions, read them back, list policies and verify webhook deliveries (standard library only)
Author-email: Catalisa <contato@catalisa.io>
License: MIT
Project-URL: Homepage, https://decision.catalisa.app
Project-URL: Source, https://github.com/catalisaio/catalisa-decision-sdk
Project-URL: Issues, https://github.com/catalisaio/catalisa-decision-sdk/issues
Keywords: catalisa,decision,dmn,credito,underwriting,webhook,fintech
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

# `catalisa-decision`

SDK Python do **Catalisa Decision**: rode uma decisão, leia de volta, liste as suas, liste as suas políticas e verifique as entregas de webhook.

```bash
pip install catalisa-decision
```

> **A API pública entra no ar com a Fase 5.** O padrão dos dois pacotes é
> `https://api.decision.catalisa.app/v1`, e esse host ainda está sendo publicado.
> Até então, aponte para o seu ambiente:
> `Decision(api_key=..., base_url="https://...")` —
> ou a variável `base_url`. O restante do SDK não muda.


Python 3.9+. **Sem dependências** — só biblioteca padrão, inclusive a verificação RSA da assinatura do webhook.

## Começando

```python
from catalisa_decision import Decision

d = Decision(api_key=os.environ["CATALISA_DECISION_API_KEY"])

r = d.execute(
    config_key="analise-credito-pessoal",
    input={"cpf": "12345678901", "valorSolicitado": 10000},
    idempotency_key="proposta-2026-09-26-00042",
)
print(r["status"], r["output"])  # COMPLETED {'resultado': 'APROVADO', 'limite': 15000}
```

## A chave de idempotência

Passe `idempotency_key` em tudo que custa dinheiro. A conexão caiu e você não sabe se a decisão saiu? Repita com a mesma chave: se a primeira chegou, você recebe a **mesma** decisão e o mesmo `executionId`.

Ela também é o que permite ao SDK repetir um 5xx. **Sem ela o `execute` não é repetido**, de propósito: a esteira lê fonte externa a cada execução, então decidir de novo pode dar outra resposta — e cobrar duas vezes. Leituras repetem sempre; `429` também.

## Subcontas

```python
d = Decision(api_key=chave, subaccount_id="sub_do_cliente")
```

Chave já presa a uma subconta ignora isso. Chave **da organização** precisa, e esquecer lê a organização inteira — todos os seus clientes somados. O erro responde 200, que é o que o torna perigoso.

## Superfície

| Método | O que faz |
|---|---|
| `execute(config_key=, input=, mode="SYNC", trace_id=, timeout_ms=, callback_url=, idempotency_key=)` | Roda a decisão |
| `get_execution(execution_id)` | Uma decisão inteira, com `trace` e `mergedInput` |
| `list_executions(page=, page_size=, status=, mode=, config_id=, date_from=, date_to=, environment=)` | As suas decisões |
| `list_policies()` | As suas políticas |
| `verify_webhook` / `construct_event` | Verificação da entrega assinada |

Sem `environment`, as leituras trazem produção e sandbox. O SDK não escolhe um por você.

## Erros

```python
from catalisa_decision import QuotaExceededError, RateLimitError, DecisionError

try:
    d.execute(config_key="credito", input=dados)
except QuotaExceededError:
    ...                      # 402 — a cota do mês acabou
except RateLimitError as e:
    tentar_em(e.retry_after)
except DecisionError as e:
    log(e.code, e.request_id)  # request_id é o que o suporte pede
```

`AuthenticationError`, `PermissionDeniedError`, `QuotaExceededError`, `SubaccountSuspendedError`, `ValidationError`, `NotFoundError`, `DecisionTimeoutError`, `RateLimitError`, `ServerError`, `APIConnectionError`.

> `DecisionTimeoutError` leva o prefixo porque `TimeoutError` é builtin do Python: sombreá-lo faria um `except TimeoutError` seu pegar isto sem querer.

## Webhooks

```python
from catalisa_decision import construct_event

@app.post("/hooks/decision")
def hook():
    evento = construct_event(request.get_data(), request.headers, chaves_publicas)  # corpo CRU
    return "", 200
```

**Passe o corpo cru.** Reserializar o JSON muda os bytes e quebra a assinatura.

A assinatura é assimétrica: verifique com a chave **pública** da assinatura (`GET /webhooks-engine/api/v1/subscriptions/:id/keys`), não com segredo compartilhado. A tolerância de tempo (300 s) é do receptor.

## Testes

```bash
python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest -q
```

O vetor de assinatura em `vectors/vectors.json` é gerado pelo **código do próprio servidor** (`scripts/bb-vector.sh`), não pelo teste.
