Files
rodado 23b56e8c87 feat: descrição arquivística NOBRADE multinível com exportação EAD 3, EAC-CPF e AtoM
- pipeline/nobrade: normalizadores de datas (ISO 8601-2), séries, autoridades,
  espécie/suporte, direitos, lugar e assuntos; montagem dos níveis 0–5 com
  código de referência BR RJXYZ.BF.<seção>.<série>.<handle>.<imagem>
- validador dos elementos obrigatórios: 0 pendências em 26.454 unidades
- EAD 3 e 457 EAC-CPF validados contra os XSD
- mural: cartão com código, data normalizada, série e reprodução; filtro por série
2026-09-30 21:22:00 +02:00

86 lines
3.9 KiB
Python

"""Validador dos elementos obrigatórios da NOBRADE, por nível.
Obrigatórios: 1.1, 1.2, 1.3 (data de produção), 1.4, 1.5, 2.1; 4.1 só nos
níveis 0 e 1. Além deles, lista avisos que não reprovam mas pedem revisão.
Escreve descricao/pendencias.md e devolve o número de pendências.
"""
from __future__ import annotations
from collections import Counter, defaultdict
from .config import SAIDA
def verifica(u: dict) -> tuple[list[str], list[str]]:
erros, avisos = [], []
n = u['nivel']
if not u.get('1.1'):
erros.append('1.1 código de referência ausente')
if not (u.get('1.2') or {}).get('titulo', '').strip():
erros.append('1.2 título ausente')
prod = (u.get('1.3') or {}).get('producao') or {}
if not prod.get('inicio') and not prod.get('fim'):
erros.append('1.3 data de produção ausente')
if not (u.get('1.4') or {}).get('nome'):
erros.append('1.4 nível ausente')
if not (u.get('1.5') or {}).get('texto'):
erros.append('1.5 dimensão e suporte ausente')
if not u.get('2.1'):
erros.append('2.1 produtor ausente')
if n <= 1 and not u.get('4.1'):
erros.append('4.1 condições de acesso ausente (obrigatório nos níveis 0 e 1)')
if n == 4:
if prod.get('qualidade') == 'atribuída pelo arquivista' or (prod.get('notas') or [''])[0].startswith('data atribuída'):
avisos.append('1.3 data atribuída pelo arquivista — conferir')
if prod.get('motivo') == 'ilegível':
avisos.append('1.3 data da origem ilegível')
if all(p.get('id') is None for p in u.get('2.1') or []):
avisos.append('2.1 autoria não identificada')
if u['id'].split('.')[-2] == 'AV':
avisos.append('nível 3 em Avulsos — série de origem não identificada')
if any('presumida' in x for x in u.get('6.2') or []):
avisos.append('1.5 espécie presumida')
if any('corrompidos' in x for x in u.get('6.2') or []):
avisos.append('valores corrompidos na origem')
if u.get('6.1'):
avisos.append('6.1 preservação indisponível na origem')
if (u.get('4.2') or {}).get('categoria') == 'consultar o detentor':
avisos.append('4.2 condição de reprodução não informada na origem')
if not (u.get('3.1') or '').strip():
avisos.append('3.1 sem âmbito e conteúdo')
return erros, avisos
def valida(unidades) -> int:
erros, avisos = defaultdict(list), defaultdict(list)
por_nivel = Counter()
for u in unidades:
por_nivel[u['nivel']] += 1
e, a = verifica(u)
for x in e:
erros[x].append(u['id'])
for x in a:
avisos[x].append(u['id'])
total = sum(len(v) for v in erros.values())
linhas = ['# Pendências da descrição NOBRADE', '',
'Gerado por `python -m nobrade`. Obrigatórios reprovam; avisos pedem revisão.', '',
'| Nível | Unidades |', '|---|--:|']
nomes = {0: 'acervo', 1: 'coleção', 2: 'seção', 3: 'série', 4: 'dossiê', 5: 'item'}
linhas += [f'| {n} {nomes[n]} | {c:,} |'.replace(',', '.') for n, c in sorted(por_nivel.items())]
linhas += ['', f'## Obrigatórios faltando: {total}', '']
if not erros:
linhas.append('Nenhum. Todas as unidades têm os elementos obrigatórios do seu nível.')
for k, ids in sorted(erros.items(), key=lambda kv: -len(kv[1])):
linhas += [f'### {k} — {len(ids)}', '', *[f'- `{i}`' for i in ids[:50]]]
if len(ids) > 50:
linhas.append(f'- … e mais {len(ids) - 50}')
linhas.append('')
linhas += ['', '## Avisos', '', '| Aviso | Dossiês |', '|---|--:|']
linhas += [f'| {k} | {len(v):,} |'.replace(',', '.') for k, v in sorted(avisos.items(), key=lambda kv: -len(kv[1]))]
linhas.append('')
(SAIDA / 'pendencias.md').write_text('\n'.join(linhas), encoding='utf-8')
return total