feat(bloco-a): higiene alembic heads e inventario dos 9 datasets no registry

This commit is contained in:
gti
2026-09-28 16:12:21 -03:00
parent 2ff79e3b0e
commit cd44ae637b
3 changed files with 298 additions and 3 deletions
+185
View File
@@ -0,0 +1,185 @@
"""Missão 6 / Bloco A.2: Inventário dos 9 datasets existentes no Hub e migração para o Reference Data Registry.
Datasets catalogados:
1. NCM (Siscomex)
2. TIPI_IPI (Receita Federal)
3. CEST (CONFAZ)
4. BA_ST_MVA (SEFAZ-BA)
5. NFE_MOC_TABLES (Receita Federal / ENCAT)
6. CFOP (Portal Nacional NF-e)
7. FCP (Portal Nacional NF-e)
8. ANP (Portal Nacional NF-e)
9. NCM_UTRIB (Portal Nacional NF-e)
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from sqlalchemy import select, update
from sqlalchemy.orm import Session
from ..models import Dataset
@dataclass(frozen=True)
class DatasetInventoryItem:
kind: str
name: str
provider: str
source_url: str
version_label: str
expected_rows_approx: int
act: str
validity_info: str
coverage: str
licence_terms: str
raw_file_name: Optional[str] = None
DATASET_INVENTORY_9: tuple[DatasetInventoryItem, ...] = (
DatasetInventoryItem(
kind="NCM",
name="Nomenclatura Comum do Mercosul (NCM)",
provider="Siscomex",
source_url="https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json",
version_label="2026-09-28",
expected_rows_approx=15156,
act="Resolução Gecex / MDIC",
validity_info="Vigente em 2026-09-28",
coverage="Códigos NCM de 8 dígitos, descrições oficiais, datas de início e fim de vigência nacional.",
licence_terms="Dado público oficial (Governo Federal / Siscomex); sem termos de uso restritivos conhecidos.",
raw_file_name="ncm_vigente_20260928.json",
),
DatasetInventoryItem(
kind="TIPI_IPI",
name="Tabela de Incidência do IPI (TIPI)",
provider="Receita Federal",
source_url="https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/tributos-e-receitas/tipi",
version_label="tipi-xlsx-v1",
expected_rows_approx=11028,
act="Decreto nº 11.158/2022 e alterações até ADE 001-2026",
validity_info="Vigente desde 2022 com atualizações até 2026",
coverage="Alíquotas de IPI (percentuais ou NT) por NCM de 8 dígitos e código de exceção EX.",
licence_terms="Dado público oficial (Receita Federal do Brasil); dados abertos governamentais.",
raw_file_name="1 TIPI 2022 - Atualizada ADE 001-2026.xlsx",
),
DatasetInventoryItem(
kind="CEST",
name="Código Especificador da Substituição Tributária (CEST)",
provider="CONFAZ",
source_url="https://www.confaz.fazenda.gov.br/legislacao/convenios/2018/CV142_18",
version_label="CV142_18",
expected_rows_approx=1096,
act="Convênio ICMS 142/18",
validity_info="Vigente com modificações sucessivas",
coverage="Relação de CEST (7 dígitos), segmentos (Anexos II a XXVI) e códigos NCM aplicáveis à ST.",
licence_terms="Dado público oficial (Conselho Nacional de Política Fazendária - CONFAZ).",
raw_file_name="CV142_18.html",
),
DatasetInventoryItem(
kind="BA_ST_MVA",
name="Pauta Fiscal e Margens de Valor Agregado ST da Bahia",
provider="SEFAZ-BA",
source_url="http://www.sefaz.ba.gov.br/legislacao/ricms/anexo1.pdf",
version_label="ba-st-mva-pdf-v1",
expected_rows_approx=757,
act="RICMS/BA Anexo 1 (Decreto nº 13.780/2012)",
validity_info="Vigente em 2026 na Bahia",
coverage="Produtos sujeitos a ICMS-ST no Estado da Bahia, NCM/SH, CEST e percentuais de MVA original/ajustada.",
licence_terms="Legislação tributária pública estadual (SEFAZ-BA).",
raw_file_name="decreto_2012_13780_ricms_anexo_1_vigente_2026.pdf",
),
DatasetInventoryItem(
kind="NFE_MOC_TABLES",
name="Tabelas de Domínios do MOC da NF-e / NFC-e",
provider="Receita Federal / ENCAT",
source_url="http://www.nfe.fazenda.gov.br/portal/principal.aspx",
version_label="moc7-tables-v1",
expected_rows_approx=104,
act="MOC 7.0 Anexo I (Visão Geral de Leiaute e Regras de Validação)",
validity_info="Vigente nacionalmente",
coverage="Domínios oficiais de ORIGEM_MERCADORIA (9), CST_ICMS (14), CSOSN (10), CST_PIS (36) e CST_COFINS (36).",
licence_terms="Documento técnico público oficial (ENCAT / Portal Nacional NF-e).",
raw_file_name="moc7-anexo-i-leiaute-e-rv.pdf",
),
DatasetInventoryItem(
kind="CFOP",
name="Tabela de Códigos Fiscais de Operações e Prestações (CFOP)",
provider="Portal Nacional NF-e",
source_url="https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=Iy/5QolvUsQ=",
version_label="cfop-xlsx-v1",
expected_rows_approx=593,
act="Informe Técnico 2023.002 v2.10",
validity_info="Vigente desde 2023",
coverage="CFOP de 4 dígitos, vigências, indicadores operacionais (devolução, anulação, remessa, combustível) e descrições.",
licence_terms="Dado público oficial (Portal Nacional da NF-e).",
raw_file_name="IT_2023_002_v2_10_Tabela_CFOP_Descrição.xlsx",
),
DatasetInventoryItem(
kind="FCP",
name="Tabela de Alíquotas de FCP por UF",
provider="Portal Nacional NF-e",
source_url="https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=Iy/5QolvUsQ=",
version_label="fcp-xlsx-v1",
expected_rows_approx=27,
act="Tabela de Alíquotas FCP por UF",
validity_info="Vigente (fevereiro/2025)",
coverage="Alíquotas adicionais do Fundo de Combate à Pobreza para os 26 estados e Distrito Federal.",
licence_terms="Dado público oficial (Portal Nacional da NF-e).",
raw_file_name="Tabela_Alíquotas_FCP_por_UF_2025-02_07.xlsx",
),
DatasetInventoryItem(
kind="ANP",
name="Tabela de Códigos de Produto ANP",
provider="Portal Nacional NF-e",
source_url="https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=Iy/5QolvUsQ=",
version_label="anp-xlsx-v1",
expected_rows_approx=184,
act="Tabela de Código de Produto ANP",
validity_info="Vigente (novembro/2024)",
coverage="Códigos cProdANP e descrições oficiais de combustíveis e derivados de petróleo regulados pela ANP.",
licence_terms="Dado público oficial (ANP / Portal Nacional da NF-e).",
raw_file_name="Tabela_Codigo_Produto_ANP-20241118.xlsx",
),
DatasetInventoryItem(
kind="NCM_UTRIB",
name="Tabela de Correlação NCM x Unidade Tributável",
provider="Portal Nacional NF-e",
source_url="https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=Iy/5QolvUsQ=",
version_label="ncm-utrib-xlsx-v1",
expected_rows_approx=10374,
act="Informe Técnico 2024.001 - NCM x uTrib",
validity_info="Vigente a partir de 01.10.2026 / 2024",
coverage="Mapeamento de cada NCM de 8 dígitos para a respectiva Unidade Tributável (uTrib) padrão nacional.",
licence_terms="Dado público oficial (Portal Nacional da NF-e).",
raw_file_name="IT2024.001v2.40 - Tabela NCM 2022 com Utrib_Comércio Exterior_vigência 01.10.26.xlsx",
),
)
def sync_inventory_to_registry(session: Session) -> dict[str, int]:
"""Sincroniza os datasets existentes com as metainformações do Reference Data Registry.
Garante que os datasets já ativos no Hub (ACTIVE ou VIGENTE) possuam:
- licence_terms preenchidos
- act oficial correto
- raw_file_path preenchido quando disponível
- visibilidade no Registry sem criar segunda fonte ou duplicar linhas.
"""
inventory_map = {item.kind: item for item in DATASET_INVENTORY_9}
updated = 0
active_datasets = session.scalars(select(Dataset).where(Dataset.status.in_(("ACTIVE", "VIGENTE")))).all()
for ds in active_datasets:
inv = inventory_map.get(ds.kind)
if inv:
changed = False
if not ds.licence_terms:
ds.licence_terms = inv.licence_terms
changed = True
if not ds.act:
ds.act = inv.act
changed = True
if inv.raw_file_name and not ds.raw_file_path:
ds.raw_file_path = f"sources/raw/bootstrap_v1/{inv.raw_file_name}"
changed = True
if changed:
updated += 1
session.commit()
return {"active_found": len(active_datasets), "updated": updated}
+4 -3
View File
@@ -12,7 +12,8 @@ from sqlalchemy import select, update
from .models import Dataset
STATUSES = ("DRAFT", "APPROVED", "VIGENTE", "OBSOLETO", "REJECTED")
STATUSES = ("DRAFT", "APPROVED", "VIGENTE", "ACTIVE", "OBSOLETO", "SUPERSEDED", "REJECTED")
VIGENTE_STATUSES = ("VIGENTE", "ACTIVE")
class RegistryError(Exception):
@@ -43,7 +44,7 @@ def diff_against_vigente(session, dataset: Dataset) -> dict:
"""A summary an ADMIN reviews before approving: what the current VIGENTE of the same kind looks
like vs this DRAFT - never the row-by-row content (that lives in the kind-specific entry tables),
just the registry-level facts that matter for an approval decision."""
current = session.scalar(select(Dataset).where(Dataset.kind == dataset.kind, Dataset.status == "VIGENTE"))
current = session.scalar(select(Dataset).where(Dataset.kind == dataset.kind, Dataset.status.in_(VIGENTE_STATUSES)).order_by(Dataset.id.desc()))
return {
"kind": dataset.kind,
"draft": {"id": dataset.id, "version_label": dataset.version_label, "sha256": dataset.sha256, "record_count": dataset.record_count,
@@ -67,7 +68,7 @@ def approve_draft(session, dataset_id: int, *, admin_name: str) -> Dataset:
raise RegistryError(f"only a DRAFT dataset can be approved (current status: {dataset.status})")
if dataset.parse_errors:
raise RegistryError("dataset has recorded parse errors - re-submit a clean version")
session.execute(update(Dataset).where(Dataset.kind == dataset.kind, Dataset.status == "VIGENTE").values(status="OBSOLETO"))
session.execute(update(Dataset).where(Dataset.kind == dataset.kind, Dataset.status.in_(VIGENTE_STATUSES)).values(status="OBSOLETO"))
dataset.status = "VIGENTE"
dataset.approved_by = admin_name
dataset.approved_at = datetime.now(timezone.utc)
+109
View File
@@ -0,0 +1,109 @@
"""Missão 6 — Bloco A: Testes de Higiene (Migrations e Inventário dos 9 Datasets)."""
import subprocess
from pathlib import Path
from alembic.config import Config
from alembic.script import ScriptDirectory
ROOT = Path(__file__).resolve().parents[2]
def test_alembic_master_head_is_single():
"""Garante que na branch master o alembic possui uma única head: 0034_reference_data_registry."""
cfg = Config(str(ROOT / "central" / "alembic.ini"))
script = ScriptDirectory.from_config(cfg)
heads = script.get_heads()
assert heads == ["0034_reference_data_registry"], f"Esperado head única 0034, obtido: {heads}"
def test_feature_branch_migration_chains_to_0034():
"""Garante que na branch feature/cfop-ippt-write a migração 0035 encadeia exatamente em 0034."""
# Obter conteúdo de 0035 na branch feature/cfop-ippt-write via git show
result = subprocess.run(
["git", "show", "feature/cfop-ippt-write:central/alembic/versions/0035_cfop_ippt_write_mode.py"],
capture_output=True,
text=True,
cwd=str(ROOT),
)
assert result.returncode == 0, f"Arquivo 0035_cfop_ippt_write_mode.py deve existir na branch feature/cfop-ippt-write: {result.stderr}"
content = result.stdout
assert 'revision = "0035_cfop_ippt_write_mode"' in content
assert 'down_revision = "0034_reference_data_registry"' in content
# Verifica que 0033 não existe mais na branch
res_old = subprocess.run(
["git", "show", "feature/cfop-ippt-write:central/alembic/versions/0033_cfop_ippt_write_mode.py"],
capture_output=True,
text=True,
cwd=str(ROOT),
)
assert res_old.returncode != 0, "Migração 0033 não deve mais existir na branch feature"
def test_inventory_9_datasets_coverage():
"""Garante que os 9 datasets existentes no Hub estão integralmente catalogados."""
from central.app.datasets.inventory import DATASET_INVENTORY_9
kinds = [item.kind for item in DATASET_INVENTORY_9]
expected_kinds = ["NCM", "TIPI_IPI", "CEST", "BA_ST_MVA", "NFE_MOC_TABLES", "CFOP", "FCP", "ANP", "NCM_UTRIB"]
assert kinds == expected_kinds
for item in DATASET_INVENTORY_9:
assert item.name
assert item.provider
assert item.source_url.startswith("http")
assert item.version_label
assert item.expected_rows_approx > 0
assert item.act
assert item.validity_info
assert item.coverage
assert item.licence_terms
def test_sync_inventory_to_registry_and_listing():
"""Garante que datasets ACTIVE são reconhecidos pelo Reference Data Registry."""
from central.app.datasets.inventory import sync_inventory_to_registry
from central.app.reference_registry import list_registry, diff_against_vigente
from central.app.models import Dataset
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.models import Base
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
# Cadastrar dataset com status antigo ACTIVE (sem licence_terms)
ds = Dataset(
kind="NCM", provider="Siscomex", sha256="abc123hash",
version_label="2026-09-28", act="", source_url="https://portalunico.siscomex.gov.br",
status="ACTIVE", record_count=15156,
)
session.add(ds)
session.commit()
# Antes do sync, campos novos eram vazios
assert ds.licence_terms is None
# Executar sincronização
stats = sync_inventory_to_registry(session)
assert stats["active_found"] == 1
assert stats["updated"] == 1
assert ds.licence_terms is not None
assert ds.act == "Resolução Gecex / MDIC"
# Listagem do registry deve listar datasets ACTIVE
listed = list_registry(session)
assert len(listed) == 1
assert listed[0].kind == "NCM"
# Diff contra DRAFT deve encontrar o ACTIVE como vigente
draft = Dataset(
kind="NCM", provider="Siscomex", sha256="diff123hash",
version_label="2026-10-01", act="Nova Resolução", source_url="url",
status="DRAFT", record_count=15200,
)
session.add(draft)
session.commit()
diff = diff_against_vigente(session, draft)
assert diff["current_vigente"] is not None
assert diff["current_vigente"]["sha256"] == "abc123hash"
assert diff["record_count_delta"] == 15200 - 15156