Metadata-Version: 2.4
Name: control-tower-sdk
Version: 0.3.0
Summary: Cliente Python (sync/async) para enviar eventos ao RPA Control Tower
Author-email: Luiza Santos <luizasantos@maxtrack.com.br>
License: MIT
Keywords: rpa,observability,automation,monitoring
Classifier: Development Status :: 4 - Beta
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.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: psutil<7.0,>=5.9
Provides-Extra: dev
Requires-Dist: pytest<9.0,>=8.3; extra == "dev"
Requires-Dist: pytest-asyncio<1.0,>=0.24; extra == "dev"
Dynamic: license-file

# control-tower-sdk

Cliente Python (sync + async) pra automações RPA reportarem eventos ao
[RPA Control Tower](../README.md).

## Princípio central

**Observabilidade nunca pode derrubar a automação.** Se a API Control Tower estiver fora do ar ou
a rede falhar, o SDK loga um warning local e segue — nunca propaga exceção de rede pro seu código.
A única exceção que sobe pro seu `with`/`async with` é a que o **seu próprio código** levantar.

## Instalação

```bash
pip install -e ./sdk   # a partir da raiz do control-tower, em modo dev
```

## Uso — automação síncrona

```python
from control_tower_sdk import ControlTowerClient

client = ControlTowerClient(
    base_url="http://control-tower.internal:8010",
    api_key="...",  # mesmo valor de API_KEY configurado na API
)

ctes = fetch_ctes_pendentes()

with client.execution("API_CTEs", expected_items=len(ctes)) as run:
    run.start_stage("baixa_ctes")
    for cte in ctes:
        try:
            processar(cte)
            run.item_processed(business_reference=cte.chave)
        except Exception as exc:
            run.item_failed(
                business_reference=cte.chave,
                error_type=type(exc).__name__,
                error_message=str(exc),
            )
    run.finish_stage("baixa_ctes")
```

- `EXECUTION_STARTED` é enviado ao entrar no `with`.
- `EXECUTION_FINISHED` é enviado ao sair — `status="success"` se nada escapou do bloco,
  `status="failed"` se uma exceção não tratada subiu (e ela continua subindo normalmente depois,
  o SDK só observa, não decide o que seu código faz).
- `client.execution(nome, expected_items=None, metadata=None)` — `metadata` é um dict livre,
  útil pra diferenciar execuções da mesma automação sem criar `automation_name` separado. Ex.: um
  serviço que roda em ciclo agendado e também aceita disparo manual pode usar
  `metadata={"trigger": "scheduled"}` vs `metadata={"trigger": "manual"}` no mesmo
  `client.execution("meu_bot", metadata={...})`.

## Uso de CPU/memória (automático)

`ControlTowerClient(..., track_resource_usage=True)` (default) amostra o **consumo do próprio
processo** (via `psutil.Process()`, não a máquina inteira) a cada `EXECUTION_STARTED`,
`EXECUTION_FINISHED` e `heartbeat()`, e injeta em `metadata.resource_usage`:

```python
{"cpu_percent": 12.4, "memory_mb": 87.3}
```

Isso é mesclado com o `metadata` que você já passar — nunca sobrescreve suas chaves. Não é medido
em cada `item_processed`/`item_failed` (frequência alta demais, poluiria o payload). Se falhar por
qualquer motivo (`psutil` ausente, plataforma sem suporte), some da `metadata` silenciosamente —
mesma garantia de nunca quebrar a automação. Pra desligar: `track_resource_usage=False`.

## Uso — automação assíncrona

```python
from control_tower_sdk import AsyncControlTowerClient

client = AsyncControlTowerClient(base_url="http://control-tower.internal:8010", api_key="...")

async with client.execution("API_CTEs", expected_items=len(ctes)) as run:
    for cte in ctes:
        await processar_async(cte)
        await run.item_processed(business_reference=cte.chave)
```

## Métodos disponíveis em `run` (mesma API nos dois clientes, com/sem `await`)

| Método                                                                      | Evento                                                                    |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `start_stage(nome, message=None)`                                           | `STAGE_STARTED`                                                           |
| `finish_stage(nome, status="success", message=None)`                        | `STAGE_FINISHED` (`status="failed"` alimenta o alerta de etapa com falha) |
| `item_started(business_reference=None, message=None)`                      | `ITEM_STARTED`                                                            |
| `item_processed(business_reference=None, **counts)`                         | `ITEM_PROCESSED`                                                          |
| `item_failed(business_reference=None, error_type=None, error_message=None, message=None)` | `ITEM_FAILED`                                                |
| `warning(message)`                                                          | `WARNING`                                                                 |
| `error(message, error_type=None, error_message=None)`                       | `ERROR`                                                                   |
| `heartbeat()`                                                               | `HEARTBEAT`                                                               |
| `business_validation(message, **metadata)`                                  | `BUSINESS_VALIDATION`                                                     |

## Testes

```bash
cd sdk
pip install -e ".[dev]"
pytest -v
```

Os testes usam `httpx.MockTransport` — não precisam de API rodando. Falhas de rede são testadas
explicitamente (`test_network_failure_never_raises_to_caller`) pra garantir a promessa central do
SDK.
