Metadata-Version: 2.3
Name: govhub-lakehouse
Version: 0.1.0
Summary: Data Lakehouse format-agnostic e infra-agnostic
Author: Lucas Bottino
Author-email: Lucas Bottino <lucasgabottino@gmail.com>
Requires-Dist: boto3>=1.34
Requires-Dist: pyiceberg[pyarrow]>=0.7
Requires-Dist: requests>=2.31
Requires-Dist: psycopg[binary]>=3.1
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# GovHub Data Lakehouse

Data Lakehouse format-agnostic e infra-agnostic: a mesma camada de
orquestração funciona independente de object storage, catálogo de
metadados, engine de query e formato de tabela por baixo (Iceberg, Delta
Lake, Hudi).

- [`docs/REQUIREMENTS.md`](docs/REQUIREMENTS.md) — visão, requisitos e arquitetura
- [`docs/USER_STORIES.md`](docs/USER_STORIES.md) — histórias de usuário e ordem de execução

## Stack

- **Core**: Python, gerenciado com [`uv`](https://github.com/astral-sh/uv)
- **Infra como código**: Terraform (módulos por ambiente: `local`, `aws`, ...)
- **Versões de ferramentas**: [`mise`](https://mise.jdx.dev) (`.mise.toml`)

## Subindo o ambiente local

```sh
mise trust
mise install
cp .env.example .env   # ajuste se necessário — os defaults já batem com a infra local

cd infra/environments/local
terraform init
terraform apply
```

`terraform apply` só retorna quando **todos** os serviços respondem como
saudáveis (ver `null_resource.wait_for_services` em
`infra/environments/local/main.tf`) — não só quando os containers existem.
Evita testes/comandos rodando contra um serviço (ex: Trino) que ainda não
terminou de subir.

Sobe, via Docker:

| Serviço              | Endpoint                    | Login                                  |
|-----------------------|------------------------------|------------------------------------------|
| MinIO API (S3)         | http://localhost:9003        | `lakehouse` / `lakehouse-dev`            |
| MinIO Console          | http://localhost:9002        | `lakehouse` / `lakehouse-dev`            |
| Iceberg REST Catalog   | http://localhost:8181        | —                                         |
| Airflow (standalone)   | http://localhost:8082        | `admin` / ver comando abaixo              |
| Trino                  | http://localhost:8083        | —                                         |

Senha do Airflow (gerada na primeira subida, persiste no volume):

```sh
docker exec govhub-lakehouse-airflow cat /opt/airflow/simple_auth_manager_passwords.json.generated
```

Bucket `warehouse` é criado automaticamente no MinIO. Catálogo Iceberg
persiste metadados em Postgres (porta `5434`).

Para derrubar: `terraform destroy` dentro de `infra/environments/local`.

## Configuração (`.env`)

`.env.example` documenta todas as variáveis `GOVHUB_*` (storage, catálogo,
extractors) com os defaults que já batem com a infra local, mais
`GOVHUB_ENV` (`dev` \| `homolog` \| `prod` — qual ambiente este checkout
está apontando agora; aparece no log de auditoria da CLI). Copie pra `.env`
(gitignorado) e ajuste — `mise` carrega automaticamente em todo comando
rodado via `mise exec` (`.mise.toml`), sem depender de nenhuma lib Python.

## Rodando o core Python

```sh
uv sync
uv run pytest tests/
```

Testes contra backends reais (`s3` em `tests/storage/`, `iceberg_rest` em
`tests/catalog/` e `tests/table/`, DuckDB + Trino em `tests/engines/`)
precisam do ambiente local do Terraform no ar — caso contrário são
pulados automaticamente.

### Storage backend

Backend escolhido via `GOVHUB_STORAGE_BACKEND` (`local` ou `s3`). Os demais
nomes são genéricos de propósito — os mesmos valerão para os próximos
backends (Azure, GCS):

| Variável                     | Uso                                   |
|-------------------------------|----------------------------------------|
| `GOVHUB_STORAGE_BACKEND`      | `local` \| `s3`                        |
| `GOVHUB_STORAGE_LOCAL_ROOT`   | raiz no filesystem (backend `local`)   |
| `GOVHUB_STORAGE_CONTAINER`    | bucket/container                       |
| `GOVHUB_STORAGE_ENDPOINT`     | endpoint do serviço                    |
| `GOVHUB_STORAGE_ACCESS_KEY`   | credencial de acesso                   |
| `GOVHUB_STORAGE_SECRET_KEY`   | credencial secreta                     |
| `GOVHUB_STORAGE_REGION`       | região (quando aplicável)              |

### Catalog backend

Backend escolhido via `GOVHUB_CATALOG_BACKEND` (`iceberg_rest` por
enquanto):

| Variável                     | Uso                                   |
|-------------------------------|----------------------------------------|
| `GOVHUB_CATALOG_BACKEND`      | `iceberg_rest`                         |
| `GOVHUB_CATALOG_URI`          | endpoint do catálogo REST              |
| `GOVHUB_CATALOG_WAREHOUSE`    | localização do warehouse (`s3://...`)  |
| `GOVHUB_CATALOG_ENDPOINT`     | endpoint do object storage (S3 FileIO) |
| `GOVHUB_CATALOG_ACCESS_KEY`   | credencial de acesso                   |
| `GOVHUB_CATALOG_SECRET_KEY`   | credencial secreta                     |
| `GOVHUB_CATALOG_REGION`       | região (quando aplicável)              |

### Table format backend

Sem variáveis próprias — reaproveita a config do catálogo (`GOVHUB_CATALOG_*`
acima), já que a camada de formato sempre opera sobre uma conexão de
catálogo existente. Seleção do formato é por código (`TableBackendFactory`),
`iceberg` por enquanto.

### Extractors e transformers

`src/govhub_lakehouse/extractors/` (Strategy + Factory, mesmo padrão de
`core/`) — `ApiExtractor`, `PostgresExtractor`, `S3Extractor`, registrados
em `ExtractorFactory`. `src/govhub_lakehouse/transformers/` (Template
Method) — `IcebergTransformer` cuida do passo específico de formato
(ajustar o schema solto do extractor para exatamente o que o Iceberg
espera) dentro de um algoritmo fixo (`transform`) compartilhado por
qualquer formato futuro.

### DAGs (`dags/`)

Ingestão organizada por domínio de dados (estilo data mesh, mesma lógica
do govhub-cidades) — cada domínio tem sua própria pasta/DAG(s) sob
`dags/<domínio>/`, montada em `/opt/airflow/dags`. Exemplo real:
`dags/ibge/estados_dag.py` — domínio `ibge`, `extract >> transform` via
TaskFlow API, puxando a lista de estados da API pública do IBGE e
carregando em `ibge.estados` (Iceberg).

```sh
docker exec govhub-lakehouse-airflow airflow dags unpause ibge_estados
docker exec govhub-lakehouse-airflow airflow dags trigger ibge_estados
docker exec govhub-lakehouse-airflow airflow dags list-runs ibge_estados
```

A imagem do Airflow é buildada com o próprio pacote instalado
(`infra/modules/orchestration/local/docker/airflow/Dockerfile`), então
qualquer DAG pode importar `govhub_lakehouse.extractors`/`.transformers`/
`.core.*` normalmente.

### Lint

```sh
uv run black --check src tests dags
uv run isort --check src tests dags
```

## CLI (`glh`)

```sh
uv run glh init
uv run glh create-table --namespace ns --table people --column id:long:required --column name:string
uv run glh ingest --namespace ns --table people --file dados.parquet [--mode append|overwrite]
uv run glh list-tables --namespace ns
uv run glh describe-table --namespace ns --table people
```

Cada comando lê a config de `GOVHUB_STORAGE_*`/`GOVHUB_CATALOG_*` (acima) e
emite um log estruturado em JSON por operação em stderr — quem rodou
(usuário do SO), quando, quais argumentos, sucesso/erro e duração. O
resultado da operação vai pro stdout, também em JSON.

## Build e deploy

Pra implantar o `glh` na infra de um órgão, sem precisar de Python/uv
instalado lá:

```sh
uv build                                    # wheel + sdist em dist/
docker build -t govhub-lakehouse-cli .      # imagem standalone do CLI
docker run --rm govhub-lakehouse-cli init   # roda contra GOVHUB_STORAGE_*/GOVHUB_CATALOG_* passadas via -e
```

A imagem não inclui nossa infra local de dev — só o `glh` e suas
dependências. O órgão aponta as variáveis de ambiente pro storage/catálogo
deles.

Também publicamos duas imagens no GHCR a cada push na `main` (CD, ver
abaixo):

```sh
docker pull ghcr.io/bottinolucas/govhub-lakehouse-cli:latest
docker pull ghcr.io/bottinolucas/govhub-lakehouse-airflow:latest
```

A `-airflow` já vem com `govhub_lakehouse` instalado — é a mesma imagem
que `infra/modules/orchestration/local` builda localmente, só que pronta
pra puxar em vez de buildar.

## CI/CD

`.github/workflows/ci.yml` roda em todo push/PR na `main`:

- **lint**: `black` + `isort` (`src`, `tests`, `dags`)
- **terraform**: `fmt -check` + `validate`
- **test**: sobe a infra local via Terraform (só retorna com tudo
  saudável — ver `null_resource.wait_for_services`), roda `pytest`,
  derruba a infra no final, mesmo se os testes falharem
- **build**: `uv build` + build das duas imagens Docker (CLI e Airflow)
- **publish** (CD — só em push na `main`, nunca em PR, só depois que os
  outros quatro jobs passarem): publica as duas imagens no GHCR
  (`ghcr.io/bottinolucas/govhub-lakehouse-{cli,airflow}`), usando o
  `GITHUB_TOKEN` nativo — nenhum secret extra a configurar.
- **publish-pypi** (CD — só em tag `v*`, ex: `v0.1.0`, nunca em push
  simples): publica o wheel/sdist no PyPI via *Trusted Publishing* (OIDC —
  sem token pra guardar). Precisa de um passo manual único, feito uma vez
  no site do PyPI, **antes do primeiro release**:

  1. Acesse https://pypi.org/manage/account/publishing/ (logado na conta
     que vai ser dona do pacote)
  2. Em "Add a new pending publisher", preencha:
     - **PyPI project name**: `govhub-lakehouse` (ou o nome que preferir —
       precisa bater com o `name` em `pyproject.toml`)
     - **Owner**: `bottinolucas`
     - **Repository name**: `govhub-data-lakehouse`
     - **Workflow name**: `ci.yml`
     - **Environment name**: `pypi`
  3. Pra liberar: `git tag v0.1.0 && git push origin v0.1.0` (a versão da
     tag deve bater com `version` em `pyproject.toml` — PyPI rejeita
     reenviar uma versão já publicada).
