Pular para conteúdo

Progresso e logging

Parte da norma Padronização de CLI para Fetchers. Os números de seção (§) são os da norma completa.

5. Padrão de callbacks para progresso em operações bulk

Quando a lógica de negócio (em bulk.py ou equivalente) executa N iterações e o plugin precisa mostrar progresso, use callbacks em vez de barras de progresso dentro da lógica de negócio.

5.1 Por que callbacks?

  • A lógica de negócio não sabe se está sendo chamada por um CLI interativo, um script, um teste, ou outro contexto.
  • Callbacks mantêm a lógica de negócio independente de Rich/tqdm.
  • Permitem que o plugin controle 100% da apresentação.

5.2 Assinaturas padrão

O ecossistema usa três famílias de callback, escolhidas pelo tipo de operação:

Família Assinatura Quando usar Referência
Bulk por item on_progress(processed, total, ok, failed, skipped) N itens homogêneos com contadores ok/falha/skip bcb-sgs-fetcher bulk.py
Por grupo/página on_grupo(nome, done, total) / on_page(page, n_pages) Operações com total descoberto ao vivo bcb-sgs-fetcher bulk.py
Por bytes de arquivo ProgressCallback = (downloaded, total_bytes) Download de arquivo único; integra com download_with_manifest(progress=cb) comex-fetcher plugin.py
Pós-arquivo on_done(filename, result) com result ∈ {"ok","skipped","failed"} N downloads paralelos onde cada arquivo reporta seu resultado ao terminar rtn-fetcher plugin.py
from collections.abc import Callable
from quantilica.core.http import ProgressCallback  # Callable[[int, int], None]

# Bulk por item (N itens com total conhecido)
on_progress: Callable[[int, int, int, int, int], None] | None = None
# args: (processed, total, ok, failed, skipped)

# Por grupo (com total descoberto ao vivo)
on_grupo: Callable[[str, int, int], None] | None = None
# args: (nome_grupo, done, total)

# Por página
on_page: Callable[[int, int], None] | None = None
# args: (page, n_pages)

# Por bytes de arquivo (integra com HttpClient.download_with_manifest)
progress: ProgressCallback | None = None
# args: (downloaded_bytes, total_bytes); total_bytes=0 quando Content-Length ausente

# Pós-arquivo
on_done: Callable[[str, str], None] | None = None
# args: (filename, result) onde result ∈ {"ok", "skipped", "failed"}

5.3 Implementação na lógica de negócio

def fetch_metadata_bulk(
    series_ids: list[int],
    scraper: ScraperClient,
    dest_dir: Path,
    sleeptime: float = 10,
    skip_existing: bool = False,
    on_progress: (
        Callable[[int, int, int, int, int], None] | None
    ) = None,
) -> tuple[int, int]:
    total = len(series_ids)
    successful = failed = skipped = processed = 0

    for series_id in sorted(series_ids):
        if skip_existing and (dest_dir / f"{series_id:06d}.json").exists():
            skipped += 1
            processed += 1
            if on_progress is not None:
                on_progress(processed, total, successful, failed, skipped)
            continue

        # ... lógica de fetch ...

        processed += 1
        if on_progress is not None:
            on_progress(processed, total, successful, failed, skipped)

    return successful, failed

Regras:

  • Sempre guarde o callback em parâmetro com valor padrão None — a função deve funcionar sem callback.
  • Chame o callback após atualizar os contadores, nunca antes.
  • Chame o callback para todos os itens, incluindo pulados (skipped) — assim a barra de progresso avança corretamente.
  • Não ponha lógica de display (print, tqdm, Rich) na lógica de negócio.

5.4 Consumo no plugin.py

with _make_progress() as progress:
    task = progress.add_task("[cyan]0✓  0✗  0 skip[/cyan]", total=len(ids))

    def on_progress(
        processed: int,
        total: int,
        ok: int,
        failed: int,
        skipped: int,
    ) -> None:
        progress.update(
            task,
            completed=processed,
            description=(
                f"[green]{ok}✓[/green]"
                f"  [red]{failed}✗[/red]"
                f"  [dim]{skipped} skip[/dim]"
            ),
        )

    successful, failed_count = bulk.fetch_metadata_bulk(
        ids, scraper, dest_dir,
        sleeptime=sleeptime,
        skip_existing=skip_existing,
        on_progress=on_progress,
    )

6. Logging na lógica de negócio

6.1 Níveis permitidos por contexto

Nível Quando usar
DEBUG Logs de rastreamento por item — "Fetching series 1234", "Skipping file X"
INFO Marcos importantes de progresso com contexto amplo
WARNING Situações anômalas mas recuperáveis — parse failure, ID mismatch
ERROR Falhas que afetam um item mas não encerram o processamento
CRITICAL Apenas para falhas que tornam impossível continuar

6.2 O que demover para DEBUG

Logs que antes apareciam em INFO mas que constituem "barulho" em execuções normais devem ser demovidos para DEBUG. Exemplos reais do bcb-sgs-fetcher:

# ❌ Antes — INFO emite uma linha por série, quebrando a barra de progresso
logger.info("Fetching metadata for series %d", series_id)

# ✅ Depois — DEBUG só aparece com --verbose
logger.debug("Fetching metadata for series %d", series_id)

Critério prático: se o log aparece N vezes dentro de um loop de N iterações, provavelmente deve ser DEBUG.

6.3 O que manter em INFO ou superior

  • Marcos do início/fim de fases em pipelines de vários passos.
  • Warnings de parsing — arquivos com dados inesperados.
  • Session errors e retries.
  • Mensagens de erro que identificam qual item falhou.
# Manter em WARNING — o usuário precisa saber
logger.warning(
    "Series ID mismatch for %d (got %d), removing files",
    series_id,
    basic.series_id,
)

# Manter em ERROR — falha de sessão é relevante mesmo sem --verbose
logger.error("Session error for series %d: %s", series_id, exc)