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
This commit is contained in:
2026-09-30 21:22:00 +02:00
parent e92df83a78
commit 23b56e8c87
46 changed files with 19562 additions and 68 deletions
+487
View File
@@ -0,0 +1,487 @@
"""Monta a descrição multinível (níveis 0–5) a partir do catálogo, do ledger do
bucket e dos posts do blog. Escreve descricao/unidades.jsonl,
descricao/autoridades.jsonl e as tabelas de revisão.
Cada unidade é um objeto com `id` (= código de referência), `pai`, `nivel` e os
elementos da NOBRADE pelo número: "1.1", "1.2", … "8.1".
"""
from __future__ import annotations
import ast
import json
import re
import sqlite3
from collections import Counter, defaultdict
from datetime import date, datetime
import polars as pl
from . import campos, datas, series
from .autoridades import Autoridades
from .config import (ACESSO, BUCKET, CATALOGO, COLECAO, COLECAO_TITULO, ENTIDADE_NOME, HANDLE_URL,
INSTITUICOES, LEDGER, ORIGEM_NOME, ORIGEM_SITE, POSTS, REGRAS, RESPONSAVEL, REVISAO,
SAIDA, TEXTOS, codigo)
NIVEIS = {0: 'acervo da entidade custodiadora', 1: 'coleção', 2: 'seção', 3: 'série',
4: 'dossiê', 5: 'item documental'}
MESES_ABNT = ['jan.', 'fev.', 'mar.', 'abr.', 'maio', 'jun.', 'jul.', 'ago.', 'set.', 'out.', 'nov.', 'dez.']
HOJE = date.today().isoformat()
# ── leitura das fontes ──────────────────────────────────────────────────────
def _valores(item: dict) -> dict[str, list[str]]:
vs = defaultdict(list)
for row in item.get('full_table') or []:
v = (row.get('value') or '').strip()
if v and v not in vs[row['field']]:
vs[row['field']].append(v)
return vs
def _ledger() -> dict[str, list[dict]]:
arquivos = defaultdict(list)
if not LEDGER.exists():
return arquivos
con = sqlite3.connect(LEDGER)
for h, nome, b, sha, quando, tipo in con.execute(
'select handle, nome, bytes, sha256, quando, tipo from enviados order by handle, tipo, nome'):
arquivos[h].append({'nome': nome, 'bytes': b, 'sha256': sha, 'quando': quando, 'tipo': tipo})
return arquivos
def _abnt(post: dict) -> str:
autor = (post.get('autor') or '').strip()
partes = autor.split()
autor_abnt = f'{partes[-1].upper()}, {" ".join(partes[:-1])}' if len(partes) > 1 else autor.upper()
d = datetime.fromisoformat(post['data_iso'][:10])
acesso = (post.get('fetched_at') or HOJE)[:10]
a = datetime.fromisoformat(acesso)
return (f"{autor_abnt}. {post['titulo']}. {ORIGEM_NOME}, Rio de Janeiro, "
f"{d.day} {MESES_ABNT[d.month - 1]} {d.year}. Disponível em: {post['url']}. "
f"Acesso em: {a.day} {MESES_ABNT[a.month - 1]} {a.year}.")
def _posts() -> tuple[dict[str, list[dict]], list[dict]]:
por_handle, todos = defaultdict(list), []
if not POSTS.exists():
return por_handle, todos
for linha in POSTS.open(encoding='utf-8'):
p = json.loads(linha)
if p.get('erro') or not p.get('titulo'):
continue
hs = p.get('handles_referenciados') or []
if isinstance(hs, str):
hs = ast.literal_eval(hs)
cats = p.get('categorias') or []
if isinstance(cats, str):
cats = ast.literal_eval(cats)
ref = {'titulo': p['titulo'], 'url': p['url'], 'autor': p.get('autor'), 'data': p['data_iso'][:10],
'categorias': cats, 'abnt': _abnt(p)}
todos.append(ref)
for h in hs:
por_handle[h].append(ref)
return por_handle, todos
def _textos(caminho) -> dict[str, str]:
"""Um .md com seções "## 2.3 História arquivística" → {'2.3': texto}."""
if not caminho.exists():
return {}
out, atual, buf = {}, None, []
for linha in caminho.read_text(encoding='utf-8').splitlines():
if m := re.match(r'^##\s+(\d\.\d)\b', linha):
if atual:
out[atual] = '\n'.join(buf).strip()
atual, buf = m[1], []
elif atual:
buf.append(linha)
if atual:
out[atual] = '\n'.join(buf).strip()
return {k: v for k, v in out.items() if v}
def _mb(b: int) -> str:
return f'{b / 1e6:.1f} MB'.replace('.', ',') if b < 1e9 else f'{b / 1e9:.1f} GB'.replace('.', ',')
def _formato(nome: str) -> str:
ext = nome.rsplit('.', 1)[-1].lower() if '.' in nome else ''
return {'jpg': 'JPEG', 'jpeg': 'JPEG', 'png': 'PNG', 'tif': 'TIFF', 'tiff': 'TIFF', 'webp': 'WebP',
'pdf': 'PDF', 'gif': 'GIF'}.get(ext, ext.upper() or 'desconhecido')
def _limites(ds: list[dict]) -> dict:
ini = [d['inicio'] for d in ds if d and d.get('inicio')]
fim = [d['fim'] for d in ds if d and d.get('fim')]
if not ini and not fim:
return {'forma': '[s.d.]', 'iso': None, 'inicio': None, 'fim': None, 'qualidade': None,
'tipo': 'produção'}
a, b = min(ini or fim), max(fim or ini)
return {'forma': f'{a}-{b}' if a != b else str(a), 'iso': f'{a}/{b}' if a != b else str(a),
'inicio': a, 'fim': b, 'qualidade': 'datas-limite', 'tipo': 'produção'}
def _dimensao(especies: Counter, n_itens: int, bytes_: int) -> dict:
partes = [f'{n} {e}' for e, n in especies.most_common()]
return {'por_especie': dict(especies), 'arquivos_digitais': n_itens, 'bytes': bytes_,
'texto': f"iconográfico: {', '.join(partes)}; eletrônico: {n_itens} arquivos digitais ({_mb(bytes_)})"}
# ── montagem ────────────────────────────────────────────────────────────────
def monta():
SAIDA.mkdir(exist_ok=True)
REVISAO.mkdir(parents=True, exist_ok=True)
ledger = _ledger()
posts_de, posts = _posts()
itens = []
for linha in CATALOGO.open(encoding='utf-8'):
if linha.strip():
it = json.loads(linha)
it['_v'] = _valores(it)
itens.append(it)
# séries (nível 3)
fontes = [(it['handle'], it['community'], (it['_v']['dc.source'] or [None])[0]) for it in itens]
serie_de, series_ = series.monta(fontes)
# autoridades
aut = Autoridades()
for it in itens:
for f in ('dc.contributor', 'dc.contributor.author', 'dc.contributor.other'):
for v in it['_v'][f]:
aut.registra(v)
aut.resolve()
for reg in aut.registros.values(): # biografias no blog (3.4) → fontes da autoridade
if reg['tipo'] == 'pessoa' and ',' in reg['nome']:
sob, nome = [x.strip() for x in reg['nome'].split(',', 1)]
primeiro = nome.split()[0] if nome else ''
for p in posts:
if 'Biografia' in p['categorias'] and sob in p['titulo'] and primeiro in p['titulo']:
reg['fontes'].append(p['abnt'])
unidades: dict[str, dict] = {}
filhos = defaultdict(list)
rev_datas, rev_tipos, rev_direitos, rev_lugares = [], [], [], []
def add(u: dict):
unidades[u['id']] = u
if u['pai']:
filhos[u['pai']].append(u['id'])
# níveis 4 e 5
for it in itens:
v, h = it['_v'], it['handle']
inst = it['community']
sigla_i, nome_i, cidade_i = INSTITUICOES[inst]
sg, notacao = serie_de[h]
num = h.rsplit('/', 1)[-1]
cod = codigo(COLECAO, sigla_i, sg, num)
pasta = h.replace('/', '_')
titulo = (v['dc.title'] or [it.get('title') or ''])[0].strip()
titulo_atribuido = False
if not titulo: # proc. 1.2: título atribuído a partir da própria unidade, entre colchetes
base = next((x for x in v['dc.description'] + v['dc.subject'] + v['dc.coverage']
if not campos.corrompido(x)), 'Sem título')
titulo, titulo_atribuido = f"[{base.split('. ')[0][:90]}]", True
d = datas.interpreta((v['dc.date'] or [''])[0])
data = d.registro()
if len(v['dc.date']) > 1:
data['notas'] = (data['notas'] or []) + [f'outra data na origem: {x}' for x in v['dc.date'][1:]]
produtores = []
for f in ('dc.contributor', 'dc.contributor.author', 'dc.contributor.other'):
for nome in v[f]:
aid = aut.id(nome)
if aid and aid not in [p['id'] for p in produtores]:
produtores.append({'id': aid, 'forma': aut[aid]['forma_autorizada'],
'funcao': 'autor' if f != 'dc.contributor.other' else 'outro'})
if not produtores:
produtores = [{'id': None, 'forma': 'Autoria não identificada', 'funcao': 'autor'}]
descricoes = [x for x in v['dc.description'] if not campos.corrompido(x)]
fisico = [x for x in descricoes if campos.e_fisico(x)]
conteudo = [x for x in descricoes if x not in fisico]
esp = campos.especie((v['dc.type'] or [None])[0])
cor = campos.cor(fisico)
arquivos = ledger.get(h, [])
preserv = sorted((a for a in arquivos if a['tipo'] == 'preservacao'),
key=lambda a: (a['nome'] != 'imagem.jpg', a['nome']))
imagens = preserv or [a for a in arquivos if a['tipo'] == 'thumbnail']
total = sum(a['bytes'] or 0 for a in imagens)
direitos = campos.direitos(v['dc.rights'] + [str(it.get('rights') or '')], nome_i)
lugar = campos.lugar(v['dc.coverage'])
assuntos = [s for s in v['dc.subject'] if not campos.corrompido(s)]
codigo_origem = next((re.sub(r'\.\w{3,4}$', '', x) for x in v['dc.identifier'] + v['dc.description']
if re.match(r'BR ?[A-Z]{2}[A-Z]{2,}\s', x)), None)
sophia = next((x for x in v['dc.relation'] if 'sophia' in x), None)
objdigital = [x for x in v['dc.identifier'] if 'objdigital' in x]
notas = ['Título atribuído pelo arquivista: a origem não informa título.'] if titulo_atribuido else []
if d.original and data['forma'] != d.original:
notas.append(f'Data como consta na origem: {d.original}')
notas += data['notas'] or []
if esp['presumida']:
notas.append('Espécie documental não informada na origem; presumida fotografia'
+ (f" (valor na origem: {esp['original']})" if esp.get('original') else '') + '.')
if direitos['originais']:
notas.append('Direitos como constam na origem: ' + ' | '.join(direitos['originais']))
lixo = [f"{f}: {x[:60]}" for f, xs in v.items() for x in xs if campos.corrompido(x)
and f in ('dc.type', 'dc.rights', 'dc.source', 'dc.subject', 'dc.date', 'dc.coverage')]
if lixo:
notas.append('Valores corrompidos na origem, desconsiderados: ' + '; '.join(lixo))
conservacao = None
if not preserv:
conservacao = ('Arquivo de preservação indisponível na origem (o repositório responde HTTP 500 '
'ao bitstream; ver errors.md). A cópia guarda apenas a miniatura gerada pelo DSpace.')
dossie = {
'id': cod, 'pai': codigo(COLECAO, sigla_i, sg), 'nivel': 4, 'handle': h,
'1.1': cod,
'1.2': {'titulo': titulo, 'atribuido': titulo.startswith('[')},
'1.3': {'producao': data, 'topica': lugar},
'1.4': {'nivel': 4, 'nome': NIVEIS[4]},
'1.5': {'genero': esp['genero'], 'especie': esp['especie'], 'suporte': esp['suporte'], 'cor': cor,
'fisico': fisico or None, 'arquivos_digitais': len(imagens), 'bytes': total,
'texto': (f"1 {esp['especie']}" + (f' {cor}' if cor else '')
+ (f", {esp['suporte']}" if esp['suporte'] else '')
+ (f" ({'; '.join(fisico)})" if fisico else '')
+ f"; {len(imagens)} arquivo{'s' if len(imagens) != 1 else ''} digita"
+ f"{'is' if len(imagens) != 1 else 'l'} ({_mb(total)})")},
'2.1': produtores,
'2.3': (f"Registrado no repositório DSpace da {ORIGEM_NOME}"
+ (f" em {v['dc.date.accessioned'][0][:10]}" if v['dc.date.accessioned'] else '')
+ f". Cópia digital capturada em {it['fetched_at'][:10]} e armazenada em "
f"{BUCKET}/itens/{pasta}/ com SHA-256 de cada arquivo."),
'2.4': (f"{ORIGEM_NOME} ({ORIGEM_SITE}), repositório mantido pela Biblioteca Nacional; "
f"coleta combinada com a Biblioteca Nacional, em {it['fetched_at'][:10]}. Handle {h}."),
'3.1': ' '.join(conteudo) or None,
'4.2': direitos,
'4.3': campos.idiomas(v['dc.language']) or None,
'4.4': (f"Arquivo{'s' if len(imagens) > 1 else ''} digita{'is' if len(imagens) > 1 else 'l'} "
f"{', '.join(sorted({_formato(a['nome']) for a in imagens})) or '—'}; "
'abre em qualquer visualizador de imagem.'
+ (f" Original: {'; '.join(fisico)}." if fisico else '')),
'5.1': {'instituicao': nome_i, 'local': cidade_i, 'url': HANDLE_URL.format(h),
'codigo_na_origem': codigo_origem, 'notacao_na_origem': notacao, 'sophia': sophia,
'texto': f"Original sob a guarda de {nome_i}, {cidade_i}"
+ (f", código {codigo_origem}" if codigo_origem else '')
+ (f", notação {notacao}" if notacao else '') + f". {HANDLE_URL.format(h)}"},
'5.2': {'nesta_entidade': [{'tipo': a['tipo'], 'chave': (f"{BUCKET}/derivados/thumb256/{a['nome']}"
if a['tipo'] == 'derivado-thumb256'
else f"{BUCKET}/itens/{pasta}/{a['tipo']}/{a['nome']}"),
'bytes': a['bytes'], 'sha256': a['sha256']} for a in arquivos],
'em_outras_entidades': [{'entidade': 'Biblioteca Nacional (Brasil) — BNDigital', 'url': u}
for u in objdigital] or None},
'5.3': (v['dc.relation.ispartof'] + v['dcterms.relation']) or None,
'5.4': [p['abnt'] for p in posts_de.get(h, [])] or None,
'6.1': conservacao,
'6.2': notas or None,
'7.1': (f'Descrição gerada pelo pipeline/nobrade a partir dos metadados Dublin Core colhidos '
f'da {ORIGEM_NOME} ({it["source_url"]}); responsável: {RESPONSAVEL}.'),
'7.3': {'coleta': it['fetched_at'][:10], 'descricao': HOJE},
'8.1': {'assuntos': [s for s in assuntos if campos.idioma_termo(s) == 'por'] or None,
'assuntos_eng': [s for s in assuntos if campos.idioma_termo(s) == 'eng'] or None,
'lugares': [c for c in v['dc.coverage'] if not campos.corrompido(c)] or None,
'nomes': [p['forma'] for p in produtores if p['id']] or None,
'periodo': data['iso']},
'_sd': not d.ok, '_especie': esp['especie'], '_serie': (inst, sg),
}
add(dossie)
rev_datas.append({'handle': h, 'original': d.original, 'forma': data['forma'], 'iso': data['iso'],
'inicio': data['inicio'], 'fim': data['fim'], 'qualidade': data['qualidade'],
'motivo': data['motivo']})
rev_tipos.append({'instituicao': inst, 'dc_type': (v['dc.type'] or [''])[0], 'genero': esp['genero'],
'especie': esp['especie'], 'suporte': esp['suporte'] or '', 'presumida': esp['presumida']})
rev_direitos.append({'instituicao': inst, 'categoria': direitos['categoria'], 'detentor': direitos['detentor'],
'originais': ' | '.join(direitos['originais'] or [])})
rev_lugares.append({'coverage': ' | '.join(v['dc.coverage']), 'data_topica': lugar or ''})
for i, a in enumerate(imagens, 1):
ci = f'{cod}.{i:02d}'
add({
'id': ci, 'pai': cod, 'nivel': 5, 'handle': h,
'1.1': ci,
'1.2': {'titulo': f"{titulo} [imagem {i} de {len(imagens)}]", 'atribuido': True},
'1.3': {'producao': data},
'1.4': {'nivel': 5, 'nome': NIVEIS[5]},
'1.5': {'genero': 'eletrônico', 'especie': 'arquivo de imagem', 'bytes': a['bytes'],
'texto': f"1 arquivo digital {_formato(a['nome'])} ({_mb(a['bytes'] or 0)})"},
'2.1': produtores,
'4.4': f"{_formato(a['nome'])}; SHA-256 {a['sha256']}"
+ ('; miniatura — preservação indisponível na origem' if a['tipo'] == 'thumbnail' else ''),
'5.2': {'nesta_entidade': [{'tipo': a['tipo'], 'chave': f"{BUCKET}/itens/{pasta}/{a['tipo']}/{a['nome']}",
'bytes': a['bytes'], 'sha256': a['sha256']}]},
'7.3': {'descricao': HOJE},
})
# níveis 3, 2, 1, 0 — de baixo para cima, agregando os filhos
textos1 = _textos(TEXTOS / 'nivel1.md')
textos0 = _textos(TEXTOS / 'nivel0.md')
por_serie = defaultdict(list)
for u in unidades.values():
if u['nivel'] == 4:
por_serie[u['_serie']].append(u)
# datas atribuídas aos s.d. (proc. 1.3.3.2): período do fotógrafo, senão datas-limite da série
rev_por_handle = {r['handle']: r for r in rev_datas}
lim_secao = {inst: _limites([u['1.3']['producao'] for (i, _), us in por_serie.items() if i == inst
for u in us if not u['_sd']]) for inst, _ in por_serie}
lim_serie = {k: _limites([u['1.3']['producao'] for u in us if not u['_sd']]) for k, us in por_serie.items()}
for k, us in por_serie.items():
for u in us:
if not u['_sd']:
continue
per = next((aut[p['id']]['periodo_atividade'] for p in u['2.1']
if p['id'] and aut[p['id']]['periodo_atividade']), None)
if per:
nova = datas.atribuida(*per, 'período de atividade do autor')
elif lim_serie[k]['inicio']:
nova = datas.atribuida(lim_serie[k]['inicio'], lim_serie[k]['fim'], 'datas-limite da série')
elif lim_secao[k[0]]['inicio']:
nova = datas.atribuida(lim_secao[k[0]]['inicio'], lim_secao[k[0]]['fim'],
'datas-limite da instituição de origem')
else:
continue
nova['original'] = u['1.3']['producao']['original']
nova['notas'] = nova['notas'] + (u['1.3']['producao']['notas'] or [])
u['1.3']['producao'] = nova
u['8.1']['periodo'] = nova['iso']
for fid in filhos[u['id']]:
unidades[fid]['1.3']['producao'] = nova
r = rev_por_handle[u['handle']]
r.update(forma=nova['forma'], iso=nova['iso'], inicio=nova['inicio'], fim=nova['fim'],
qualidade='atribuída pelo arquivista')
def agrega(us: list[dict]) -> dict:
esp = Counter(u['_especie'] for u in us)
n = sum(u['1.5']['arquivos_digitais'] for u in us)
b = sum(u['1.5']['bytes'] for u in us)
autores = Counter(p['forma'] for u in us for p in u['2.1'] if p['id'])
lugares = Counter(u['1.3']['topica'] for u in us if u['1.3'].get('topica'))
return {'data': _limites([u['1.3']['producao'] for u in us]), 'dim': _dimensao(esp, n, b),
'autores': autores, 'lugares': lugares, 'dossies': len(us)}
def resumo(a: dict) -> str:
s = f"{a['dossies']} dossiês, {a['data']['forma']}."
if a['autores']:
s += ' Principais autores: ' + '; '.join(f'{k} ({n})' for k, n in a['autores'].most_common(5)) + '.'
if a['lugares']:
s += ' Principais lugares: ' + '; '.join(f'{k} ({n})' for k, n in a['lugares'].most_common(5)) + '.'
return s
rev_series = []
por_secao = defaultdict(list)
for (inst, sg), us in sorted(por_serie.items()):
sigla_i, nome_i, cidade_i = INSTITUICOES[inst]
info = series_[(inst, sg)]
cod = codigo(COLECAO, sigla_i, sg)
a = agrega(us)
t = _textos(TEXTOS / 'series' / f'{sigla_i}.{sg}.md')
titulo = info['titulo'] if sg != 'AV' else f'Avulsos — {nome_i}'
produtor = re.sub(r'(?i)^(cole[cç][aã]o( particular)?|fundo|arquivo|s[ée]rie|conv[eê]nio)\s+(d[aeo]s?\s+)?', '', titulo)
add({
'id': cod, 'pai': codigo(COLECAO, sigla_i), 'nivel': 3,
'1.1': cod, '1.2': {'titulo': titulo, 'atribuido': sg == 'AV'},
'1.3': {'producao': a['data']}, '1.4': {'nivel': 3, 'nome': NIVEIS[3]},
'1.5': a['dim'],
'2.1': [{'id': None, 'forma': nome_i if sg == 'AV' else produtor, 'funcao': 'colecionador'}],
'2.2': t.get('2.2'),
'3.1': t.get('3.1') or resumo(a),
'3.4': t.get('3.4') or ('Dossiês ordenados pelo número do handle na origem.' if sg != 'AV' else
'Reúne os dossiês sem coleção/fundo de origem identificável em dc.source; '
'a notação da origem, quando há, fica em 5.1 de cada dossiê.'),
'5.1': {'instituicao': nome_i, 'local': cidade_i,
'variantes_na_origem': dict(info['variantes'].most_common()) or None},
'7.3': {'descricao': HOJE},
})
por_secao[inst].extend(us)
for var, n in (info['variantes'].most_common() or [('', info['n'])]):
rev_series.append({'instituicao': inst, 'codigo': cod, 'titulo': titulo, 'dc_source': var, 'n': n})
todos = []
for inst, us in sorted(por_secao.items(), key=lambda kv: -len(kv[1])):
sigla_i, nome_i, cidade_i = INSTITUICOES[inst]
cod = codigo(COLECAO, sigla_i)
a = agrega(us)
t = _textos(TEXTOS / 'secoes' / f'{sigla_i}.md')
add({
'id': cod, 'pai': codigo(COLECAO), 'nivel': 2,
'1.1': cod, '1.2': {'titulo': f'Seção {nome_i}', 'atribuido': True},
'1.3': {'producao': a['data']}, '1.4': {'nivel': 2, 'nome': NIVEIS[2]},
'1.5': a['dim'],
'2.1': [{'id': None, 'forma': nome_i, 'funcao': 'instituição custodiadora dos originais'}],
'2.2': t.get('2.2'),
'3.1': t.get('3.1') or resumo(a),
'3.4': f"{len(filhos[cod])} séries, por coleção ou fundo de origem.",
'5.1': {'instituicao': nome_i, 'local': cidade_i},
'7.3': {'descricao': HOJE},
})
todos.extend(us)
a = agrega(todos)
c1 = codigo(COLECAO)
add({
'id': c1, 'pai': codigo(), 'nivel': 1,
'1.1': c1, '1.2': {'titulo': COLECAO_TITULO, 'atribuido': False},
'1.3': {'producao': a['data'], 'acumulacao': {'forma': '2026', 'iso': '2026'}},
'1.4': {'nivel': 1, 'nome': NIVEIS[1]}, '1.5': a['dim'],
'2.1': [{'id': None, 'forma': ENTIDADE_NOME, 'funcao': 'colecionador'}],
'2.2': textos1.get('2.2'), '2.3': textos1.get('2.3'), '2.4': textos1.get('2.4'),
'3.1': textos1.get('3.1') or resumo(a), '3.2': textos1.get('3.2'), '3.3': textos1.get('3.3'),
'3.4': textos1.get('3.4'),
'4.1': textos1.get('4.1') or ACESSO, '4.2': textos1.get('4.2'),
'4.3': textos1.get('4.3'), '4.4': textos1.get('4.4'), '4.5': textos1.get('4.5'),
'5.1': textos1.get('5.1'), '5.4': textos1.get('5.4'),
'6.2': textos1.get('6.2'),
'7.1': textos1.get('7.1'), '7.2': REGRAS, '7.3': {'descricao': HOJE},
'8.1': textos1.get('8.1'),
})
c0 = codigo()
add({
'id': c0, 'pai': None, 'nivel': 0,
'1.1': c0, '1.2': {'titulo': f'Acervo {ENTIDADE_NOME}', 'atribuido': False},
'1.3': {'producao': a['data']}, '1.4': {'nivel': 0, 'nome': NIVEIS[0]}, '1.5': a['dim'],
'2.1': [{'id': None, 'forma': ENTIDADE_NOME, 'funcao': 'entidade custodiadora'}],
'2.2': textos0.get('2.2'), '3.1': textos0.get('3.1'),
'4.1': textos0.get('4.1') or ACESSO, '7.2': REGRAS, '7.3': {'descricao': HOJE},
})
# 1.4: títulos dos níveis superiores (proc. 1.1.4)
for u in unidades.values():
cad, p = [], u['pai']
while p:
cad.append(unidades[p]['1.2']['titulo'])
p = unidades[p]['pai']
u['1.4']['hierarquia'] = list(reversed(cad)) or None
ordem = sorted(unidades.values(), key=lambda u: (u['nivel'], u['id']))
with (SAIDA / 'unidades.jsonl').open('w', encoding='utf-8') as f:
for u in ordem:
f.write(json.dumps({k: x for k, x in u.items() if not k.startswith('_') and x is not None},
ensure_ascii=False) + '\n')
with (SAIDA / 'autoridades.jsonl').open('w', encoding='utf-8') as f:
for r in aut.registros.values():
f.write(json.dumps(r, ensure_ascii=False) + '\n')
# tabelas de revisão
pl.DataFrame(rev_datas, infer_schema_length=None).filter(pl.col('qualidade').is_null() | (pl.col('qualidade') != 'exata')) \
.sort('motivo', 'original', nulls_last=True).write_csv(REVISAO / 'datas.tsv', separator='\t')
pl.DataFrame(rev_series, infer_schema_length=None).sort('codigo', 'n', descending=[False, True]) \
.write_csv(REVISAO / 'series.tsv', separator='\t')
pl.DataFrame(rev_tipos, infer_schema_length=None).group_by(pl.all()).len('n').sort('n', descending=True) \
.write_csv(REVISAO / 'tipos.tsv', separator='\t')
pl.DataFrame(rev_direitos, infer_schema_length=None).group_by(pl.all()).len('n').sort('n', descending=True) \
.write_csv(REVISAO / 'direitos.tsv', separator='\t')
pl.DataFrame(rev_lugares, infer_schema_length=None).group_by(pl.all()).len('n').sort('n', descending=True) \
.write_csv(REVISAO / 'lugares.tsv', separator='\t')
pl.DataFrame([{'id': r['id'], 'forma_autorizada': r['forma_autorizada'], 'tipo': r['tipo'],
'ocorrencias': r['ocorrencias'], 'variantes': ' | '.join(r['variantes'])}
for r in aut.registros.values()], infer_schema_length=None).write_csv(REVISAO / 'autoridades.tsv', separator='\t')
return unidades, aut