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:
@@ -0,0 +1,53 @@
|
||||
"""python -m nobrade (de dentro de pipeline/)
|
||||
|
||||
Gera a descrição NOBRADE completa em descricao/: unidades, autoridades,
|
||||
tabelas de revisão, pendências, EAD 3, EAC-CPF e CSV do AtoM, e valida os XML
|
||||
contra os XSD de nobrade/xsd/ quando o xmllint está disponível.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from . import exporta, montar, valida
|
||||
from .config import SAIDA
|
||||
|
||||
|
||||
def main():
|
||||
print('montando unidades…', flush=True)
|
||||
unidades, aut = montar.monta()
|
||||
lista = sorted(unidades.values(), key=lambda u: (u['nivel'], u['id']))
|
||||
lista = [{k: v for k, v in u.items() if not k.startswith('_') and v is not None} for u in lista]
|
||||
pend = valida.valida(lista)
|
||||
print(f'{len(lista)} unidades, {len(aut.registros)} autoridades, {pend} pendências obrigatórias')
|
||||
|
||||
print('exportando EAD 3, EAC-CPF e AtoM…', flush=True)
|
||||
exporta.ead(lista, aut.registros)
|
||||
usos = defaultdict(list)
|
||||
for u in lista:
|
||||
if u['nivel'] == 4:
|
||||
for p in u['2.1']:
|
||||
if p.get('id'):
|
||||
usos[p['id']].append(u['id'])
|
||||
exporta.eac(list(aut.registros.values()), usos)
|
||||
exporta.atom(lista)
|
||||
|
||||
if shutil.which('xmllint') and '--sem-xsd' not in sys.argv:
|
||||
xsd = Path(__file__).parent / 'xsd'
|
||||
r = subprocess.run(['xmllint', '--noout', '--huge', '--schema', xsd / 'ead3.xsd',
|
||||
SAIDA / 'ead' / 'brasiliana.xml'], capture_output=True, text=True)
|
||||
print('EAD 3:', (r.stderr.strip().splitlines() or ['?'])[-1])
|
||||
eacs = sorted((SAIDA / 'eac').glob('*.xml'))
|
||||
r = subprocess.run(['xmllint', '--noout', '--schema', xsd / 'cpf.xsd', *eacs],
|
||||
capture_output=True, text=True)
|
||||
falhas = [x for x in r.stderr.splitlines() if 'fails to validate' in x or 'error' in x.lower()]
|
||||
print(f'EAC-CPF: {len(eacs)} arquivos, {len(falhas)} com erro')
|
||||
for x in falhas[:10]:
|
||||
print(' ', x)
|
||||
return 1 if pend else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Registros de autoridade (ISAAR(CPF)) para produtores — elemento 2.1 e 8.1.
|
||||
|
||||
Junta as grafias de `dc.contributor` ("Malta, Augusto", "Malta, Augusto,
|
||||
1864-1957", "Marc Ferrez" e "Ferrez, Marc") numa forma autorizada, com as
|
||||
variantes como remissivas (proc. 8.1.4).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
ANONIMO = re.compile(r'(?i)^\s*(an[oô]nimo|autoria n[aã]o identificada|s/\s*autor|sem autor|desconhecid[oa]|'
|
||||
r'n[aã]o identificad[oa]|autor n[aã]o identificado|autor desconhecido)\s*\.?$')
|
||||
COLETIVA = re.compile(r'(?i)\b(escola|officina|oficina|photographia|fotografia|photo|foto|studio|est[uú]dio|atelier|'
|
||||
r'casa|companhia|cia\.?|museu|instituto|minist[eé]rio|servi[cç]o|diretoria|departamento|'
|
||||
r'comiss[aã]o|biblioteca|arquivo|editora|lith|litografia|tipografia|typ|&|irm[aã]os|'
|
||||
r'sociedade|empresa|jornal|revista|ag[eê]ncia|governo|prefeitura|marinha|ex[eé]rcito|'
|
||||
r'for[cç]a|aeron[aá]utica|avia[cç][aã]o|universidade|fundação)\b')
|
||||
DATAS = re.compile(r',?\s*((?:ca\.\s*|fl\.\s*|m\.\s*|n\.\s*)?\d{4}\??(?:\s*-\s*(?:ca\.\s*)?\d{0,4}\??)?)\.?\s*$')
|
||||
PARTICULAS = {'de', 'da', 'do', 'das', 'dos', 'e', 'von', 'van', 'du', 'del', 'di'}
|
||||
|
||||
|
||||
def _chave(s: str) -> str:
|
||||
s = ''.join(c for c in unicodedata.normalize('NFD', s.lower()) if unicodedata.category(c) != 'Mn')
|
||||
s = re.sub(r'\(.*?\)', '', s)
|
||||
return re.sub(r'[^\w,]+', ' ', s).strip(' ,')
|
||||
|
||||
|
||||
def _separa_datas(nome: str) -> tuple[str, str | None]:
|
||||
nome = nome.strip().rstrip('.')
|
||||
if m := DATAS.search(nome):
|
||||
if ',' in nome[:m.start()] or m[1].startswith(('fl', 'm.', 'n.', 'ca')):
|
||||
return nome[:m.start()].strip(' ,'), m[1]
|
||||
return nome, None
|
||||
|
||||
|
||||
def _inverte(nome: str) -> str | None:
|
||||
"""'Marc Ferrez' → 'Ferrez, Marc'. Só para nomes de pessoa sem vírgula."""
|
||||
partes = nome.split()
|
||||
if len(partes) < 2 or ',' in nome:
|
||||
return None
|
||||
i = len(partes) - 1
|
||||
while i > 1 and partes[i - 1].lower() in PARTICULAS:
|
||||
i -= 1
|
||||
return f"{' '.join(partes[i:])}, {' '.join(partes[:i])}"
|
||||
|
||||
|
||||
def periodo(datas: str | None) -> tuple[int, int] | None:
|
||||
"""Anos prováveis de atividade: fl. A-B, ou nascimento+15 até a morte."""
|
||||
if not datas:
|
||||
return None
|
||||
anos = [int(a) for a in re.findall(r'\d{4}', datas)]
|
||||
if not anos:
|
||||
return None
|
||||
if datas.startswith('fl'):
|
||||
return (anos[0], anos[-1]) if len(anos) > 1 else (anos[0], anos[0])
|
||||
if datas.startswith('m.'):
|
||||
return None
|
||||
if len(anos) == 2:
|
||||
return anos[0] + 15, anos[1]
|
||||
return None
|
||||
|
||||
|
||||
class Autoridades:
|
||||
def __init__(self):
|
||||
self.contagem: Counter = Counter()
|
||||
|
||||
def registra(self, nome: str):
|
||||
if nome and nome.strip() and not ANONIMO.match(nome):
|
||||
self.contagem[nome.strip()] += 1
|
||||
|
||||
def resolve(self):
|
||||
"""Agrupa as grafias. Depois disso `forma(nome)` devolve o id da autoridade."""
|
||||
grupos: dict[str, Counter] = defaultdict(Counter)
|
||||
for nome, n in self.contagem.items():
|
||||
base, _ = _separa_datas(nome)
|
||||
grupos[_chave(base)][nome] += n
|
||||
# grafias diretas ("Marc Ferrez") que batem com uma invertida já conhecida
|
||||
for k in list(grupos):
|
||||
if ',' in k:
|
||||
continue
|
||||
nome = next(iter(grupos[k]))
|
||||
if COLETIVA.search(nome):
|
||||
continue
|
||||
inv = _inverte(_separa_datas(nome)[0])
|
||||
if inv and _chave(inv) in grupos and _chave(inv) != k:
|
||||
grupos[_chave(inv)].update(grupos.pop(k))
|
||||
|
||||
self.registros: dict[str, dict] = {}
|
||||
self._id_de: dict[str, str] = {}
|
||||
for k, variantes in sorted(grupos.items(), key=lambda kv: -sum(kv[1].values())):
|
||||
# forma autorizada: a variante mais completa (com datas) e mais usada
|
||||
# (datas com início e fim > só uma data > nenhuma), depois frequência
|
||||
def peso(v):
|
||||
dt = _separa_datas(v)[1] or ''
|
||||
return (len(re.findall(r'\d{4}', dt)), variantes[v], v)
|
||||
forma = max(variantes, key=peso)
|
||||
forma = forma.strip().rstrip('.')
|
||||
base, datas = _separa_datas(forma)
|
||||
tipo = 'entidade coletiva' if COLETIVA.search(base) else ('pessoa' if ',' in base else 'pessoa ou entidade')
|
||||
if re.match(r'(?i)fam[ií]lia\b', base):
|
||||
tipo = 'família'
|
||||
aid = 'A' + str(len(self.registros) + 1).zfill(4)
|
||||
self.registros[aid] = {
|
||||
'id': aid, 'forma_autorizada': forma, 'nome': base, 'datas_existencia': datas,
|
||||
'tipo': tipo, 'variantes': sorted(v for v in variantes if v.strip().rstrip('.') != forma),
|
||||
'ocorrencias': sum(variantes.values()), 'periodo_atividade': periodo(datas),
|
||||
'fontes': [],
|
||||
}
|
||||
for v in variantes:
|
||||
self._id_de[v] = aid
|
||||
|
||||
def id(self, nome: str) -> str | None:
|
||||
return self._id_de.get((nome or '').strip())
|
||||
|
||||
def __getitem__(self, aid: str) -> dict:
|
||||
return self.registros[aid]
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Normalizadores de campo único: espécie/suporte (1.5), características
|
||||
físicas (1.5/4.4), direitos (4.2), idioma (4.3), lugar (1.3.1 data tópica) e
|
||||
assuntos (8.1)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
CORROMPIDO = re.compile(r'[<>{}]|ns:|PALAVRAS-CHAVE|^[0-9a-f]{6,}|\d[a-f]\d[a-f]\d')
|
||||
|
||||
|
||||
def sem_acento(s: str) -> str:
|
||||
return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
|
||||
|
||||
|
||||
def corrompido(v: str | None) -> bool:
|
||||
return bool(v) and bool(CORROMPIDO.search(v))
|
||||
|
||||
|
||||
# ── 1.5 espécie e suporte (a partir de dc.type) ─────────────────────────────
|
||||
ESPECIES = [ # (padrão, gênero, espécie)
|
||||
(r'^negativo', 'iconográfico', 'negativo fotográfico'),
|
||||
(r'^diapositivo', 'iconográfico', 'diapositivo'),
|
||||
(r'reprodu[cç][aã]o fotomec', 'iconográfico', 'reprodução fotomecânica'),
|
||||
(r'^(fotografia|vista panor)', 'iconográfico', 'fotografia'),
|
||||
(r'^gravura', 'iconográfico', 'gravura'),
|
||||
(r'^desenho', 'iconográfico', 'desenho'),
|
||||
(r'^[áa]lbum', 'iconográfico', 'álbum fotográfico'),
|
||||
(r'^manuscrito', 'textual', 'manuscrito'),
|
||||
(r'^livro', 'bibliográfico', 'livro'),
|
||||
(r'^artigo de peri', 'bibliográfico', 'periódico'),
|
||||
]
|
||||
|
||||
|
||||
def especie(tipo: str | None) -> dict:
|
||||
"""{'genero', 'especie', 'suporte', 'presumida'}. Sem dc.type (ou com lixo),
|
||||
presume fotografia — o acervo inteiro é um repositório fotográfico — e marca."""
|
||||
t = (tipo or '').strip()
|
||||
if t and not corrompido(t):
|
||||
base, _, sup = t.partition('/')
|
||||
b = sem_acento(base.strip().lower())
|
||||
for pat, gen, esp in ESPECIES:
|
||||
if re.search(pat, b) or re.search(pat, base.strip().lower()):
|
||||
suporte = sup.strip().lower() or None
|
||||
if esp == 'negativo fotográfico' and 'flex' in b:
|
||||
suporte = 'flexível'
|
||||
return {'genero': gen, 'especie': esp, 'suporte': suporte, 'presumida': False}
|
||||
return {'genero': 'iconográfico', 'especie': 'fotografia', 'suporte': None, 'presumida': True,
|
||||
'original': t or None}
|
||||
|
||||
|
||||
# ── características físicas que algumas instituições põem em dc.description ──
|
||||
FISICO = re.compile(
|
||||
r'(?i)^\s*(p\s*&\s*b|p/b|preto e branco|cor(ida)?|colorid[ao]|s[ée]pia|'
|
||||
r'albumina|gelatina|prata|col[oó]dio|cianotipia|platina|carv[aã]o|ambr[oó]tipo|daguerre[oó]tipo|'
|
||||
r'negativo|acetato|nitrato|vidro|papel|cart[aã]o|\d+[,.]?\d*\s*x\s*\d+|processo:|segunda gera)')
|
||||
|
||||
|
||||
def e_fisico(v: str) -> bool:
|
||||
return bool(FISICO.match(v)) and len(v) < 120
|
||||
|
||||
|
||||
def cor(fisico: list[str]) -> str | None:
|
||||
t = ' '.join(fisico).lower()
|
||||
if re.search(r'p\s*&\s*b|p/b|preto e branco', t):
|
||||
return 'p&b'
|
||||
if re.search(r'colorid|\bcor\b', t):
|
||||
return 'cor'
|
||||
return None
|
||||
|
||||
|
||||
# ── 4.2 condições de reprodução ─────────────────────────────────────────────
|
||||
def direitos(textos: list[str], instituicao: str) -> dict:
|
||||
"""{'categoria', 'detentor', 'texto', 'originais'}."""
|
||||
limpos = list(dict.fromkeys(t.strip() for t in textos if t and t.strip() and t.strip() != 'None'))
|
||||
junto = sem_acento(' '.join(limpos).lower())
|
||||
if re.search(r'dominio publico|omínio público|omi?nio publico', junto):
|
||||
cat = 'domínio público'
|
||||
elif 'liberado para uso de natureza cultural' in junto:
|
||||
cat = 'uso cultural liberado'
|
||||
elif re.search(r'solicitar|requer libera', junto):
|
||||
cat = 'requer autorização do detentor'
|
||||
else:
|
||||
cat = 'consultar o detentor'
|
||||
detentor = None
|
||||
for t in limpos:
|
||||
a = sem_acento(t.lower())
|
||||
if corrompido(t) or re.search(r'dominio publico|solicitar|requer|liberado', a) or len(t) < 6:
|
||||
continue
|
||||
if 'bibli' in a and 'nac' in a:
|
||||
detentor = 'Biblioteca Nacional (Brasil)'
|
||||
elif 'moreira' in a and 'salles' in a and '/' not in t:
|
||||
detentor = 'Instituto Moreira Salles'
|
||||
else:
|
||||
detentor = t
|
||||
break
|
||||
detentor = detentor or instituicao
|
||||
frases = {
|
||||
'domínio público': 'Imagem em domínio público.',
|
||||
'uso cultural liberado': 'Uso liberado para fins de natureza cultural.',
|
||||
'requer autorização do detentor': 'Reprodução sujeita a autorização do detentor dos direitos.',
|
||||
'consultar o detentor': 'Condições de reprodução não informadas na origem; consultar o detentor.',
|
||||
}
|
||||
return {'categoria': cat, 'detentor': detentor,
|
||||
'texto': f'{frases[cat]} Detentor indicado: {detentor}.',
|
||||
'originais': limpos or None}
|
||||
|
||||
|
||||
# ── 4.3 idioma ───────────────────────────────────────────────────────────────
|
||||
NOMES_IDIOMA = {'por': 'português', 'fre': 'francês', 'eng': 'inglês', 'lat': 'latim', 'ger': 'alemão',
|
||||
'ita': 'italiano', 'ukr': 'ucraniano', 'hun': 'húngaro', 'spa': 'espanhol'}
|
||||
|
||||
|
||||
def idiomas(vs: list[str]) -> list[dict]:
|
||||
out = []
|
||||
for v in vs:
|
||||
c = (v or '').strip().lower()
|
||||
if c in NOMES_IDIOMA and c not in [o['codigo'] for o in out]:
|
||||
out.append({'codigo': c, 'nome': NOMES_IDIOMA[c]})
|
||||
return out
|
||||
|
||||
|
||||
# ── 1.3.1 data tópica e 8.1 lugares ─────────────────────────────────────────
|
||||
UF = {'AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE',
|
||||
'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO'}
|
||||
ESTADOS = {'rio de janeiro': 'RJ', 'sao paulo': 'SP', 'bahia': 'BA', 'pernambuco': 'PE', 'minas gerais': 'MG',
|
||||
'para': 'PA', 'amazonas': 'AM', 'parana': 'PR', 'rio grande do sul': 'RS', 'santa catarina': 'SC',
|
||||
'ceara': 'CE', 'maranhao': 'MA', 'espirito santo': 'ES', 'goias': 'GO', 'mato grosso': 'MT',
|
||||
'paraiba': 'PB', 'alagoas': 'AL', 'sergipe': 'SE', 'piaui': 'PI', 'rio grande do norte': 'RN'}
|
||||
PAISES = {'brasil', 'brazil'}
|
||||
|
||||
|
||||
def lugar(coberturas: list[str]) -> str | None:
|
||||
"""A forma mais específica de cidade: 'Cidade (UF)'. Junta 'Rio de Janeiro' + 'RJ'."""
|
||||
cs = [c.strip() for c in coberturas if c and c.strip() and not corrompido(c)]
|
||||
for c in cs:
|
||||
if m := re.fullmatch(r'(.+?)\s*(?:\(|,\s*|\s-\s*)([A-Z]{2})\)?', c):
|
||||
if m[2] in UF and sem_acento(m[1].lower()) not in PAISES:
|
||||
return f'{m[1].strip()} ({m[2]})'
|
||||
ufs = [c for c in cs if c in UF]
|
||||
for c in cs:
|
||||
k = sem_acento(c.lower())
|
||||
if k in PAISES or c in UF or '(estado)' in k or 'state' in k:
|
||||
continue
|
||||
if re.search(r'\(([^)]+)\)$', c): # "Paris (França)", "Rio de Janeiro (Brazil)"
|
||||
nome, pais = re.match(r'(.+?)\s*\(([^)]+)\)$', c).groups()
|
||||
if sem_acento(pais.lower()) in PAISES:
|
||||
uf = ESTADOS.get(sem_acento(nome.lower()))
|
||||
return f'{nome} ({uf})' if uf else nome
|
||||
return c
|
||||
uf = ufs[0] if ufs else ESTADOS.get(k)
|
||||
return f'{c} ({uf})' if uf else c
|
||||
return None
|
||||
|
||||
|
||||
# ── 8.1 assuntos: separa as traduções inglesas que o DSpace da BN repete ─────
|
||||
EN = {'the', 'and', 'of', 'in', 'on', 'women', 'men', 'children', 'streets', 'street', 'buildings', 'building',
|
||||
'churches', 'church', 'views', 'view', 'slaves', 'railroads', 'railroad', 'harbors', 'harbor', 'landscapes',
|
||||
'portraits', 'bridges', 'rivers', 'houses', 'squares', 'parks', 'gardens', 'mountains', 'beaches',
|
||||
'ships', 'soldiers', 'history', 'photographs', 'people', 'indians', 'coffee', 'workers', 'palaces',
|
||||
'monuments', 'waterfalls', 'boats', 'plantations', 'aerial', 'military', 'city', 'cities', 'public',
|
||||
'social', 'life', 'customs', 'description', 'travel', 'transportation', 'agriculture', 'industry',
|
||||
'architecture', 'fountains', 'hospitals', 'schools', 'markets', 'lakes', 'bays', 'islands', 'forts',
|
||||
'wars', 'war', 'emperors', 'kings', 'queens', 'nobility', 'family', 'families', 'politicians'}
|
||||
PT = {'de', 'da', 'do', 'das', 'dos', 'e', 'em', 'no', 'na', 'nos', 'nas', 'para', 'com'}
|
||||
|
||||
|
||||
def idioma_termo(t: str) -> str:
|
||||
if re.search(r'[ãõçáéíóúâêô]', t.lower()):
|
||||
return 'por'
|
||||
palavras = set(re.findall(r'[a-z]+', t.lower()))
|
||||
if palavras & PT:
|
||||
return 'por'
|
||||
if palavras & EN or re.search(r'\((?:brazil|state)', t.lower()):
|
||||
return 'eng'
|
||||
return 'por'
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Constantes da descrição NOBRADE — decisões registradas em CONARQ.md §5."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
RAIZ = Path(__file__).resolve().parents[2]
|
||||
PIPELINE = RAIZ / 'pipeline'
|
||||
CATALOGO = PIPELINE / 'catalogo.jsonl'
|
||||
LEDGER = PIPELINE / 'subidor.sqlite'
|
||||
POSTS = PIPELINE / 'wordpress-posts.jsonl'
|
||||
ERROS = RAIZ / 'errors.md'
|
||||
AJUSTES = Path(__file__).resolve().parent / 'ajustes'
|
||||
SAIDA = RAIZ / 'descricao'
|
||||
TEXTOS = SAIDA / 'textos'
|
||||
REVISAO = SAIDA / 'revisao'
|
||||
|
||||
# 1.1 — código da entidade custodiadora. PROVISÓRIO até o CONARQ atribuir um
|
||||
# (proc. 1.1.1). Trocar aqui regenera todos os códigos de referência.
|
||||
# Formato do CODEARQ, como o AGCRJ usa: país, espaço, UF + sigla, e as partes
|
||||
# da unidade separadas por ponto — BR RJAGCRJ.SOP.NL.AVE.03.01
|
||||
PAIS = 'BR'
|
||||
ENTIDADE = 'RJXYZ'
|
||||
ENTIDADE_NOME = 'xyz — cópia digital de acervos fotográficos'
|
||||
ENTIDADE_SITE = 'https://xn--2dk.xyz/brasiliana/'
|
||||
COLECAO = 'BF'
|
||||
COLECAO_TITULO = 'Coleção Brasiliana Fotográfica (cópia digital)'
|
||||
RESPONSAVEL = 'rafapolo'
|
||||
BUCKET = 's3://brasiliana'
|
||||
|
||||
REGRAS = ('NOBRADE: Norma brasileira de descrição arquivística (CONARQ, 2006); '
|
||||
'ISAD(G) 2ª ed. (CIA, 2000); ISAAR(CPF) 2ª ed. (CIA, 2004); '
|
||||
'datas em ISO 8601-2 (EDTF); idiomas em ISO 639-2; países em ISO 3166.')
|
||||
|
||||
ORIGEM_NOME = 'Brasiliana Fotográfica'
|
||||
ORIGEM_SITE = 'https://brasilianafotografica.bn.gov.br/'
|
||||
HANDLE_URL = 'https://brasilianafotografica.bn.gov.br/brasiliana/handle/{}'
|
||||
|
||||
# Nível 2 — uma seção por instituição de origem (campo `community` do catálogo).
|
||||
# sigla → (nome autorizado, cidade)
|
||||
INSTITUICOES = {
|
||||
'Instituto Moreira Salles': ('IMS', 'Instituto Moreira Salles', 'Rio de Janeiro (RJ)'),
|
||||
'Biblioteca Nacional': ('BN', 'Biblioteca Nacional (Brasil)', 'Rio de Janeiro (RJ)'),
|
||||
'Museu Aeroespacial': ('MUSAL', 'Museu Aeroespacial', 'Rio de Janeiro (RJ)'),
|
||||
'Museu da Republica': ('MR', 'Museu da República', 'Rio de Janeiro (RJ)'),
|
||||
'Museu Historico Nacional': ('MHN', 'Museu Histórico Nacional', 'Rio de Janeiro (RJ)'),
|
||||
'Fiocruz': ('COC', 'Fundação Oswaldo Cruz. Casa de Oswaldo Cruz', 'Rio de Janeiro (RJ)'),
|
||||
'Diretoria do Patrimonio Historico e Documentacao da Marinha':
|
||||
('DPHDM', 'Diretoria do Patrimônio Histórico e Documentação da Marinha', 'Rio de Janeiro (RJ)'),
|
||||
'Arquivo Nacional': ('AN', 'Arquivo Nacional (Brasil)', 'Rio de Janeiro (RJ)'),
|
||||
'Arquivo Geral da Cidade do Rio de Janeiro':
|
||||
('AGCRJ', 'Arquivo Geral da Cidade do Rio de Janeiro', 'Rio de Janeiro (RJ)'),
|
||||
'Leibniz-Institut fuer Laenderkunde, Leipzig':
|
||||
('IFL', 'Leibniz-Institut für Länderkunde', 'Leipzig (Alemanha)'),
|
||||
'Instituto Historico e Geografico Brasileiro':
|
||||
('IHGB', 'Instituto Histórico e Geográfico Brasileiro', 'Rio de Janeiro (RJ)'),
|
||||
'Fundacao Joaquim Nabuco': ('FUNDAJ', 'Fundação Joaquim Nabuco', 'Recife (PE)'),
|
||||
'Museu Paraense Emilio Goeldi': ('MPEG', 'Museu Paraense Emílio Goeldi', 'Belém (PA)'),
|
||||
'Escola de Ciencias Sociais (FGV CPDOC)':
|
||||
('CPDOC', 'Fundação Getulio Vargas. Escola de Ciências Sociais (FGV CPDOC)', 'Rio de Janeiro (RJ)'),
|
||||
'Instituto Brasileiro de Geografia e Estatistica':
|
||||
('IBGE', 'Instituto Brasileiro de Geografia e Estatística', 'Rio de Janeiro (RJ)'),
|
||||
}
|
||||
|
||||
# 4.1 — obrigatório nos níveis 0 e 1
|
||||
ACESSO = ('Sem restrição de acesso à descrição. As cópias digitais são mantidas '
|
||||
f'para preservação em {BUCKET} e consultáveis pelo mural em {ENTIDADE_SITE}. '
|
||||
'O acesso aos originais segue as regras de cada instituição de origem '
|
||||
'(ver 5.1 em cada dossiê).')
|
||||
|
||||
|
||||
def codigo(*partes: str) -> str:
|
||||
"""BR RJXYZ, BR RJXYZ.BF, BR RJXYZ.BF.IMS.GF.12345.01 …"""
|
||||
return f'{PAIS} ' + '.'.join((ENTIDADE,) + tuple(p for p in partes if p))
|
||||
@@ -0,0 +1,333 @@
|
||||
"""Normalização de dc.date para o elemento 1.3 da NOBRADE.
|
||||
|
||||
Saída de `interpreta(texto)`:
|
||||
forma — como a NOBRADE manda exibir: `1914`, `[1860]`, `[1902?]`,
|
||||
`[ca. 1905]`, `[193-]`, `[18--?]`, `8/2/1890-18/4/1890`
|
||||
iso — ISO 8601-2 (EDTF), para indexação e intercâmbio (proc. 1.3.2.7):
|
||||
`1914`, `1902?`, `1905~`, `193X`, `18XX?`, `1931/1937`, `../1921`
|
||||
inicio, fim — anos inteiros (ou None) para filtrar e ordenar
|
||||
qualidade — `exata` | `atribuida` | `incerta` | `aproximada`
|
||||
tipo — `produção` (padrão) ou `publicação`
|
||||
motivo — quando não deu para interpretar: `sem data`, `registro de sistema`,
|
||||
`ilegível`
|
||||
|
||||
A regra geral: tira do texto as marcas (colchetes, `?`, `circa`, qualificadores
|
||||
entre parênteses), interpreta o que sobra como ponto ou intervalo e remonta a
|
||||
forma com as marcas no padrão da norma (proc. 1.3.2.1–1.3.2.4).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
MESES = {
|
||||
'jan': 1, 'janeiro': 1, 'fev': 2, 'fevereiro': 2, 'mar': 3, 'março': 3, 'marco': 3,
|
||||
'abr': 4, 'abril': 4, 'mai': 5, 'maio': 5, 'jun': 6, 'junho': 6,
|
||||
'jul': 7, 'julho': 7, 'ago': 8, 'agosto': 8, 'set': 9, 'setembro': 9,
|
||||
'out': 10, 'outubro': 10, 'nov': 11, 'novembro': 11, 'dez': 12, 'dezembro': 12,
|
||||
}
|
||||
MES_RE = '(' + '|'.join(sorted(MESES, key=len, reverse=True)) + r')\.?'
|
||||
|
||||
SEM_DATA = re.compile(r'^(s\.?\s*/?\s*d\.?|s/data|sem data|s\.?\s*n\.?|n[aã]o identificad[ao])$', re.I)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ponto:
|
||||
ano: int
|
||||
mes: int | None = None
|
||||
dia: int | None = None
|
||||
prec: str = 'ano' # dia | mes | ano | decada | seculo
|
||||
|
||||
def forma(self) -> str:
|
||||
if self.prec == 'dia':
|
||||
return f'{self.dia}/{self.mes}/{self.ano}'
|
||||
if self.prec == 'mes':
|
||||
return f'{self.mes}/{self.ano}'
|
||||
if self.prec == 'decada':
|
||||
return f'{self.ano // 10}-'
|
||||
if self.prec == 'seculo':
|
||||
return f'{self.ano // 100}--'
|
||||
return str(self.ano)
|
||||
|
||||
def iso(self) -> str:
|
||||
if self.prec == 'dia':
|
||||
return f'{self.ano:04d}-{self.mes:02d}-{self.dia:02d}'
|
||||
if self.prec == 'mes':
|
||||
return f'{self.ano:04d}-{self.mes:02d}'
|
||||
if self.prec == 'decada':
|
||||
return f'{self.ano // 10}X'
|
||||
if self.prec == 'seculo':
|
||||
return f'{self.ano // 100}XX'
|
||||
return f'{self.ano:04d}'
|
||||
|
||||
def limites(self) -> tuple[int, int]:
|
||||
if self.prec == 'decada':
|
||||
return self.ano, self.ano + 9
|
||||
if self.prec == 'seculo':
|
||||
return self.ano, self.ano + 99
|
||||
return self.ano, self.ano
|
||||
|
||||
|
||||
@dataclass
|
||||
class Data:
|
||||
original: str
|
||||
inicio_p: Ponto | None = None
|
||||
fim_p: Ponto | None = None
|
||||
relacao: str = '' # '' | 'antes' | 'depois'
|
||||
colchete: bool = False
|
||||
incerta: bool = False
|
||||
aproximada: bool = False
|
||||
tipo: str = 'produção'
|
||||
motivo: str = ''
|
||||
notas: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return self.inicio_p is not None
|
||||
|
||||
@property
|
||||
def qualidade(self) -> str:
|
||||
if self.aproximada:
|
||||
return 'aproximada'
|
||||
if self.incerta:
|
||||
return 'incerta'
|
||||
if self.colchete:
|
||||
return 'atribuida'
|
||||
return 'exata'
|
||||
|
||||
def forma(self) -> str:
|
||||
if not self.ok:
|
||||
return '[s.d.]'
|
||||
a = self.inicio_p.forma()
|
||||
if self.relacao == 'antes':
|
||||
s = f'antes de {a}'
|
||||
elif self.relacao == 'depois':
|
||||
s = f'depois de {a}'
|
||||
elif self.fim_p and self.fim_p != self.inicio_p:
|
||||
s = f'{a}-{self.fim_p.forma()}'
|
||||
else:
|
||||
s = a
|
||||
if self.aproximada:
|
||||
s = 'ca. ' + s
|
||||
if self.incerta:
|
||||
s += '?'
|
||||
if self.colchete or self.aproximada or self.incerta or self.relacao:
|
||||
s = f'[{s}]'
|
||||
return s
|
||||
|
||||
def iso(self) -> str | None:
|
||||
if not self.ok:
|
||||
return None
|
||||
q = ('%' if self.incerta else '~') if self.aproximada else ('?' if self.incerta else '')
|
||||
a = self.inicio_p.iso() + q
|
||||
if self.relacao == 'antes':
|
||||
return f'../{a}'
|
||||
if self.relacao == 'depois':
|
||||
return f'{a}/..'
|
||||
if self.fim_p and self.fim_p != self.inicio_p:
|
||||
return f'{a}/{self.fim_p.iso()}{q}'
|
||||
return a
|
||||
|
||||
def anos(self) -> tuple[int | None, int | None]:
|
||||
if not self.ok:
|
||||
return None, None
|
||||
i, f = self.inicio_p.limites()
|
||||
if self.fim_p:
|
||||
f = self.fim_p.limites()[1]
|
||||
if self.relacao == 'antes':
|
||||
return None, f
|
||||
if self.relacao == 'depois':
|
||||
return i, None
|
||||
return i, f
|
||||
|
||||
def registro(self) -> dict:
|
||||
i, f = self.anos()
|
||||
return {
|
||||
'original': self.original, 'forma': self.forma(), 'iso': self.iso(),
|
||||
'inicio': i, 'fim': f, 'qualidade': self.qualidade if self.ok else None,
|
||||
'tipo': self.tipo, 'motivo': self.motivo or None,
|
||||
'notas': self.notas or None,
|
||||
}
|
||||
|
||||
|
||||
def _sem_acento(s: str) -> str:
|
||||
return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
|
||||
|
||||
|
||||
def _ano(s: str, base: int | None = None) -> int:
|
||||
n = int(s)
|
||||
if len(s) == 2:
|
||||
return (base // 100 * 100 if base else 1900) + n
|
||||
return n
|
||||
|
||||
|
||||
def _ponto(s: str) -> tuple[Ponto | None, bool]:
|
||||
"""Um ponto no tempo. Devolve (ponto, incerto) — incerto quando o próprio
|
||||
formato carrega a dúvida, como `193?` (década) ou `?/08/1938` (dia)."""
|
||||
s = s.strip(' .,:;')
|
||||
s = re.sub(r'^[-–]\s*', '', s)
|
||||
if not s:
|
||||
return None, False
|
||||
if m := re.fullmatch(r'(\d{4})', s):
|
||||
return Ponto(int(m[1])), False
|
||||
if m := re.fullmatch(r'(\d{3})-', s):
|
||||
return Ponto(int(m[1]) * 10, prec='decada'), False
|
||||
if m := re.fullmatch(r'(\d{3})-?\?', s):
|
||||
return Ponto(int(m[1]) * 10, prec='decada'), True
|
||||
if m := re.fullmatch(r'(\d{2})(?:-{2,3}|_)', s):
|
||||
return Ponto(int(m[1]) * 100, prec='seculo'), False
|
||||
if m := re.fullmatch(r'(\d{2})(?:-{2})?\?', s):
|
||||
return Ponto(int(m[1]) * 100, prec='seculo'), True
|
||||
if m := re.fullmatch(r'(\d{3})--', s): # [186--]: década escrita com traço a mais
|
||||
return Ponto(int(m[1]) * 10, prec='decada'), False
|
||||
# dia/mês/ano, com separadores tortos: 22-12--1938, 14 -07-1928, 05/12-1934
|
||||
if m := re.fullmatch(r'(\d{1,2})\s*[-/.]\s*(\d{1,2})\s*[-/.]+\s*(\d{4}|\d{2})', s):
|
||||
d, mo, a = int(m[1]), int(m[2]), _ano(m[3])
|
||||
if 1 <= mo <= 12 and 1 <= d <= 31:
|
||||
return Ponto(a, mo, d, 'dia'), len(m[3]) == 2
|
||||
if m := re.fullmatch(r'(\d{4})\s*[-/]\s*(\d{1,2})\s*[-/]\s*(\d{1,2})', s):
|
||||
a, mo, d = int(m[1]), int(m[2]), int(m[3])
|
||||
if 1 <= mo <= 12 and 1 <= d <= 31:
|
||||
return Ponto(a, mo, d, 'dia'), False
|
||||
# dia desconhecido: 2-/12/1923, ?/08/1938
|
||||
if m := re.fullmatch(r'(?:\d?-|\?)\s*/\s*(\d{1,2})\s*/\s*(\d{4})', s):
|
||||
return Ponto(int(m[2]), int(m[1]), prec='mes'), False
|
||||
if m := re.fullmatch(r'(\d{1,2})\s*/\s*(\d{4})', s):
|
||||
if 1 <= int(m[1]) <= 12:
|
||||
return Ponto(int(m[2]), int(m[1]), prec='mes'), False
|
||||
if m := re.fullmatch(r'(\d{4})\s*/\s*(\d{1,2})', s):
|
||||
if 1 <= int(m[2]) <= 12:
|
||||
return Ponto(int(m[1]), int(m[2]), prec='mes'), False
|
||||
t = _sem_acento(s.lower())
|
||||
if m := re.fullmatch(rf'(\d{{1,2}})\s*(?:de\s+)?{MES_RE}\s*,?\s*(?:de\s+)?(\d{{4}})', t):
|
||||
return Ponto(int(m[3]), MESES[m[2]], int(m[1]), 'dia'), False
|
||||
if m := re.fullmatch(rf'{MES_RE}\s*,?\s*(?:de\s+)?(\d{{4}})', t):
|
||||
return Ponto(int(m[2]), MESES[m[1]], prec='mes'), False
|
||||
if m := re.fullmatch(r'(?:decada de )?(\d{3})\d(?: decada)?( provavel)?', t):
|
||||
if 'decada' in t:
|
||||
return Ponto(int(m[1]) * 10, prec='decada'), bool(m[2])
|
||||
if m := re.fullmatch(r'(\d{2}) seculo', t):
|
||||
return Ponto((int(m[1]) - 1) * 100, prec='seculo'), False
|
||||
return None, False
|
||||
|
||||
|
||||
def _intervalo(s: str) -> tuple[Ponto | None, Ponto | None, bool]:
|
||||
t = _sem_acento(s.lower()).strip(' .,:;')
|
||||
t = re.sub(r'^entre\s+', '', t)
|
||||
p, inc = _ponto(t)
|
||||
if p:
|
||||
return p, None, inc
|
||||
# dias ou meses coordenados: "15 e 16 de outubro de 1930", "outubro e novembro de 1930"
|
||||
if m := re.fullmatch(rf'(\d{{1,2}})\s+e\s+(\d{{1,2}})\s+(?:de\s+)?{MES_RE}\s+(?:de\s+)?(\d{{4}})', t):
|
||||
mo, a = MESES[m[3]], int(m[4])
|
||||
return Ponto(a, mo, int(m[1]), 'dia'), Ponto(a, mo, int(m[2]), 'dia'), False
|
||||
if m := re.fullmatch(rf'{MES_RE}\s+e\s+{MES_RE}\s+(?:de\s+)?(\d{{4}})', t):
|
||||
a = int(m[3])
|
||||
return Ponto(a, MESES[m[1]], prec='mes'), Ponto(a, MESES[m[2]], prec='mes'), False
|
||||
# 1859-61: segundo ano abreviado
|
||||
if m := re.fullmatch(r'(\d{4})\s*-\s*(\d{2})', t):
|
||||
a = int(m[1])
|
||||
b = _ano(m[2], a)
|
||||
if b > a:
|
||||
return Ponto(a), Ponto(b), False
|
||||
# o traço separa só entre um ano e o início do outro — `193-` é década, não intervalo
|
||||
for sep in (r'\s+e\s+', r'\s+a\s+', r'(?<=[\d?])\s*[-–]\s*(?=\d)', r'\s*/\s*'):
|
||||
partes = re.split(sep, t)
|
||||
if len(partes) != 2:
|
||||
continue
|
||||
# "/" só separa anos inteiros (1952/1953); com meses é data (1910/12)
|
||||
if sep == r'\s*/\s*' and not all(re.fullmatch(r'\d{4}', p) for p in partes):
|
||||
continue
|
||||
a, ia = _ponto(partes[0])
|
||||
b, ib = _ponto(partes[1])
|
||||
if a and b and b.limites()[1] >= a.limites()[0]:
|
||||
return a, b, ia or ib
|
||||
return None, None, False
|
||||
|
||||
|
||||
def interpreta(texto: str | None) -> Data:
|
||||
original = (texto or '').strip()
|
||||
d = Data(original)
|
||||
s = original
|
||||
if not s:
|
||||
d.motivo = 'sem data'
|
||||
return d
|
||||
if re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z', s):
|
||||
# carimbo de registro no DSpace gravado no campo errado — não é produção
|
||||
d.motivo = 'registro de sistema'
|
||||
d.notas.append(f'dc.date traz a data de registro no sistema de origem ({s}), não a de produção')
|
||||
return d
|
||||
|
||||
# partes separadas por ";" com qualificador: fica a de produção, o resto vira nota
|
||||
partes = [p.strip() for p in re.split(r';(?![^(]*\))', s) if p.strip()]
|
||||
if len(partes) > 1:
|
||||
principal = next((p for p in partes if re.search(r'produ', p, re.I)), partes[0])
|
||||
d.notas += [f'dc.date também registra: {p}' for p in partes if p is not principal]
|
||||
s = principal
|
||||
|
||||
if m := re.search(r'\(([^)]*)\)\s*$', s):
|
||||
q = m[1].lower()
|
||||
if 'publica' in q:
|
||||
d.tipo = 'publicação'
|
||||
if 'atribu' in q:
|
||||
d.colchete = True
|
||||
s = s[:m.start()].strip()
|
||||
|
||||
if re.search(r'[\[\]{]', s):
|
||||
d.colchete = True
|
||||
s = re.sub(r'[\[\]{}]', '', s)
|
||||
s = re.sub(r'^\s*-\s*', '', s)
|
||||
s = re.sub(r'^(RJ|[A-Z]{2}),\s*', '', s) # [RJ, ca. 1860]: lugar vai na data tópica
|
||||
s = re.sub(r'(\d)\.(\d{3})\b', r'\1\2', s) # 1.885 → 1885
|
||||
s = re.sub(r'\s*:?\.\s*$', '', s).strip()
|
||||
|
||||
if SEM_DATA.fullmatch(s):
|
||||
d.motivo = 'sem data'
|
||||
return d
|
||||
|
||||
s, n = re.subn(r'\b(circa|cica|cira|ca\.?|c\.)\s*|(?<=\[)c(?=\d)|^c(?=\d)', '', s, flags=re.I)
|
||||
if n:
|
||||
d.aproximada = True
|
||||
s, n = re.subn(r'\s*\b(anterior|antes de)\b\s*', ' ', s, flags=re.I)
|
||||
if n:
|
||||
d.relacao = 'antes'
|
||||
s, n = re.subn(r'\s*\b(posterior|depois de)\b\s*', ' ', s, flags=re.I)
|
||||
if n:
|
||||
d.relacao = 'depois'
|
||||
s = s.strip()
|
||||
# interrogação no fim vale para a data inteira; dentro de um ponto (193?) é o ponto que diz
|
||||
if re.search(r'\d{4}\s*\?\s*$', s) or re.search(r'\d{4}\?\s*\]?$', original):
|
||||
d.incerta = True
|
||||
s = re.sub(r'\s*\?\s*$', '', s)
|
||||
|
||||
# "Foto aérea da Fábrica ... - SP - 23-06-1933": só a data do fim interessa
|
||||
if not re.match(r'[\d?]', s) and not re.match(rf'(?i){MES_RE}|entre|decada|década', _sem_acento(s)):
|
||||
if m := re.search(r'(\d{1,2}-\d{1,2}-\d{4}|\d{4})\s*$', s):
|
||||
d.notas.append(f'data extraída do texto: {original}')
|
||||
s = m[1]
|
||||
|
||||
p, inc = _ponto(s)
|
||||
if p:
|
||||
d.inicio_p, d.incerta = p, d.incerta or inc
|
||||
return d
|
||||
a, b, inc = _intervalo(s)
|
||||
if a:
|
||||
d.inicio_p, d.fim_p, d.incerta = a, b, d.incerta or inc
|
||||
return d
|
||||
# "1900-" (colchete aberto): dali em diante
|
||||
if m := re.fullmatch(r'(\d{4})\s*-', s):
|
||||
d.inicio_p, d.relacao = Ponto(int(m[1])), 'depois'
|
||||
return d
|
||||
d.motivo = 'ilegível'
|
||||
return d
|
||||
|
||||
|
||||
def atribuida(inicio: int, fim: int, base: str) -> dict:
|
||||
"""Data atribuída pelo arquivista a partir de outra fonte (proc. 1.3.3.2)."""
|
||||
d = Data('', Ponto(inicio), Ponto(fim) if fim != inicio else None, incerta=True, colchete=True)
|
||||
r = d.registro()
|
||||
r['original'] = None
|
||||
r['notas'] = [f'data atribuída pelo arquivista: {base}']
|
||||
return r
|
||||
@@ -0,0 +1,352 @@
|
||||
"""Exportação da descrição: EAD 3 (descrições), EAC-CPF (autoridades) e CSV
|
||||
de importação do AtoM (modelo ISAD). A NOBRADE não impõe formato; recomenda o
|
||||
EAD para intercâmbio (nota 13).
|
||||
|
||||
Correspondência NOBRADE → EAD 3:
|
||||
1.1 unitid · 1.2 unittitle · 1.3 unitdate (@normal em ISO 8601-2) ·
|
||||
1.4 @level · 1.5 physdesc + physdescstructured · 2.1 origination ·
|
||||
2.2 bioghist · 2.3 custodhist · 2.4 acqinfo · 3.1 scopecontent ·
|
||||
3.2 appraisal · 3.3 accruals · 3.4 arrangement · 4.1 accessrestrict ·
|
||||
4.2 userestrict · 4.3 langmaterial · 4.4 phystech · 4.5 otherfindaid ·
|
||||
5.1 originalsloc · 5.2 altformavail + dao · 5.3 relatedmaterial ·
|
||||
5.4 bibliography · 6.1 odd[@localtype=conservacao] · 6.2 odd ·
|
||||
7.1 processinfo · 7.2 control/conventiondeclaration · 7.3 control/maintenancehistory ·
|
||||
8.1 controlaccess
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
|
||||
from .config import ENTIDADE, ENTIDADE_NOME, PAIS, REGRAS, SAIDA
|
||||
|
||||
EAD = 'http://ead3.archivists.org/schema/'
|
||||
CPF = 'urn:isbn:1-931666-33-4'
|
||||
XLINK = 'http://www.w3.org/1999/xlink'
|
||||
NIVEL_EAD = {1: ('collection', None), 2: ('otherlevel', 'seção'), 3: ('series', None),
|
||||
4: ('file', None), 5: ('item', None)}
|
||||
HOJE = date.today().isoformat()
|
||||
|
||||
|
||||
# ── texto plano de cada elemento (usado por EAD, AtoM e mural) ──────────────
|
||||
def texto(u: dict, el: str) -> str | None:
|
||||
v = u.get(el)
|
||||
if v is None:
|
||||
return None
|
||||
if el == '1.2':
|
||||
return v['titulo']
|
||||
if el == '1.3':
|
||||
p = v.get('producao') or {}
|
||||
return p.get('forma')
|
||||
if el == '1.5':
|
||||
return v.get('texto')
|
||||
if el == '2.1':
|
||||
return '; '.join(p['forma'] for p in v)
|
||||
if el == '4.2':
|
||||
return v['texto'] if isinstance(v, dict) else v
|
||||
if el == '4.3':
|
||||
return '; '.join(i['nome'] for i in v) if isinstance(v, list) else v
|
||||
if el == '5.1':
|
||||
return v.get('texto') or v.get('instituicao') if isinstance(v, dict) else v
|
||||
if el == '5.2':
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
partes = [f"{c['tipo']}: {c['chave']} (SHA-256 {c['sha256']})" for c in v.get('nesta_entidade') or []]
|
||||
partes += [f"{c['entidade']}: {c['url']}" for c in v.get('em_outras_entidades') or []]
|
||||
return '; '.join(partes) or None
|
||||
if el in ('5.3', '5.4', '6.2') and isinstance(v, list):
|
||||
return '\n'.join(v)
|
||||
if el == '7.3':
|
||||
return '; '.join(f'{k}: {x}' for k, x in v.items())
|
||||
if el == '8.1' and isinstance(v, dict):
|
||||
return None
|
||||
return v if isinstance(v, str) else None
|
||||
|
||||
|
||||
# ── EAD 3 ────────────────────────────────────────────────────────────────────
|
||||
def _sub(pai, tag, txt=None, **attrs):
|
||||
e = ET.SubElement(pai, f'{{{EAD}}}{tag}', {k.rstrip('_'): str(x) for k, x in attrs.items() if x is not None})
|
||||
if txt is not None:
|
||||
e.text = txt
|
||||
return e
|
||||
|
||||
|
||||
def _paragrafos(pai, tag, txt, head=None, **attrs):
|
||||
if not txt:
|
||||
return
|
||||
e = _sub(pai, tag, **attrs)
|
||||
if head:
|
||||
_sub(e, 'head', head)
|
||||
for par in [p.strip() for p in re.split(r'\n\s*\n|\n(?=[-\d])', txt) if p.strip()]:
|
||||
_sub(e, 'p', re.sub(r'\s+', ' ', par))
|
||||
|
||||
|
||||
def _nome(pai, forma: str, aid: str | None, tipo: str | None, relator=None):
|
||||
tag = {'entidade coletiva': 'corpname', 'família': 'famname'}.get(tipo, 'persname')
|
||||
e = _sub(pai, tag, identifier=aid, relator=relator)
|
||||
_sub(e, 'part', forma)
|
||||
return e
|
||||
|
||||
|
||||
def _componente(pai, u: dict, aut: dict, tag='c'):
|
||||
nivel, outro = NIVEL_EAD[u['nivel']]
|
||||
c = pai if tag == 'archdesc' else _sub(pai, 'c', level=nivel, otherlevel=outro, id=_xmlid(u['id']))
|
||||
did = _sub(c, 'did')
|
||||
_sub(did, 'unitid', u['1.1'], countrycode=PAIS, repositorycode=f'{PAIS}-{ENTIDADE}')
|
||||
_sub(did, 'unittitle', u['1.2']['titulo'])
|
||||
p = u['1.3'].get('producao') or {}
|
||||
cert = {'aproximada': 'approximate', 'incerta': 'uncertain', 'atribuida': 'inferred',
|
||||
'atribuída pelo arquivista': 'inferred'}.get(p.get('qualidade'))
|
||||
_sub(did, 'unitdate', p.get('forma') or '[s.d.]', normal=p.get('iso'),
|
||||
datechar='publication' if p.get('tipo') == 'publicação' else 'creation', certainty=cert,
|
||||
unitdatetype='inclusive' if u['nivel'] < 4 else None)
|
||||
orig = _sub(did, 'origination')
|
||||
for pr in u['2.1']:
|
||||
_nome(orig, pr['forma'], pr.get('id'), (aut.get(pr.get('id')) or {}).get('tipo'),
|
||||
relator=pr.get('funcao'))
|
||||
dim = u['1.5']
|
||||
_sub(did, 'physdesc', dim['texto'])
|
||||
for esp, n in (dim.get('por_especie') or {}).items():
|
||||
ps = _sub(did, 'physdescstructured', physdescstructuredtype='materialtype', coverage='part')
|
||||
_sub(ps, 'quantity', str(n))
|
||||
_sub(ps, 'unittype', esp)
|
||||
if isinstance(u.get('4.3'), list):
|
||||
lm = _sub(did, 'langmaterial')
|
||||
for i in u['4.3']:
|
||||
_sub(lm, 'language', i['nome'], langcode=i['codigo'])
|
||||
if u['nivel'] == 5:
|
||||
for cp in (u.get('5.2') or {}).get('nesta_entidade') or []:
|
||||
_sub(did, 'dao', daotype='derived', href=cp['chave'], linktitle=cp['tipo'],
|
||||
identifier=f"sha256:{cp['sha256']}")
|
||||
|
||||
_paragrafos(c, 'bioghist', u.get('2.2'))
|
||||
_paragrafos(c, 'custodhist', u.get('2.3'))
|
||||
_paragrafos(c, 'acqinfo', u.get('2.4'))
|
||||
_paragrafos(c, 'scopecontent', u.get('3.1'))
|
||||
_paragrafos(c, 'appraisal', u.get('3.2'))
|
||||
_paragrafos(c, 'accruals', u.get('3.3'))
|
||||
_paragrafos(c, 'arrangement', u.get('3.4'))
|
||||
_paragrafos(c, 'accessrestrict', u.get('4.1'))
|
||||
_paragrafos(c, 'userestrict', texto(u, '4.2'))
|
||||
_paragrafos(c, 'phystech', u.get('4.4'))
|
||||
_paragrafos(c, 'otherfindaid', u.get('4.5'))
|
||||
_paragrafos(c, 'originalsloc', texto(u, '5.1'))
|
||||
if u['nivel'] != 5:
|
||||
_paragrafos(c, 'altformavail', texto(u, '5.2'))
|
||||
if isinstance(u.get('5.3'), list):
|
||||
rm = _sub(c, 'relatedmaterial')
|
||||
for x in u['5.3']:
|
||||
_sub(rm, 'p', x)
|
||||
if isinstance(u.get('5.4'), list):
|
||||
bib = _sub(c, 'bibliography')
|
||||
for x in u['5.4']:
|
||||
_sub(bib, 'bibref', x)
|
||||
elif u.get('5.4'):
|
||||
_paragrafos(c, 'bibliography', u['5.4'])
|
||||
_paragrafos(c, 'odd', u.get('6.1'), head='Notas sobre conservação', localtype='conservacao')
|
||||
_paragrafos(c, 'odd', texto(u, '6.2'), head='Notas gerais')
|
||||
_paragrafos(c, 'processinfo', u.get('7.1'))
|
||||
|
||||
ca = u.get('8.1')
|
||||
if isinstance(ca, dict):
|
||||
ctl = _sub(c, 'controlaccess')
|
||||
for s in ca.get('assuntos') or []:
|
||||
_sub(_sub(ctl, 'subject', lang='por'), 'part', s)
|
||||
for s in ca.get('assuntos_eng') or []:
|
||||
_sub(_sub(ctl, 'subject', lang='eng'), 'part', s)
|
||||
if u['1.3'].get('topica'):
|
||||
_sub(_sub(ctl, 'geogname', localtype='data tópica'), 'part', u['1.3']['topica'])
|
||||
for s in ca.get('lugares') or []:
|
||||
_sub(_sub(ctl, 'geogname'), 'part', s)
|
||||
for pr in u['2.1']:
|
||||
if pr.get('id'):
|
||||
_nome(ctl, pr['forma'], pr['id'], (aut.get(pr['id']) or {}).get('tipo'))
|
||||
if len(ctl) == 0:
|
||||
c.remove(ctl)
|
||||
elif ca:
|
||||
_paragrafos(c, 'processinfo', ca, head='Pontos de acesso e indexação')
|
||||
return c
|
||||
|
||||
|
||||
def _xmlid(cod: str) -> str:
|
||||
return 'u-' + re.sub(r'[^\w.-]', '_', cod)
|
||||
|
||||
|
||||
def ead(unidades: list[dict], aut: dict):
|
||||
filhos = defaultdict(list)
|
||||
por_id = {}
|
||||
for u in unidades:
|
||||
por_id[u['id']] = u
|
||||
if u.get('pai'):
|
||||
filhos[u['pai']].append(u['id'])
|
||||
col = next(u for u in unidades if u['nivel'] == 1)
|
||||
|
||||
ET.register_namespace('', EAD)
|
||||
raiz = ET.Element(f'{{{EAD}}}ead')
|
||||
ctrl = _sub(raiz, 'control', langencoding='iso639-2b', scriptencoding='iso15924',
|
||||
dateencoding='iso8601', countryencoding='iso3166-1', repositoryencoding='iso15511')
|
||||
_sub(ctrl, 'recordid', col['id'])
|
||||
fd = _sub(ctrl, 'filedesc')
|
||||
ts = _sub(fd, 'titlestmt')
|
||||
_sub(ts, 'titleproper', col['1.2']['titulo'])
|
||||
_sub(ctrl, 'maintenancestatus', value='new')
|
||||
ag = _sub(ctrl, 'maintenanceagency', countrycode=PAIS)
|
||||
_sub(ag, 'agencycode', f'{PAIS}-{ENTIDADE}')
|
||||
_sub(ag, 'agencyname', ENTIDADE_NOME)
|
||||
ld = _sub(ctrl, 'languagedeclaration')
|
||||
_sub(ld, 'language', 'português', langcode='por')
|
||||
_sub(ld, 'script', 'latino', scriptcode='Latn')
|
||||
cd = _sub(ctrl, 'conventiondeclaration')
|
||||
_sub(cd, 'citation', REGRAS)
|
||||
mh = _sub(ctrl, 'maintenancehistory')
|
||||
ev = _sub(mh, 'maintenanceevent')
|
||||
_sub(ev, 'eventtype', value='created')
|
||||
_sub(ev, 'eventdatetime', HOJE, standarddatetime=HOJE)
|
||||
_sub(ev, 'agenttype', value='machine')
|
||||
_sub(ev, 'agent', 'pipeline/nobrade')
|
||||
|
||||
arch = _sub(raiz, 'archdesc', level='collection')
|
||||
_componente(arch, col, aut, tag='archdesc')
|
||||
rep = arch.find(f'{{{EAD}}}did')
|
||||
r = _sub(rep, 'repository')
|
||||
_sub(_sub(r, 'corpname', identifier=f'{PAIS}-{ENTIDADE}'), 'part', ENTIDADE_NOME)
|
||||
dsc = _sub(arch, 'dsc')
|
||||
|
||||
def desce(pai_xml, cod):
|
||||
for fid in sorted(filhos[cod], key=lambda x: (len(x), x)):
|
||||
c = _componente(pai_xml, por_id[fid], aut)
|
||||
desce(c, fid)
|
||||
desce(dsc, col['id'])
|
||||
|
||||
(SAIDA / 'ead').mkdir(exist_ok=True)
|
||||
ET.indent(raiz, space=' ')
|
||||
ET.ElementTree(raiz).write(SAIDA / 'ead' / 'brasiliana.xml', encoding='utf-8', xml_declaration=True)
|
||||
|
||||
|
||||
# ── EAC-CPF ──────────────────────────────────────────────────────────────────
|
||||
def _c(pai, tag, txt=None, **attrs):
|
||||
e = ET.SubElement(pai, f'{{{CPF}}}{tag}', {k.rstrip('_'): str(x) for k, x in attrs.items() if x is not None})
|
||||
if txt is not None:
|
||||
e.text = txt
|
||||
return e
|
||||
|
||||
|
||||
def eac(autoridades: list[dict], usos: dict[str, list[str]]):
|
||||
ET.register_namespace('', CPF)
|
||||
ET.register_namespace('xlink', XLINK)
|
||||
pasta = SAIDA / 'eac'
|
||||
pasta.mkdir(exist_ok=True)
|
||||
for a in autoridades:
|
||||
raiz = ET.Element(f'{{{CPF}}}eac-cpf')
|
||||
ctrl = _c(raiz, 'control')
|
||||
_c(ctrl, 'recordId', f"{PAIS}-{ENTIDADE}-{a['id']}")
|
||||
_c(ctrl, 'maintenanceStatus', 'new')
|
||||
ag = _c(ctrl, 'maintenanceAgency')
|
||||
_c(ag, 'agencyCode', f'{PAIS}-{ENTIDADE}')
|
||||
_c(ag, 'agencyName', ENTIDADE_NOME)
|
||||
ld = _c(ctrl, 'languageDeclaration')
|
||||
_c(ld, 'language', 'português', languageCode='por')
|
||||
_c(ld, 'script', 'latino', scriptCode='Latn')
|
||||
cd = _c(ctrl, 'conventionDeclaration')
|
||||
_c(cd, 'citation', 'ISAAR(CPF) 2ª ed.; NOBRADE (CONARQ, 2006), proc. 8.1.3–8.1.4')
|
||||
mh = _c(ctrl, 'maintenanceHistory')
|
||||
ev = _c(mh, 'maintenanceEvent')
|
||||
_c(ev, 'eventType', 'created')
|
||||
_c(ev, 'eventDateTime', HOJE, standardDateTime=HOJE)
|
||||
_c(ev, 'agentType', 'machine')
|
||||
_c(ev, 'agent', 'pipeline/nobrade')
|
||||
if a['fontes']:
|
||||
src = _c(ctrl, 'sources')
|
||||
for f in a['fontes']:
|
||||
s = _c(src, 'source')
|
||||
_c(s, 'sourceEntry', f)
|
||||
|
||||
cpf = _c(raiz, 'cpfDescription')
|
||||
ident = _c(cpf, 'identity')
|
||||
_c(ident, 'entityType', {'entidade coletiva': 'corporateBody', 'família': 'family'}.get(a['tipo'], 'person'))
|
||||
ne = _c(ident, 'nameEntry', localType='autorizada')
|
||||
_c(ne, 'part', a['nome'])
|
||||
if a['datas_existencia']:
|
||||
_c(ne, 'useDates').append(_data_eac(a['datas_existencia']))
|
||||
_c(ne, 'authorizedForm', 'NOBRADE')
|
||||
for v in a['variantes']:
|
||||
nv = _c(ident, 'nameEntry', localType='variante')
|
||||
_c(nv, 'part', v)
|
||||
_c(nv, 'alternativeForm', 'NOBRADE')
|
||||
desc = _c(cpf, 'description')
|
||||
if a['datas_existencia']:
|
||||
ex = _c(desc, 'existDates')
|
||||
ex.append(_data_eac(a['datas_existencia']))
|
||||
bh = _c(desc, 'biogHist')
|
||||
_c(bh, 'p', f"Autor de {a['ocorrencias']} dossiês da Coleção Brasiliana Fotográfica (cópia digital)."
|
||||
+ (f" Tipo presumido: {a['tipo']}." if a['tipo'] == 'pessoa ou entidade' else ''))
|
||||
if usos.get(a['id']):
|
||||
rel = _c(cpf, 'relations')
|
||||
for cod in usos[a['id']][:500]:
|
||||
r = _c(rel, 'resourceRelation', resourceRelationType='creatorOf')
|
||||
_c(r, 'relationEntry', cod)
|
||||
ET.indent(raiz, space=' ')
|
||||
ET.ElementTree(raiz).write(pasta / f"{a['id']}.xml", encoding='utf-8', xml_declaration=True)
|
||||
|
||||
|
||||
def _data_eac(datas: str):
|
||||
anos = re.findall(r'\d{4}', datas)
|
||||
if len(anos) >= 2:
|
||||
e = ET.Element(f'{{{CPF}}}dateRange')
|
||||
_c(e, 'fromDate', datas.split('-')[0].strip(), standardDate=anos[0])
|
||||
_c(e, 'toDate', datas.split('-')[-1].strip(), standardDate=anos[1])
|
||||
return e
|
||||
e = ET.Element(f'{{{CPF}}}date', standardDate=anos[0] if anos else None)
|
||||
e.text = datas
|
||||
return e
|
||||
|
||||
|
||||
# ── AtoM (modelo CSV ISAD da versão 2.x) ────────────────────────────────────
|
||||
ATOM = ['legacyId', 'parentId', 'identifier', 'title', 'levelOfDescription', 'extentAndMedium',
|
||||
'repository', 'archivalHistory', 'acquisition', 'scopeAndContent', 'appraisal', 'accruals',
|
||||
'arrangement', 'accessConditions', 'reproductionConditions', 'language', 'physicalCharacteristics',
|
||||
'findingAids', 'locationOfOriginals', 'locationOfCopies', 'relatedUnitsOfDescription',
|
||||
'publicationNote', 'generalNote', 'subjectAccessPoints', 'placeAccessPoints', 'nameAccessPoints',
|
||||
'descriptionIdentifier', 'rules', 'archivistNote', 'eventDates', 'eventTypes', 'eventStartDates',
|
||||
'eventEndDates', 'eventActors', 'culture']
|
||||
NIVEL_ATOM = {1: 'Coleção', 2: 'Seção', 3: 'Série', 4: 'Dossiê', 5: 'Item documental'}
|
||||
|
||||
|
||||
def atom(unidades: list[dict]):
|
||||
with (SAIDA / 'atom.csv').open('w', encoding='utf-8', newline='') as f:
|
||||
w = csv.DictWriter(f, fieldnames=ATOM)
|
||||
w.writeheader()
|
||||
for u in unidades:
|
||||
if u['nivel'] == 0:
|
||||
continue
|
||||
p = u['1.3'].get('producao') or {}
|
||||
ca = u.get('8.1') if isinstance(u.get('8.1'), dict) else {}
|
||||
w.writerow({
|
||||
'legacyId': u['id'], 'parentId': u['pai'] if u['nivel'] > 1 else '',
|
||||
'identifier': u['1.1'].split('.')[-1] if u['nivel'] > 1 else u['1.1'],
|
||||
'title': u['1.2']['titulo'], 'levelOfDescription': NIVEL_ATOM[u['nivel']],
|
||||
'extentAndMedium': texto(u, '1.5'), 'repository': ENTIDADE_NOME,
|
||||
'archivalHistory': u.get('2.3'), 'acquisition': u.get('2.4'),
|
||||
'scopeAndContent': u.get('3.1'), 'appraisal': u.get('3.2'), 'accruals': u.get('3.3'),
|
||||
'arrangement': u.get('3.4'), 'accessConditions': u.get('4.1'),
|
||||
'reproductionConditions': texto(u, '4.2'),
|
||||
'language': '|'.join(i['codigo'] for i in u['4.3']) if isinstance(u.get('4.3'), list) else '',
|
||||
'physicalCharacteristics': u.get('4.4'), 'findingAids': u.get('4.5'),
|
||||
'locationOfOriginals': texto(u, '5.1'), 'locationOfCopies': texto(u, '5.2'),
|
||||
'relatedUnitsOfDescription': texto(u, '5.3'), 'publicationNote': texto(u, '5.4'),
|
||||
'generalNote': '\n'.join(x for x in [u.get('6.1'), texto(u, '6.2')] if x),
|
||||
'subjectAccessPoints': '|'.join(ca.get('assuntos') or []),
|
||||
'placeAccessPoints': '|'.join(ca.get('lugares') or []),
|
||||
'nameAccessPoints': '|'.join(ca.get('nomes') or []),
|
||||
'descriptionIdentifier': u['id'], 'rules': u.get('7.2') or '',
|
||||
'archivistNote': u.get('7.1'),
|
||||
'eventDates': p.get('forma'), 'eventTypes': 'Produção',
|
||||
'eventStartDates': f"{p['inicio']}-01-01" if p.get('inicio') else '',
|
||||
'eventEndDates': f"{p['fim']}-12-31" if p.get('fim') else '',
|
||||
'eventActors': '|'.join(x['forma'] for x in u['2.1'] if x.get('id')),
|
||||
'culture': 'pt_BR',
|
||||
})
|
||||
@@ -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
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Leva a descrição NOBRADE para o mural: grava no `meta` de cada peça do
|
||||
web/atlas/atlas-map.json o código de referência, a data normalizada, a série
|
||||
e o suporte, sem re-renderizar os atlas .webp.
|
||||
|
||||
python -m nobrade.mural (de dentro de pipeline/, depois de python -m nobrade)
|
||||
|
||||
Campos novos em meta (curtos: o mapa inteiro desce para o navegador):
|
||||
ref código de referência (1.1) dt data na forma da NOBRADE (1.3)
|
||||
ano ano inicial, para o histograma si índice em `series` do mapa (nível 3)
|
||||
sup espécie e suporte (1.5) rep categoria de reprodução (4.2)
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from .config import RAIZ, SAIDA
|
||||
|
||||
MAPA = RAIZ / 'web' / 'atlas' / 'atlas-map.json'
|
||||
|
||||
|
||||
def main():
|
||||
series, dossies = {}, {}
|
||||
for linha in (SAIDA / 'unidades.jsonl').open(encoding='utf-8'):
|
||||
u = json.loads(linha)
|
||||
if u['nivel'] == 3:
|
||||
series[u['id']] = u['1.2']['titulo']
|
||||
elif u['nivel'] == 4:
|
||||
dossies[u['handle']] = u
|
||||
|
||||
d = json.loads(MAPA.read_text(encoding='utf-8'))
|
||||
titulos = sorted(set(series.values()), key=str.casefold)
|
||||
idx = {t: i for i, t in enumerate(titulos)}
|
||||
d['series'] = titulos
|
||||
feitos = 0
|
||||
for h, e in d['map'].items():
|
||||
u = dossies.get(h)
|
||||
if not u:
|
||||
continue
|
||||
p = u['1.3']['producao']
|
||||
dim = u['1.5']
|
||||
sup = dim['especie'] + (f" {dim['cor']}" if dim.get('cor') else '') + (f", {dim['suporte']}" if dim.get('suporte') else '')
|
||||
e['meta'].update({
|
||||
'ref': u['1.1'], 'dt': p.get('forma'), 'ano': p.get('inicio') or p.get('fim'),
|
||||
'si': idx[series[u['pai']]], 'sup': sup, 'rep': u['4.2']['categoria'],
|
||||
})
|
||||
feitos += 1
|
||||
MAPA.write_text(json.dumps(d, ensure_ascii=False, separators=(',', ':')), encoding='utf-8')
|
||||
print(f'{feitos} de {len(d["map"])} peças com descrição NOBRADE; {len(titulos)} séries')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Nível 3 — séries a partir de `dc.source`.
|
||||
|
||||
O campo traz, misturados, o nome da coleção/fundo de origem ("Coleção Gilberto
|
||||
Ferrez" e "Gilberto Ferrez"), sufixos da instituição ("… / Museu da
|
||||
República"), notações de caixa ("CX 14") e lixo de extração. A regra:
|
||||
|
||||
1. limpa o valor até uma chave (sem prefixo Coleção/Fundo/Arquivo, sem sufixo
|
||||
institucional, sem notação no fim, sem acento, sem pontuação);
|
||||
2. chaves com pelo menos MINIMO itens viram série;
|
||||
3. valor raro que contém a chave de uma série vai para ela (os "Negativo de
|
||||
vidro MPEG639, Coleção Fotográfica do Museu Paraense…");
|
||||
4. o resto vai para a série AV (Avulsos) da instituição, e o valor original
|
||||
fica como notação na origem (5.1).
|
||||
|
||||
Correções manuais em `ajustes/series.tsv` (instituicao, dc.source, titulo)
|
||||
prevalecem sobre a regra.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import re
|
||||
import unicodedata
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
from .config import AJUSTES
|
||||
|
||||
MINIMO = 3
|
||||
|
||||
# variantes que a limpeza não junta sozinha: chave → chave canônica
|
||||
ALIAS = {
|
||||
'd teresa cristina maria': 'thereza christina maria',
|
||||
'teresa cristina maria': 'thereza christina maria',
|
||||
'federacao brasileira para o progresso feminino fbpf': 'federacao brasileira pelo progresso feminino',
|
||||
'federacao brasileira para o progresso feminino': 'federacao brasileira pelo progresso feminino',
|
||||
'fotografica mpeg': 'fotografica do museu paraense emilio goeldi',
|
||||
'brascan cem anos no brasil': 'brascan cem anos no brasil',
|
||||
'escola de ciencias sociais fgv cpdoc': 'escola de ciencias sociais fgv cpdoc',
|
||||
'escola de ciencias sociais': 'escola de ciencias sociais fgv cpdoc',
|
||||
'dos eventos institucionais': 'eventos institucionais',
|
||||
'mestres do sec xix': 'mestres do seculo xix',
|
||||
'cristovao barcellos': 'cristovao barcelos',
|
||||
}
|
||||
|
||||
# notação de caixa/negativo/foto: é localização na origem (5.1), não série
|
||||
NOTACAO = re.compile(r'(?i)^\s*(cx|caixas?|nv|foto|negativo|estampa)\b')
|
||||
|
||||
PREFIXO = re.compile(r'^(convenio|colecao particular|colecao|fundo|arquivo|acervo|serie)\s+(d[aeo]s?\s+)?')
|
||||
LIXO = re.compile(r'[<>{}]|ns:|^[0-9a-f]{12,}')
|
||||
|
||||
|
||||
def sem_acento(s: str) -> str:
|
||||
return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
|
||||
|
||||
|
||||
def limpa(v: str) -> str:
|
||||
s = v.strip()
|
||||
s = re.sub(r'\s*/\s*(acervo|museu|ibge|instituto moreira).*$', '', s, flags=re.I)
|
||||
s = re.sub(r'\s+-\s+(museu|funda|fgv|ibge).*$', '', s, flags=re.I)
|
||||
s = re.sub(r'\s*\((museu|ibram)[^)]*\)', '', s, flags=re.I)
|
||||
s = re.sub(r'\s*/\s*ibge$', '', s, flags=re.I)
|
||||
s = re.sub(r'\s*(?:-\s*|\()\s*\w*\d\w*\)?\s*$', '', s) # "- 1123K", "(148T)", "- EFi9"
|
||||
s = sem_acento(s.lower())
|
||||
s = re.sub(r'\bsec\.?\s', 'sec ', s)
|
||||
s = re.sub(r'[^\w\s]', ' ', s)
|
||||
s = re.sub(r'\s+', ' ', s).strip()
|
||||
s = PREFIXO.sub('', s)
|
||||
return ALIAS.get(s, s)
|
||||
|
||||
|
||||
def sigla(titulo: str, usadas: set[str]) -> str:
|
||||
if m := re.match(r'(?i)[áa]lbum\s+(\d+)', titulo):
|
||||
base = f'A{m[1]}'
|
||||
usadas.add(base)
|
||||
return base
|
||||
miolo = PREFIXO.sub('', sem_acento(titulo.lower()))
|
||||
palavras = [p for p in re.findall(r'[a-z0-9]+', miolo) if p not in {'de', 'da', 'do', 'das', 'dos', 'e', 'a', 'o'}]
|
||||
base = ''.join(p[0] for p in palavras).upper()[:6] or 'S'
|
||||
if len(base) < 2 and palavras:
|
||||
base = palavras[0][:4].upper()
|
||||
s, n = base, 2
|
||||
while s in usadas or s == 'AV':
|
||||
s, n = f'{base}{n}', n + 1
|
||||
usadas.add(s)
|
||||
return s
|
||||
|
||||
|
||||
def _titulo(variantes: Counter) -> str:
|
||||
"""A variante mais frequente; se ela não diz o que é, herda o tipo
|
||||
(Coleção/Fundo/Série) de outra variante — "Thereza Christina Maria" +
|
||||
"Coleção D. Teresa…" → "Coleção Thereza Christina Maria"."""
|
||||
tipo_re = r'(?i)^(cole[cç][aã]o( particular)?|fundo|arquivo|s[ée]rie|conv[eê]nio|[áa]lbum)\b'
|
||||
_, v = max((n, v) for v, n in variantes.items())
|
||||
v = re.sub(r'\s+', ' ', v).strip()
|
||||
v = re.sub(r'\s*[-/]\s*(Acervo|Museu|Fundação|Instituto Moreira|IBGE).*$', '', v)
|
||||
v = re.sub(r'\s*\((Museu|IBRAM)[^)]*\)', '', v)
|
||||
if not re.match(tipo_re, v):
|
||||
tipos = Counter(m[1].capitalize() for x in variantes if (m := re.match(tipo_re, x.strip())))
|
||||
v = f"{tipos.most_common(1)[0][0] if tipos else 'Coleção'} {v}"
|
||||
return v
|
||||
|
||||
|
||||
def _ajustes() -> dict[tuple[str, str], str]:
|
||||
p = AJUSTES / 'series.tsv'
|
||||
if not p.exists():
|
||||
return {}
|
||||
with p.open(encoding='utf-8') as f:
|
||||
return {(r['instituicao'], r['dc_source']): r['titulo'] for r in csv.DictReader(f, delimiter='\t')}
|
||||
|
||||
|
||||
def monta(fontes: list[tuple[str, str, str | None]]):
|
||||
"""fontes: (handle, instituição, dc.source | None).
|
||||
|
||||
Devolve (por_handle, series): por_handle[handle] = (sigla, notação na origem
|
||||
ou None); series[(instituição, sigla)] = {'titulo', 'variantes', 'n'}.
|
||||
"""
|
||||
ajuste = _ajustes()
|
||||
chaves = defaultdict(Counter) # (inst, chave) → Counter(valor original)
|
||||
for _, inst, v in fontes:
|
||||
if v and not LIXO.search(v) and not NOTACAO.match(v) and (inst, v) not in ajuste:
|
||||
chaves[(inst, limpa(v))][v] += 1
|
||||
|
||||
series: dict[tuple[str, str], dict] = {}
|
||||
sigla_de: dict[tuple[str, str], str] = {}
|
||||
usadas: dict[str, set[str]] = defaultdict(set)
|
||||
fortes = defaultdict(list) # inst → [chave]
|
||||
for (inst, chave), variantes in sorted(chaves.items(), key=lambda kv: -sum(kv[1].values())):
|
||||
if not chave or sum(variantes.values()) < MINIMO:
|
||||
continue
|
||||
titulo = _titulo(variantes)
|
||||
sg = sigla(titulo, usadas[inst])
|
||||
sigla_de[(inst, chave)] = sg
|
||||
series[(inst, sg)] = {'titulo': titulo, 'variantes': variantes, 'n': 0}
|
||||
fortes[inst].append(chave)
|
||||
for (inst, v), titulo in ajuste.items():
|
||||
k = (inst, 'ajuste:' + titulo)
|
||||
if k not in sigla_de:
|
||||
sg = sigla(titulo, usadas[inst])
|
||||
sigla_de[k] = sg
|
||||
series[(inst, sg)] = {'titulo': titulo, 'variantes': Counter(), 'n': 0}
|
||||
|
||||
por_handle = {}
|
||||
for handle, inst, v in fontes:
|
||||
sg = None
|
||||
if v and (inst, v) in ajuste:
|
||||
sg = sigla_de[(inst, 'ajuste:' + ajuste[(inst, v)])]
|
||||
series[(inst, sg)]['variantes'][v] += 1
|
||||
elif v and not LIXO.search(v):
|
||||
chave = limpa(v)
|
||||
sg = sigla_de.get((inst, chave))
|
||||
if sg is None:
|
||||
# o valor raro contém o nome de uma série forte da mesma instituição?
|
||||
for forte in sorted(fortes[inst], key=len, reverse=True):
|
||||
if len(forte) > 5 and re.search(rf'\b{re.escape(forte)}\b', chave):
|
||||
sg = sigla_de[(inst, forte)]
|
||||
series[(inst, sg)]['variantes'][v] += 1
|
||||
break
|
||||
if sg is None:
|
||||
sg = 'AV'
|
||||
series.setdefault((inst, 'AV'), {'titulo': 'Avulsos', 'variantes': Counter(), 'n': 0})
|
||||
series[(inst, sg)]['n'] += 1
|
||||
# o valor original fica como notação na origem quando não é o próprio nome da série
|
||||
notacao = v if v and (sg == 'AV' or limpa(v) not in {limpa(x) for x in [series[(inst, sg)]['titulo']]}) else None
|
||||
por_handle[handle] = (sg, notacao)
|
||||
return por_handle, series
|
||||
@@ -0,0 +1,85 @@
|
||||
"""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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Subconjunto do xlink.xsd da LoC (http://www.loc.gov/standards/xlink/xlink.xsd),
|
||||
que responde 403 a clientes fora do navegador: só o que o cpf.xsd usa. -->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
targetNamespace="http://www.w3.org/1999/xlink"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
attributeFormDefault="qualified">
|
||||
<xs:attribute name="href" type="xs:anyURI"/>
|
||||
<xs:attribute name="role" type="xs:string"/>
|
||||
<xs:attribute name="arcrole" type="xs:string"/>
|
||||
<xs:attribute name="title" type="xs:string"/>
|
||||
<xs:attribute name="show">
|
||||
<xs:simpleType><xs:restriction base="xs:string">
|
||||
<xs:enumeration value="new"/><xs:enumeration value="replace"/><xs:enumeration value="embed"/>
|
||||
<xs:enumeration value="other"/><xs:enumeration value="none"/>
|
||||
</xs:restriction></xs:simpleType>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="actuate">
|
||||
<xs:simpleType><xs:restriction base="xs:string">
|
||||
<xs:enumeration value="onLoad"/><xs:enumeration value="onRequest"/>
|
||||
<xs:enumeration value="other"/><xs:enumeration value="none"/>
|
||||
</xs:restriction></xs:simpleType>
|
||||
</xs:attribute>
|
||||
<xs:attributeGroup name="simpleLink">
|
||||
<xs:attribute name="type" type="xs:string" fixed="simple" form="qualified"/>
|
||||
<xs:attribute ref="xlink:href" use="optional"/>
|
||||
<xs:attribute ref="xlink:role" use="optional"/>
|
||||
<xs:attribute ref="xlink:arcrole" use="optional"/>
|
||||
<xs:attribute ref="xlink:title" use="optional"/>
|
||||
<xs:attribute ref="xlink:show" use="optional"/>
|
||||
<xs:attribute ref="xlink:actuate" use="optional"/>
|
||||
</xs:attributeGroup>
|
||||
</xs:schema>
|
||||
@@ -0,0 +1,287 @@
|
||||
<?xml version='1.0'?>
|
||||
<?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?>
|
||||
<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns ="http://www.w3.org/1999/xhtml"
|
||||
xml:lang="en">
|
||||
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
<div>
|
||||
<h1>About the XML namespace</h1>
|
||||
|
||||
<div class="bodytext">
|
||||
<p>
|
||||
This schema document describes the XML namespace, in a form
|
||||
suitable for import by other schema documents.
|
||||
</p>
|
||||
<p>
|
||||
See <a href="http://www.w3.org/XML/1998/namespace.html">
|
||||
http://www.w3.org/XML/1998/namespace.html</a> and
|
||||
<a href="http://www.w3.org/TR/REC-xml">
|
||||
http://www.w3.org/TR/REC-xml</a> for information
|
||||
about this namespace.
|
||||
</p>
|
||||
<p>
|
||||
Note that local names in this namespace are intended to be
|
||||
defined only by the World Wide Web Consortium or its subgroups.
|
||||
The names currently defined in this namespace are listed below.
|
||||
They should not be used with conflicting semantics by any Working
|
||||
Group, specification, or document instance.
|
||||
</p>
|
||||
<p>
|
||||
See further below in this document for more information about <a
|
||||
href="#usage">how to refer to this schema document from your own
|
||||
XSD schema documents</a> and about <a href="#nsversioning">the
|
||||
namespace-versioning policy governing this schema document</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
<xs:attribute name="lang">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
<div>
|
||||
|
||||
<h3>lang (as an attribute name)</h3>
|
||||
<p>
|
||||
denotes an attribute whose value
|
||||
is a language code for the natural language of the content of
|
||||
any element; its value is inherited. This name is reserved
|
||||
by virtue of its definition in the XML specification.</p>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<h4>Notes</h4>
|
||||
<p>
|
||||
Attempting to install the relevant ISO 2- and 3-letter
|
||||
codes as the enumerated possible values is probably never
|
||||
going to be a realistic possibility.
|
||||
</p>
|
||||
<p>
|
||||
See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">
|
||||
http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a>
|
||||
and the IANA language subtag registry at
|
||||
<a href="http://www.iana.org/assignments/language-subtag-registry">
|
||||
http://www.iana.org/assignments/language-subtag-registry</a>
|
||||
for further information.
|
||||
</p>
|
||||
<p>
|
||||
The union allows for the 'un-declaration' of xml:lang with
|
||||
the empty string.
|
||||
</p>
|
||||
</div>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:union memberTypes="xs:language">
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value=""/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:union>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
|
||||
<xs:attribute name="space">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
<div>
|
||||
|
||||
<h3>space (as an attribute name)</h3>
|
||||
<p>
|
||||
denotes an attribute whose
|
||||
value is a keyword indicating what whitespace processing
|
||||
discipline is intended for the content of the element; its
|
||||
value is inherited. This name is reserved by virtue of its
|
||||
definition in the XML specification.</p>
|
||||
|
||||
</div>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:NCName">
|
||||
<xs:enumeration value="default"/>
|
||||
<xs:enumeration value="preserve"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
|
||||
<xs:attribute name="base" type="xs:anyURI"> <xs:annotation>
|
||||
<xs:documentation>
|
||||
<div>
|
||||
|
||||
<h3>base (as an attribute name)</h3>
|
||||
<p>
|
||||
denotes an attribute whose value
|
||||
provides a URI to be used as the base for interpreting any
|
||||
relative URIs in the scope of the element on which it
|
||||
appears; its value is inherited. This name is reserved
|
||||
by virtue of its definition in the XML Base specification.</p>
|
||||
|
||||
<p>
|
||||
See <a
|
||||
href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a>
|
||||
for information about this attribute.
|
||||
</p>
|
||||
</div>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
|
||||
<xs:attribute name="id" type="xs:ID">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
<div>
|
||||
|
||||
<h3>id (as an attribute name)</h3>
|
||||
<p>
|
||||
denotes an attribute whose value
|
||||
should be interpreted as if declared to be of type ID.
|
||||
This name is reserved by virtue of its definition in the
|
||||
xml:id specification.</p>
|
||||
|
||||
<p>
|
||||
See <a
|
||||
href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a>
|
||||
for information about this attribute.
|
||||
</p>
|
||||
</div>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
|
||||
<xs:attributeGroup name="specialAttrs">
|
||||
<xs:attribute ref="xml:base"/>
|
||||
<xs:attribute ref="xml:lang"/>
|
||||
<xs:attribute ref="xml:space"/>
|
||||
<xs:attribute ref="xml:id"/>
|
||||
</xs:attributeGroup>
|
||||
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
<div>
|
||||
|
||||
<h3>Father (in any context at all)</h3>
|
||||
|
||||
<div class="bodytext">
|
||||
<p>
|
||||
denotes Jon Bosak, the chair of
|
||||
the original XML Working Group. This name is reserved by
|
||||
the following decision of the W3C XML Plenary and
|
||||
XML Coordination groups:
|
||||
</p>
|
||||
<blockquote>
|
||||
<p>
|
||||
In appreciation for his vision, leadership and
|
||||
dedication the W3C XML Plenary on this 10th day of
|
||||
February, 2000, reserves for Jon Bosak in perpetuity
|
||||
the XML name "xml:Father".
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
<div xml:id="usage" id="usage">
|
||||
<h2><a name="usage">About this schema document</a></h2>
|
||||
|
||||
<div class="bodytext">
|
||||
<p>
|
||||
This schema defines attributes and an attribute group suitable
|
||||
for use by schemas wishing to allow <code>xml:base</code>,
|
||||
<code>xml:lang</code>, <code>xml:space</code> or
|
||||
<code>xml:id</code> attributes on elements they define.
|
||||
</p>
|
||||
<p>
|
||||
To enable this, such a schema must import this schema for
|
||||
the XML namespace, e.g. as follows:
|
||||
</p>
|
||||
<pre>
|
||||
<schema . . .>
|
||||
. . .
|
||||
<import namespace="http://www.w3.org/XML/1998/namespace"
|
||||
schemaLocation="http://www.w3.org/2001/xml.xsd"/>
|
||||
</pre>
|
||||
<p>
|
||||
or
|
||||
</p>
|
||||
<pre>
|
||||
<import namespace="http://www.w3.org/XML/1998/namespace"
|
||||
schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
|
||||
</pre>
|
||||
<p>
|
||||
Subsequently, qualified reference to any of the attributes or the
|
||||
group defined below will have the desired effect, e.g.
|
||||
</p>
|
||||
<pre>
|
||||
<type . . .>
|
||||
. . .
|
||||
<attributeGroup ref="xml:specialAttrs"/>
|
||||
</pre>
|
||||
<p>
|
||||
will define a type which will schema-validate an instance element
|
||||
with any of those attributes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
<div id="nsversioning" xml:id="nsversioning">
|
||||
<h2><a name="nsversioning">Versioning policy for this schema document</a></h2>
|
||||
<div class="bodytext">
|
||||
<p>
|
||||
In keeping with the XML Schema WG's standard versioning
|
||||
policy, this schema document will persist at
|
||||
<a href="http://www.w3.org/2009/01/xml.xsd">
|
||||
http://www.w3.org/2009/01/xml.xsd</a>.
|
||||
</p>
|
||||
<p>
|
||||
At the date of issue it can also be found at
|
||||
<a href="http://www.w3.org/2001/xml.xsd">
|
||||
http://www.w3.org/2001/xml.xsd</a>.
|
||||
</p>
|
||||
<p>
|
||||
The schema document at that URI may however change in the future,
|
||||
in order to remain compatible with the latest version of XML
|
||||
Schema itself, or with the XML namespace itself. In other words,
|
||||
if the XML Schema or XML namespaces change, the version of this
|
||||
document at <a href="http://www.w3.org/2001/xml.xsd">
|
||||
http://www.w3.org/2001/xml.xsd
|
||||
</a>
|
||||
will change accordingly; the version at
|
||||
<a href="http://www.w3.org/2009/01/xml.xsd">
|
||||
http://www.w3.org/2009/01/xml.xsd
|
||||
</a>
|
||||
will not change.
|
||||
</p>
|
||||
<p>
|
||||
Previous dated (and unchanging) versions of this schema
|
||||
document are at:
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="http://www.w3.org/2009/01/xml.xsd">
|
||||
http://www.w3.org/2009/01/xml.xsd</a></li>
|
||||
<li><a href="http://www.w3.org/2007/08/xml.xsd">
|
||||
http://www.w3.org/2007/08/xml.xsd</a></li>
|
||||
<li><a href="http://www.w3.org/2004/10/xml.xsd">
|
||||
http://www.w3.org/2004/10/xml.xsd</a></li>
|
||||
<li><a href="http://www.w3.org/2001/03/xml.xsd">
|
||||
http://www.w3.org/2001/03/xml.xsd</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
</xs:schema>
|
||||
|
||||
Reference in New Issue
Block a user