Metadata-Version: 2.4
Name: datamais-api
Version: 0.3.0
Summary: Cliente Python da API DataMais, feito para bots de carga de dados.
Author: SECONV-RR
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/seconv-rr/datamais-lib-python
Project-URL: Issues, https://github.com/seconv-rr/datamais-lib-python/issues
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: Portuguese (Brazilian)
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.28.1
Requires-Dist: pandas>=3.0.1
Dynamic: license-file

# datamais-api-python

Cliente Python da API DataMais da **SECONV-RR** (Governo do Estado de
Roraima), feito para tornar triviais os bots de carga de dados ("cargas"):
autentique com uma chave de API, leia as linhas atuais de uma fonte, envie um
snapshot de substituição completo.

O código é escrito em português sem acentos, acompanhando a API e o seu wire;
apenas o nome de distribuição e de import continua `datamais_api`.

## Instalação

```sh
uv add datamais-api
```

Ou direto do repositório:

```sh
uv add git+ssh://git@github.com/seconv-rr/datamais-api-python.git
```

## Configuração

O cliente lê duas variáveis de ambiente; argumentos do construtor têm
precedência sobre elas:

| Variável | Propósito |
| --- | --- |
| `DATAMAIS_BASE_URL` | URL base da API (ex.: `https://api.datamais.example`) |
| `DATAMAIS_API_KEY` | Segredo da chave de API (`dmk_...`) com concessões `view`/`manage` nas fontes alvo |

Sem `DATAMAIS_BASE_URL`, a construção do cliente levanta `ErroConfigAusente`.
`DATAMAIS_API_KEY` é opcional: omita-a para clientes somente-sessão, que
autenticam com `entrar` e um token `Bearer` em vez de uma chave de máquina.

## Início rápido: um bot de carga

```python
from datamais_api import ClienteDatamais, Coluna, TipoColuna

colunas = [
    Coluna("convenio", "Convênio", TipoColuna.TEXTO, filtravel=True),
    Coluna("valor_global", "Valor Global", TipoColuna.DINHEIRO),
    Coluna("assinatura", "Assinatura", TipoColuna.DATA),
]
linhas = [
    {"convenio": "923456/2026", "valor_global": 150000.0, "assinatura": "2026-07-01"},
]

with ClienteDatamais() as cliente:
    cliente.substituir_dados_fonte("geral", "programas", colunas, linhas)
    dados = cliente.obter_dados_fonte("geral", "programas")
    print(dados.atualizado_em, len(dados.linhas))
```

`substituir_dados_fonte` substitui atomicamente o catálogo de colunas e as
linhas da fonte (`PUT /v1/grupos/{grupo}/fontes/{slug}/dados`); a chave de API
precisa possuir `dataset:<grupo>:<slug>:manage` (ou a equivalente em nível de
grupo, `group:<grupo>:manage`). `obter_dados_fonte` exige
`dataset:<grupo>:<slug>:view`, a menos que a fonte seja pública.

`criar_fonte` cadastra uma fonte em um grupo (`POST /v1/grupos/{grupo}/fontes`),
para que um bot de carga possa criar a fonte que está prestes a preencher:

```python
from datamais_api import ClienteDatamais, ErroConflito

with ClienteDatamais() as cliente:
    try:
        cliente.criar_fonte("geral", "programas", "Programas", descricao="SICONV")
    except ErroConflito:
        pass
    cliente.substituir_dados_fonte("geral", "programas", colunas, linhas)
```

`acoes` tem como padrão `ACOES_FONTE_PADRAO` (`view`, `manage`); a API sempre
acrescenta `admin`. A chave de API precisa de `group:<grupo>:manage` ou do
próprio `dataset:<grupo>:<slug>:manage` da fonte, e o grupo ainda precisa
existir e ter dono — uma chave não é um usuário, então ela nunca pode criar o
grupo. Uma fonte que já existe volta como `ErroConflito`; um grupo inexistente
ou sem dono, como `ErroValidacao` (`422`).

## Layout de painel (autoria)

Além das cargas de fonte, o cliente lê e escreve o **layout** de um painel — a
`DefinicaoPainel` (abas, grade, widgets, fontes) guardada como JSONB:

```python
from datamais_api import ClienteDatamais

with ClienteDatamais() as cliente:
    cliente.entrar("autora@example.com", "segredo")   # escrever layout exige sessão
    definicao = cliente.obter_layout("geral", "convenios")
    if definicao is not None:
        cliente.substituir_layout("geral", "convenios", definicao)
```

A autenticação é resolvida a cada requisição: um `token` de sessão explícito
(ou um obtido com `entrar`) é enviado como `Authorization: Bearer`; caso
contrário vai o `X-API-Chave`. `obter_layout` aceita qualquer um dos dois (ou
nenhum, para painéis públicos) e retorna `None` quando o painel não tem
layout. `substituir_layout` é um **endpoint de autoria** — somente sessão,
então levanta `ErroSessaoObrigatoria` sem um `entrar`/`token`, e a API nunca
aceita uma chave de API para ele.

`entrar` retorna um `ResultadoLogin` (token mais `Usuario`) e `None` quando as
credenciais são recusadas — uma senha errada é um resultado esperado, não uma
exceção. `eu` lê a sessão de volta da mesma forma, e `sair` descarta o token.

Compor uma `DefinicaoPainel` em Python é trabalho do SDK de autoria irmão, o
[`datamais-sdk`](../datamais-sdk-python), que constrói e valida a definição e
a publica através deste cliente.

## pandas

`DadosPainel.para_dataframe()` retorna as linhas como um DataFrame ordenado
pelo catálogo de colunas. Para cargas, `substituir_dados_fonte_de_dataframe`
envia um DataFrame inteiro em uma única chamada, inferindo o catálogo de
colunas a partir dos dtypes (bool → `bool`, inteiro → `int`, float →
`number`, datetime → `datetime`, o resto → `text`), a menos que uma lista
explícita de `colunas` seja passada. NaN e NaT viram `null`, e timestamps são
serializados como ISO-8601.

```python
import pandas as pd
from datamais_api import ClienteDatamais

quadro = pd.read_csv("convenios.csv")

with ClienteDatamais() as cliente:
    cliente.substituir_dados_fonte_de_dataframe("geral", "programas", quadro)
```

Vale a pena passar o catálogo explicitamente sempre que o painel se importa
com a renderização: a inferência não tem como adivinhar `money`, `date`,
`link`, `percent` nem quais colunas são `filtravel`.

## Erros

Toda resposta não-2xx levanta uma exceção tipada carregando `status`,
`detalhe` e a lista estruturada `erros` da API:

- `ErroAutenticacao` (401) — chave ausente, inválida, expirada ou revogada
- `ErroProibido` (403) — a chave não possui a permissão exigida
- `ErroNaoEncontrado` (404) — fonte desconhecida (grupo, slug)
- `ErroConflito` (409)
- `ErroValidacao` (422) — colunas/linhas malformadas
- `ErroAPI` — qualquer outro status não-2xx

Todas são subclasses de `ErroAPI`, que por sua vez é subclasse de
`ErroDatamais`, ao lado de `ErroConfigAusente` e `ErroSessaoObrigatoria`.

## Consumidores

- [`datamais-cargas`](../datamais-cargas) — os jobs de carga que alimentam as fontes.
- [`datamais-sdk`](../datamais-sdk-python) — o SDK de autoria de painéis.

## Publicação

Todo push na `main` roda o workflow de release: o python-semantic-release
calcula a próxima versão a partir dos Conventional Commits, escreve-a em
`pyproject.toml` e em `src/datamais_api/__init__.py`, cria a tag, atualiza o
changelog, compila com uv e — somente quando uma nova versão foi cortada —
publica no PyPI via `uv publish`, usando PyPI Trusted Publishing (sem
segredos de token).

Configuração única no PyPI: adicionar um trusted publisher para o projeto
`datamais-api` apontando para o repositório `seconv-rr/datamais-api-python` e
o workflow `release.yml`.

## Desenvolvimento

```sh
uv sync                      # instala as dependências
uv run pytest                # testes (offline, transporte mockado)
uv run ruff check src tests  # lint
uv run ruff format src tests # formatação
uv run basedpyright src      # checagem de tipos
```
