"""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', })