quantilica-cli 0.3.2__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantilica_cli-0.8.0/.githooks/pre-push +30 -0
- quantilica_cli-0.8.0/.github/workflows/test.yml +55 -0
- quantilica_cli-0.8.0/CHANGELOG.md +105 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/PKG-INFO +2 -2
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/pyproject.toml +2 -2
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/src/quantilica/cli/cli.py +3 -1
- quantilica_cli-0.8.0/src/quantilica/cli/health.py +188 -0
- quantilica_cli-0.8.0/src/quantilica/cli/sdk.py +789 -0
- quantilica_cli-0.8.0/tests/test_health.py +208 -0
- quantilica_cli-0.8.0/tests/test_sdk.py +277 -0
- quantilica_cli-0.8.0/tests/test_sdk_onda_a2.py +295 -0
- quantilica_cli-0.3.2/.github/workflows/test.yml +0 -38
- quantilica_cli-0.3.2/CHANGELOG.md +0 -46
- quantilica_cli-0.3.2/src/quantilica/cli/sdk.py +0 -335
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/.githooks/pre-commit +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/.github/workflows/publish.yml +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/.gitignore +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/LICENSE +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/README.md +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/src/quantilica/cli/__init__.py +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/src/quantilica/cli/manifests.py +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/src/quantilica/cli/progress.py +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/src/quantilica/cli/sources.py +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/src/quantilica/cli/ui.py +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/src/quantilica/py.typed +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/tests/__init__.py +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/tests/test_manifests.py +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/tests/test_sources.py +0 -0
- {quantilica_cli-0.3.2 → quantilica_cli-0.8.0}/tests/test_ui.py +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Barra o push se a árvore inteira não passar em lint/format.
|
|
3
|
+
#
|
|
4
|
+
# Complementa o pre-commit (que só vê .py staged): pega dívida de lint
|
|
5
|
+
# pré-existente e commits que não tocam Python — foi assim que E501 dos
|
|
6
|
+
# sweeps de 2026-08-14 ficou vermelho no CI por semanas e um commit de
|
|
7
|
+
# pyproject re-ativou CI vermelho no inmet (2026-08-31).
|
|
8
|
+
#
|
|
9
|
+
# Instalação (uma vez por clone): git config core.hooksPath .githooks
|
|
10
|
+
# (o bootstrap.sh faz isso automaticamente).
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
14
|
+
|
|
15
|
+
[ -d src ] || exit 0
|
|
16
|
+
if [ -d tests ]; then
|
|
17
|
+
TARGETS="src/ tests/"
|
|
18
|
+
else
|
|
19
|
+
TARGETS="src/"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if ! uv run --no-sync ruff --version >/dev/null 2>&1; then
|
|
23
|
+
echo "pre-push: ruff indisponível no ambiente — rodando uv sync --group dev" >&2
|
|
24
|
+
uv sync --group dev
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
uv run --no-sync ruff check ${TARGETS}
|
|
28
|
+
uv run --no-sync ruff format --check ${TARGETS}
|
|
29
|
+
|
|
30
|
+
echo "pre-push: lint/format da árvore OK."
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.12", "3.13"]
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v5
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
28
|
+
run: uv python install ${{ matrix.python-version }}
|
|
29
|
+
|
|
30
|
+
# O índice Quantilica (GitHub Pages) já deu flake de DNS no runner
|
|
31
|
+
# (2026-08-23: rtn e inmet). Três tentativas removem o flake sem
|
|
32
|
+
# esconder erro real de resolução.
|
|
33
|
+
- name: Install dependencies
|
|
34
|
+
run: |
|
|
35
|
+
for i in 1 2 3; do
|
|
36
|
+
if uv sync --group dev --python ${{ matrix.python-version }} \
|
|
37
|
+
--index https://index.quantilica.com/simple/ \
|
|
38
|
+
--index-strategy unsafe-best-match; then
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
echo "::warning::uv sync falhou (tentativa $i/3); nova tentativa em 10s"
|
|
42
|
+
sleep 10
|
|
43
|
+
done
|
|
44
|
+
exit 1
|
|
45
|
+
|
|
46
|
+
# Cada `uv run` sem --no-sync re-resolve o lockfile virtual e REMOVE os
|
|
47
|
+
# extras instalados no passo de sync acima (polars/openpyxl já caíram
|
|
48
|
+
# assim: anp 2026-08-29). Todos os passos usam --no-sync.
|
|
49
|
+
- name: Lint with ruff
|
|
50
|
+
run: |
|
|
51
|
+
uv run --no-sync ruff check src/ tests/
|
|
52
|
+
uv run --no-sync ruff format --check src/ tests/
|
|
53
|
+
|
|
54
|
+
- name: Run tests
|
|
55
|
+
run: uv run --no-sync pytest
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
|
|
4
|
+
|
|
5
|
+
O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
|
6
|
+
e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|
7
|
+
|
|
8
|
+
## [0.8.0] - 2026-10-03
|
|
9
|
+
|
|
10
|
+
Onda A.2 do plano `2026-10-03-padronizacao-core-e-consolidacao-fetchers` —
|
|
11
|
+
decorators de ciclo de vida no SDK (`quantilica.cli.sdk`) e abstração de
|
|
12
|
+
pré-visualização tabular de sincronização.
|
|
13
|
+
|
|
14
|
+
### Adicionado
|
|
15
|
+
- `FetcherApp.command_convert(func)`: decorator que registra o subcomando
|
|
16
|
+
`convert` com flags canônicas (`-i/--input`, `-o/--output`, `--verbose`,
|
|
17
|
+
padrões derivados de `default_output`). Configura logging Rich, trata
|
|
18
|
+
`ImportError` graciosamente (sugerindo `pip install {nome}[analysis]`,
|
|
19
|
+
saída com código 1) e exibe confirmação com check verde.
|
|
20
|
+
- `FetcherApp.command_pipeline(func)`: decorator que registra o subcomando
|
|
21
|
+
`pipeline` encadeando sincronização (passo 1/2, via `sync`) e conversão
|
|
22
|
+
analítica (passo 2/2, via `func`). Opções: grupos, `--output`,
|
|
23
|
+
`--parquet-dir`, `--workers`, `--dry-run` (interrompe antes do passo 2) e
|
|
24
|
+
`--verbose`.
|
|
25
|
+
- `FetcherApp.command_archive(func)`: decorator que registra o subcomando
|
|
26
|
+
`archive` para arquivamento histórico, com o mesmo padrão de flags e
|
|
27
|
+
tratamento gracioso de `ImportError`.
|
|
28
|
+
- `SyncPlanItem`/`SyncPlan`: dataclasses (imutáveis) para planejamento de
|
|
29
|
+
sincronização, com `SyncPlan.render_table(console=None)` que renderiza uma
|
|
30
|
+
tabela Rich (`Dataset | Partição | Arquivo | URL`) e o sumário `Total: X
|
|
31
|
+
arquivos planejados. Y ignorados fora de cobertura.` Importados do
|
|
32
|
+
`quantilica.cli.sdk`.
|
|
33
|
+
- `quantilica health`: subcomando de diagnóstico que faz sondas HTTP leves
|
|
34
|
+
(HEAD com fallback para GET, timeout padrão 5s) contra as fontes canônicas
|
|
35
|
+
do ecossistema — BCB SGS, SIDRA, Tesouro Direto, Comex e INMET — em
|
|
36
|
+
paralelo, com saída em tabela Rich (`Fonte | Estado | Latência | HTTP`) ou
|
|
37
|
+
em JSON estruturado via `--json`. Usa `quantilica.core.http` para o probe.
|
|
38
|
+
|
|
39
|
+
## [0.7.0] - 2026-10-02
|
|
40
|
+
|
|
41
|
+
Onda 2 — extensões do SDK (`quantilica.cli.sdk`) para eliminação de
|
|
42
|
+
boilerplate nos fetchers (decisão
|
|
43
|
+
`2026-10-02-padronizacao-e-deduplicacao-fetchers`).
|
|
44
|
+
|
|
45
|
+
### Adicionado
|
|
46
|
+
- `DataRepository` canônico no SDK (baseado em `StampedDataRepository` do core)
|
|
47
|
+
com `path_for_entry(entry, last_modified=...)`, estabelecendo a convenção
|
|
48
|
+
única de layout: `{dataset_id}/{slug}[@{partition}]@{YYYYMMDD}.{ext}`.
|
|
49
|
+
- `default_path_builder(output_dir, entry, last_modified)`: path builder
|
|
50
|
+
canônico para fetchers que não fornecem um próprio.
|
|
51
|
+
- `FetcherApp.attach_command(cmd_func, name=None, **kwargs)`: registro limpo de
|
|
52
|
+
subcomandos customizados (`convert`, `pipeline`, `archive`) sem subclassificar
|
|
53
|
+
e sobrescrever `_build_commands`.
|
|
54
|
+
- `FetcherApp(build_default_commands=False)`: instancie o app sem os comandos
|
|
55
|
+
padrão `sync`/`list` (fim do padrão `def _build_commands(): pass`).
|
|
56
|
+
- `make_resolve_groups(groups_dict, aliases_dict)`: helper que constrói
|
|
57
|
+
resolver de grupos/aliases para comandos customizados (dedup, ordem
|
|
58
|
+
declarada, erro em grupos desconhecidos); o comando `sync` padrão agora o
|
|
59
|
+
usa.
|
|
60
|
+
- Commit `feat(sdk)`: `default_client()` já com `emulate_browser` e pooling
|
|
61
|
+
keep-alive por worker em `download_datasets` (anteriomente não documentado).
|
|
62
|
+
|
|
63
|
+
### Alterado
|
|
64
|
+
- `path_builder` no `FetcherApp` é agora opcional (default:
|
|
65
|
+
`default_path_builder`).
|
|
66
|
+
|
|
67
|
+
## [0.3.2] - 2026-08-22
|
|
68
|
+
|
|
69
|
+
### Corrigido
|
|
70
|
+
- **Crítico:** wheels publicados desde a 0.3.0 vinham **sem os módulos do pacote** — o diretório `quantilica/cli/` não era incluído no build por configuração incorreta do hatchling (`sources` na seção global e `packages` apontando para o subpacote). Instalações via pip/uv reportavam sucesso, mas `import quantilica.cli` falhava em qualquer ambiente não-editable. Configuração realinhada ao padrão dos pacotes irmãos (`packages = ["src/quantilica"]` dentro de `[tool.hatch.build.targets.wheel]`).
|
|
71
|
+
|
|
72
|
+
## [0.3.1] - 2026-08-22
|
|
73
|
+
|
|
74
|
+
### Corrigido
|
|
75
|
+
- `DEFAULT_INDEX_URL` apontado para `https://index.quantilica.com/simple/` — a URL anterior (`quantilica.com/quantilica-index/`) parou de ser servida quando o domínio passou ao portal, quebrando o `quantilica install` para fetchers fora do PyPI legado (detalhes no ADR de distribuição de 2026-08-22).
|
|
76
|
+
- `install`/`uninstall` agora mesclam o registro remoto (`sources.json`) com o registro local, resolvendo também nomes canônicos do índice (ex.: `tesouro-direto`, além de `td`).
|
|
77
|
+
|
|
78
|
+
## [0.3.0] - 2026-08-10
|
|
79
|
+
|
|
80
|
+
### Adicionado
|
|
81
|
+
- Extensão da `FetcherApp` (`sdk.py`) com suporte opcional para `FtpClient`.
|
|
82
|
+
- Exposição do método `download_datasets` na `FetcherApp` para uso por comandos Typer customizados nos plugins.
|
|
83
|
+
- Aceitação e passagem do parâmetro `aliases_dict` para personalização total dos subcomandos por fetcher.
|
|
84
|
+
|
|
85
|
+
### Alterado
|
|
86
|
+
- Padrão de metadados do pacote portado integralmente para a PEP 639 (licença) e PEP 561 (tipagem estática).
|
|
87
|
+
|
|
88
|
+
## [0.2.2] - 2026-07-28
|
|
89
|
+
*(Release retroativo não documentado)*
|
|
90
|
+
|
|
91
|
+
## [0.2.0] - 2026-07-15
|
|
92
|
+
*(Release retroativo não documentado)*
|
|
93
|
+
|
|
94
|
+
## [0.1.0] - 2026-06-04
|
|
95
|
+
|
|
96
|
+
Primeira entrada em formato Keep a Changelog; documenta o estado do pacote nesta
|
|
97
|
+
versão.
|
|
98
|
+
|
|
99
|
+
### Adicionado
|
|
100
|
+
|
|
101
|
+
- CLI unificada `quantilica` que descobre e monta os fetchers instalados via
|
|
102
|
+
entry points `quantilica.fetchers`, sem depender diretamente dos pacotes de
|
|
103
|
+
fetcher.
|
|
104
|
+
- Comando `list-sources` e montagem automática dos sub-apps Typer de cada fetcher
|
|
105
|
+
instalado (`quantilica <fonte> ...`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quantilica-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Unified CLI for Quantilica open data fetchers
|
|
5
5
|
Author-email: "Komesu, D.K." <daniel@dkko.me>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
|
-
Requires-Dist: quantilica-core>=0.
|
|
19
|
+
Requires-Dist: quantilica-core>=0.7.0
|
|
20
20
|
Requires-Dist: rich>=13.0.0
|
|
21
21
|
Requires-Dist: typer>=0.15.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quantilica-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Unified CLI for Quantilica open data fetchers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Komesu, D.K.", email = "daniel@dkko.me" }]
|
|
@@ -20,7 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.13",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"quantilica-core>=0.
|
|
23
|
+
"quantilica-core>=0.7.0",
|
|
24
24
|
"rich>=13.0.0",
|
|
25
25
|
"typer>=0.15.0",
|
|
26
26
|
]
|
|
@@ -13,6 +13,7 @@ from rich.logging import RichHandler
|
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
15
|
from quantilica.cli import __version__
|
|
16
|
+
from quantilica.cli.health import cmd_health
|
|
16
17
|
from quantilica.cli.manifests import app as manifests_app
|
|
17
18
|
from quantilica.cli.sources import (
|
|
18
19
|
app as sources_app,
|
|
@@ -35,10 +36,11 @@ app = typer.Typer(
|
|
|
35
36
|
app.add_typer(manifests_app, name="manifests")
|
|
36
37
|
app.add_typer(sources_app, name="sources")
|
|
37
38
|
|
|
38
|
-
# Adiciona comandos top-level install, uninstall e
|
|
39
|
+
# Adiciona comandos top-level install, uninstall, doctor e health
|
|
39
40
|
app.command("install")(cmd_install)
|
|
40
41
|
app.command("uninstall")(cmd_uninstall)
|
|
41
42
|
app.command("doctor")(cmd_doctor)
|
|
43
|
+
app.command("health")(cmd_health)
|
|
42
44
|
|
|
43
45
|
console = Console()
|
|
44
46
|
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Comando `quantilica health` — sondas de disponibilidade das fontes.
|
|
2
|
+
|
|
3
|
+
Realiza sondas HTTP leves (HEAD com fallback para GET) contra as fontes
|
|
4
|
+
canônicas de dados do ecossistema Quantilica e reporta o resultado em
|
|
5
|
+
tabela Rich ou JSON estruturado.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import concurrent.futures
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import httpx2
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
DEFAULT_TIMEOUT = 5.0
|
|
23
|
+
USER_AGENT = "quantilica-cli (health)"
|
|
24
|
+
|
|
25
|
+
# Lista canônica de fontes sondadas (nome, URL de sonda leve).
|
|
26
|
+
HEALTH_SOURCES: list[tuple[str, str]] = [
|
|
27
|
+
("bcb", "https://api.bcb.gov.br/dados/serie/bcdata.sgs.1/dados?formato=json"),
|
|
28
|
+
("sidra", "https://servicodados.ibge.gov.br/api/v3/calendario"),
|
|
29
|
+
(
|
|
30
|
+
"tesouro_direto",
|
|
31
|
+
"https://www.tesourotransparente.gov.br/ckan/api/3/action/package_search?rows=1",
|
|
32
|
+
),
|
|
33
|
+
("comex", "https://balanca.economia.gov.br"),
|
|
34
|
+
("inmet", "https://apitempo.inmet.gov.br/estacoes"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _make_client(timeout: float) -> httpx2.Client:
|
|
39
|
+
"""Cria o cliente HTTP usado pelas sondas.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
timeout: Timeout (segundos) aplicado a cada requisição.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Um httpx2.Client configurado com redirects e User-Agent da CLI.
|
|
46
|
+
"""
|
|
47
|
+
return httpx2.Client(
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
follow_redirects=True,
|
|
50
|
+
headers={"User-Agent": USER_AGENT},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _probe(client: httpx2.Client, name: str, url: str) -> dict[str, object]:
|
|
55
|
+
"""Executa uma sonda HEAD (com fallback GET) contra uma fonte.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
client: O cliente HTTP a ser usado.
|
|
59
|
+
name: Nome canônico da fonte.
|
|
60
|
+
url: URL alvo da sonda.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dicioário com name, url, status, http_status, latency_ms e error.
|
|
64
|
+
"""
|
|
65
|
+
start = time.perf_counter()
|
|
66
|
+
try:
|
|
67
|
+
response = client.head(url)
|
|
68
|
+
# Muitos endpoints não implementam HEAD — fallback para GET leve.
|
|
69
|
+
if response.status_code >= 400:
|
|
70
|
+
response = client.get(url)
|
|
71
|
+
latency_ms = (time.perf_counter() - start) * 1000.0
|
|
72
|
+
if response.status_code < 400:
|
|
73
|
+
return {
|
|
74
|
+
"name": name,
|
|
75
|
+
"url": url,
|
|
76
|
+
"status": "ok",
|
|
77
|
+
"http_status": response.status_code,
|
|
78
|
+
"latency_ms": round(latency_ms, 1),
|
|
79
|
+
"error": None,
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
"name": name,
|
|
83
|
+
"url": url,
|
|
84
|
+
"status": "falha",
|
|
85
|
+
"http_status": response.status_code,
|
|
86
|
+
"latency_ms": round(latency_ms, 1),
|
|
87
|
+
"error": f"HTTP {response.status_code}",
|
|
88
|
+
}
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
latency_ms = (time.perf_counter() - start) * 1000.0
|
|
91
|
+
return {
|
|
92
|
+
"name": name,
|
|
93
|
+
"url": url,
|
|
94
|
+
"status": "falha",
|
|
95
|
+
"http_status": None,
|
|
96
|
+
"latency_ms": round(latency_ms, 1),
|
|
97
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run_probes(timeout: float, fail_fast: bool) -> list[dict[str, object]]:
|
|
102
|
+
"""Sonda todas as fontes, em sequência (fail-fast) ou em paralelo.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
timeout: Timeout por requisição, em segundos.
|
|
106
|
+
fail_fast: Se True, interrompe as sondas na primeira falha.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
A lista de resultados por fonte.
|
|
110
|
+
"""
|
|
111
|
+
with _make_client(timeout=timeout) as client:
|
|
112
|
+
if fail_fast:
|
|
113
|
+
results = []
|
|
114
|
+
for name, url in HEALTH_SOURCES:
|
|
115
|
+
result = _probe(client, name, url)
|
|
116
|
+
results.append(result)
|
|
117
|
+
if result["status"] != "ok":
|
|
118
|
+
break
|
|
119
|
+
return results
|
|
120
|
+
|
|
121
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
122
|
+
max_workers=len(HEALTH_SOURCES)
|
|
123
|
+
) as pool:
|
|
124
|
+
futures = [
|
|
125
|
+
(name, pool.submit(_probe, client, name, url))
|
|
126
|
+
for name, url in HEALTH_SOURCES
|
|
127
|
+
]
|
|
128
|
+
return [future.result() for _, future in futures]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _render_table(results: list[dict[str, object]]) -> None:
|
|
132
|
+
"""Renderiza o relatório de health como tabela Rich.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
results: Resultados das sondas.
|
|
136
|
+
"""
|
|
137
|
+
table = Table(title="Health — fontes de dados", show_header=True)
|
|
138
|
+
table.add_column("Fonte", style="cyan")
|
|
139
|
+
table.add_column("Status")
|
|
140
|
+
table.add_column("Código", justify="right")
|
|
141
|
+
table.add_column("Latência (ms)", justify="right")
|
|
142
|
+
|
|
143
|
+
for r in results:
|
|
144
|
+
if r["status"] == "ok":
|
|
145
|
+
status = "[green]OK[/green]"
|
|
146
|
+
else:
|
|
147
|
+
status = "[red]FALHA[/red]"
|
|
148
|
+
code = str(r["http_status"]) if r["http_status"] is not None else "-"
|
|
149
|
+
latency = str(r["latency_ms"]) if r["latency_ms"] is not None else "-"
|
|
150
|
+
table.add_row(str(r["name"]), status, code, latency)
|
|
151
|
+
|
|
152
|
+
console.print(table)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cmd_health(
|
|
156
|
+
json_output: Annotated[
|
|
157
|
+
bool,
|
|
158
|
+
typer.Option(
|
|
159
|
+
"--json",
|
|
160
|
+
help="Saída em JSON estruturado em vez de tabela.",
|
|
161
|
+
),
|
|
162
|
+
] = False,
|
|
163
|
+
fail_fast: Annotated[
|
|
164
|
+
bool,
|
|
165
|
+
typer.Option(
|
|
166
|
+
"--fail-fast",
|
|
167
|
+
help="Interrompe as sondas na primeira falha e encerra com código 1.",
|
|
168
|
+
),
|
|
169
|
+
] = False,
|
|
170
|
+
timeout: Annotated[
|
|
171
|
+
float,
|
|
172
|
+
typer.Option(
|
|
173
|
+
"--timeout",
|
|
174
|
+
help="Timeout (s) aplicado a cada sonda HTTP.",
|
|
175
|
+
),
|
|
176
|
+
] = DEFAULT_TIMEOUT,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Verifica a disponibilidade das fontes de dados (sondas HTTP leves)."""
|
|
179
|
+
results = _run_probes(timeout, fail_fast)
|
|
180
|
+
|
|
181
|
+
if json_output:
|
|
182
|
+
status = "ok" if all(r["status"] == "ok" for r in results) else "degraded"
|
|
183
|
+
print(json.dumps({"status": status, "sources": results}))
|
|
184
|
+
else:
|
|
185
|
+
_render_table(results)
|
|
186
|
+
|
|
187
|
+
if fail_fast and any(r["status"] != "ok" for r in results):
|
|
188
|
+
raise typer.Exit(code=1)
|