Coverage for changes_metadata_manager / folder_metadata_builder.py: 92%
121 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-29 18:29 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-29 18:29 +0000
1# SPDX-FileCopyrightText: 2025-2026 Arcangelo Massari <arcangelomas@gmail.com>
2#
3# SPDX-License-Identifier: ISC
5import argparse
6import re
7from pathlib import Path
9import pyshacl
10from rdflib import Dataset, Graph, URIRef
11from rdflib.namespace import DCTERMS
12from rich.console import Console
13from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, MofNCompleteColumn
15from changes_metadata_manager.generate_provenance import generate_provenance_snapshots
18BASE_URI = "https://w3id.org/changes/4/aldrovandi"
19KG_PATH = Path("data/kg.ttl")
20SHAPES_PATH = Path("data/shapes-chadap.ttl")
21RESP_AGENT = "https://w3id.org/changes/4/agent/morph-kgc-changes-metadata/1.0.1"
22PRIMARY_SOURCE = "https://doi.org/10.5281/zenodo.19898905"
23CC0 = URIRef("https://creativecommons.org/publicdomain/zero/1.0/")
25STAGE_STEPS = {
26 "raw": ["00"],
27 "rawp": ["00", "01"],
28 "dcho": ["00", "01", "02"],
29 "dchoo": ["00", "01", "02", "03", "04", "05", "06"],
30}
32SKIP_FOLDERS = {
33 "S1-CNR_SoffittoSala1",
34 "S5-B basso-DICAM_FanoneBalenaAlto",
35 "S5-Vetrina_2_alto_n2_BezoarGazzella",
36 "S5-Vetrina_2_alto_s_3_ghiandoleCastoroeCapra",
37 "S5-vetrina_6_alto_s_1_Chamaleo",
38 "S5-vetrina_6_alto_s_1_Scincus_Pescedellesabbie",
39 "S5-Vetrina 3 alto N-4- DA- Apice vegetativo di palma",
40 "materials",
41 "sala 4",
42 "_files",
43}
45FOLDER_TO_ID = {
46 "S3-PT-DICAM_VetrinaMatriciXilografiche": "ptb",
47 "S3-PT-DICAM_Matrice Xilografica Fiore": "ptb_1",
48 "S3-PT-DICAM_Matrice Xilografica Pianta": "ptb_2",
49 "S3-PT-DICAM_Matrice Xilografica Serpente": "ptb_3",
50 "S3-VS6-DBC_Matrice 1 egizia": "ptb_4",
51 "S5-s.n.-DBC_Busto di Ulisse Aldrovandi": "s_n",
52 "S4-ManicoColtelloZoomorfo": "50",
53 "S5-CNR-AAltoCentro_TestamentoUlisseAldrovandi": "a_alto_centro",
54 "S5-B alto destra 1-FICLIT_Mammuthus1": "b_alto_destra_1",
55 "S5-B alto destra 1-FICLIT_Mammuthus2": "b_alto_destra_2",
56 "S5-B basso-DICAM_FanoneBalenaBasso": "b_basso",
57 "S5-A_alto_sinistra_1-FICLIT_Medaglia-Archeologico": "a_alto_sinistra_1",
58 "S5-A alto sinistra\xa0- 2-FICLIT_MedagliaCommemorativa": "a_alto_sinistra_2",
59 "S5-A alto sinistra 3- DBC_Calchi in gesso": "a_alto_sinistra_3",
60 "S5-B alto centro 1-FICLIT_HarpactocarcinusPunctulatus": "b_alto_centro_1",
61 "S5-B alto centro 2-DBC_Harpactocarcinus sp": "b_alto_centro_2",
62 "S5-B alto centro - 3-FICLIT_LophoraninaAldrovandi": "b_alto_centro_3",
63 "S5-B alto sinistra - 1-FICLIT_Carbonifero": "b_alto_sinistra_1",
64 "S5-B alto sinistra 2-CNR_Miocene": "b_alto_sinistra_2",
65 "S5-B alto sinistra 3-FICLIT_DentiDiPesci": "b_alto_sinistra_3",
66 "S5-B alto destra 3-FICLIT_Hippopotamus": "b_alto_destra_3",
67 # Vetrina 1
68 "S5-Vetrina 1 alto N - 3-FICLIT_SonaglioThevetiaPeruviana": "vetrina_1_alto_n_3",
69 "S5-Vetrina 1 alto N 1-FICLIT_CollanaMesoamericana": "vetrina_1_alto_n_1",
70 "S5-Vetrina 1 alto N-2-DBC_Bambù_lavorato": "vetrina_1_alto_n_2",
71 "S5-Vetrina 1 alto N-2-t-TavolettaConBambù": "vetrina_1_alto_n_2_t",
72 "S5-Vetrina 1 alto S-1-FICLIT-Statuetta ushabti in faïence": "vetrina_1_alto_s_1",
73 "S5-Vetrina 1 alto S-10-DICAM_GemmaInDiasproGialloConCinocefaloeIscrizione": "vetrina_1_alto_s_10",
74 "S5-Vetrina 1 alto S-11-DICAM_GemmaInDiasproConScorpione": "vetrina_1_alto_s_11",
75 "S5-Vetrina 1 alto S-12-DICAM_GemmaInDiasproNeroConIscrizioneAraba": "vetrina_1_alto_s_12",
76 "S5-Vetrina 1 alto S-14-FICLIT_PuntaFrecciaNeolitico": "vetrina_1_alto_s_14",
77 "S5-Vetrina 1 alto S-15-DBC_Corno_lavorato": "vetrina_1_alto_s_15",
78 "S5-Vetrina 1 alto S-16-DBC_Ascia_di_giadeite": "vetrina_1_alto_s_16",
79 "S5-Vetrina 1 alto S-3-DICAM_ScarabeoInStileEgizio": "vetrina_1_alto_s_3",
80 "S5-Vetrina 1 alto S-4-DICAM_GemmaInAgataConCapraPressoAlbero": "vetrina_1_alto_s_4",
81 "S5-Vetrina 1 alto S-5-DICAM_GemmaInAgataconMascheraDellaCommediaDell’Arte": "vetrina_1_alto_s_5",
82 "S5-Vetrina 1 alto S-6-DICAM_GemmaInAgataConSerapideInTrono": "vetrina_1_alto_s_6",
83 "S5-Vetrina 1 alto S-7-DICAM_GemmaInAgataConUccello": "vetrina_1_alto_s_7",
84 "S5-Vetrina 1 alto S-8-DICAM_GemmaInAgataConMercurioeFontana": "vetrina_1_alto_s_8",
85 "S5-Vetrina 1 alto S-9-DICAM_GemmainPastaVitreaStratificataconFiguraAppoggiataAunBastone": "vetrina_1_alto_s_9",
86 "S5-Vetrina 1 basso-DICAM_Carapaci": "vetrina_1_basso",
87 "S5-vetrina_1_alto_s_2-FICLIT_Lucerna fittile a volute con spalla decorata": "vetrina_1_alto_s_2",
88 "S5-vetrina_1_alto_s_13-FICLIT_Sferule di avorio e calcare": "vetrina_1_alto_s_13",
89 # Vetrina 2
90 "S5-Vetrina 2 ALTO N 3-FICLIT_SezioneDenteDiElefante": "vetrina_2_alto_n_3",
91 "S5-Vetrina 2 alto N - 1-DICAM_Calcoli": "vetrina_2_alto_n_1",
92 "S5-Vetrina 2 alto N - 3 - t-DICAM_MatriceElefante": "vetrina_2_alto_n_3_t",
93 "S5-Vetrina 2 alto N-1-t1-DBC_Tavoletta_con_calcolo_1": "vetrina_2_alto_n_1_t1",
94 "S5-Vetrina 2 alto N-1-t2-DBC_Tavoletta_con_calcolo_2": "vetrina_2_alto_n_1_t2",
95 "S5-Vetrina 2 alto S - 1 - t-DICAM_MatriceZanna": "vetrina_2_alto_s_1_t",
96 "S5-Vetrina 2 alto S - 1-DICAM_ZanneDiElefante": "vetrina_2_alto_s_1",
97 "S5-Vetrina 2 alto S - 2-DICAM_CornaDiBovidiECervidi": "vetrina_2_alto_s_2",
98 "S5-Vetrina 2 alto S-2-t-DBC_Tavoletta_con_cervo": "vetrina_2_alto_s_2_t",
99 "S5-Vetrina 2 basso t-DICAM_MatriceRinoceronte": "vetrina_2_basso_t",
100 "S5-Vetrina 2 basso-DICAM_Alce": "vetrina_2_basso",
101 # Vetrina 3
102 "S5-Vetrina 3 alto N - 1-DICAM_UovaDiStruzzo": "vetrina_3_alto_n_1",
103 "S5-Vetrina 3 alto N - 3-DA_Graminacea Subfossile": "vetrina_3_alto_n_3",
104 "S5-Vetrina 3 alto S-1-t-TavolettaConBaccelli": "vetrina_3_alto_s_1_t",
105 "S5-Vetrina 3 alto S-2-t-DBC_Tavoletta_con_nido": "vetrina_3_alto_s_2_t",
106 "S5-Vetrina 3 alto S-4-FICLIT_NidoDiPendolino": "vetrina_3_alto_s_4",
107 "S5-Vetrina 3 alto sinistra-1-DA-BaccelloConSeme": "vetrina_3_alto_s_1",
108 "S5-Vetrina 3 basso-DICAM_Preparati di terre e Terre sigillate": "vetrina_3_basso",
109 "S5-Vetrina_3_alto_n2_uovamostruosedipolloefagiano": "vetrina_3_alto_n_2",
110 "S5-vetrina_3_alto_s_3_nidipreparativegetali": "vetrina_3_alto_s_3",
111 # Vetrina 4
112 "S5-Vetrina 4 alto N - 10-DICAM_Echinoidifossilin.10": "vetrina_4_alto_n_10",
113 "S5-Vetrina 4 alto N - 11-DICAM_EchinoidiFossilin.11": "vetrina_4_alto_n_11",
114 "S5-Vetrina 4 alto N - 7-DICAM_EchinoidiFossilin.7": "vetrina_4_alto_n_7",
115 "S5-Vetrina 4 alto N - 8-DICAM_EchinoidiFossilin.8": "vetrina_4_alto_n_8",
116 "S5-Vetrina 4 alto N - 9-FICLIT_EchinoidiFossilin.9": "vetrina_4_alto_n_9",
117 "S5-Vetrina 4 alto N-1-DBC_Conoclypeus_conoideus": "vetrina_4_alto_n_1",
118 "S5-Vetrina 4 alto N-2-DBC_Dollaro_di_mare": "vetrina_4_alto_n_2",
119 "S5-Vetrina 4 alto N-3-DBC_Clypeaster_marginatus": "vetrina_4_alto_n_3",
120 "S5-Vetrina 4 alto N-4-DBC_Mazettia_pareti": "vetrina_4_alto_n_4",
121 "S5-Vetrina 4 alto N-5-DBC_Discoidea sp": "vetrina_4_alto_n_5",
122 "S5-Vetrina 4 alto N-6-DBC_Macropneustes_sp": "vetrina_4_alto_n_6",
123 "S5-Vetrina 4 alto S-1-DBC_Calcare a coralli lavorati": "vetrina_4_alto_s_1",
124 "S5-Vetrina 4 alto S-10-DBC_Productus_geinitzianus": "vetrina_4_alto_s_10",
125 "S5-Vetrina 4 alto S-11-DBC_Stephanoceras_bayleanus": "vetrina_4_alto_s_11",
126 "S5-Vetrina 4 alto S-12-DBC_Tellina_planata": "vetrina_4_alto_s_12",
127 "S5-Vetrina 4 alto S-13-DBC_Glossus_humanus": "vetrina_4_alto_s_13",
128 "S5-Vetrina 4 alto S-2-DBC_Coralli": "vetrina_4_alto_s_2",
129 "S5-Vetrina 4 alto S-3-DBC_Lumachella_a_Helicidi": "vetrina_4_alto_s_3",
130 "S5-Vetrina 4 alto S-4-DBC_Calcare_a_lumachella": "vetrina_4_alto_s_4",
131 "S5-Vetrina 4 alto S-5-DBC_Dentalium_elephantinum": "vetrina_4_alto_s_5",
132 "S5-Vetrina 4 alto S-6-DBC_Glycymeris_glycymeris": "vetrina_4_alto_s_6",
133 "S5-Vetrina 4 alto S-7-DBC_Bivalvi_indet": "vetrina_4_alto_s_7",
134 "S5-Vetrina 4 alto S-8-DBC_Lytoceras_sp": "vetrina_4_alto_s_8",
135 "S5-Vetrina 4 alto S-9-DBC_Megalodon_sp": "vetrina_4_alto_s_9",
136 "S5-Vetrina 4 basso-FICLIT_Campioni di rocce levigate": "vetrina_4_basso",
137 # Vetrina 5
138 "S5-Vetrina 5 alto N - t-DICAM_MatriceBotroide1": "vetrina_5_alto_n_t",
139 "S5-Vetrina 5 alto N-DICAM_Botroide1": "vetrina_5_alto_n",
140 "S5-Vetrina 5 alto S - 1-DICAM_Botroide2": "vetrina_5_alto_s_1",
141 "S5-Vetrina 5 alto S - 2 - t-DICAM_MatriceBotroide2": "vetrina_5_alto_s_2_t",
142 "S5-Vetrina 5 alto S-2-DICAM_Botroide_triorchites": "vetrina_5_alto_s_2",
143 "S5-Vetrina 5 basso t-DICAM_MatriceBotroide3": "vetrina_5_basso_t",
144 "S5-Vetrina 5 basso-DICAM_Botroide3": "vetrina_5_basso",
145 # Vetrina 6
146 "S5-Vetrina 6 alto N-1-DBC_Bufo sp., Rospo": "vetrina_6_alto_n_1",
147 "S5-Vetrina 6 alto N-1-t-TavolettaBufo": "vetrina_6_alto_n_1_t",
148 "S5-Vetrina 6 alto N-2-DBC_Cordylus sp., lucertola": "vetrina_6_alto_n_2",
149 "S5-Vetrina 6 alto N-2-t-TavolettaConLucertola": "vetrina_6_alto_n_2_t",
150 "S5-Vetrina 6 alto S - 2 - t-DICAM_MatricePesce12_Pesce": "vetrina_6_alto_s_2_t",
151 "S5-Vetrina 6 basso 2-ScheletroDiDelfino": "vetrina_6_basso_2",
152 "S5-Vetrina 6 basso 2-t-TavolettaConDelfino": "vetrina_6_basso_2_t",
153 "S5-Vetrina 6 basso-DBC_Scapola di balena": "vetrina_6_basso_1",
154 # Vetrina 7
155 "S5-Vetrina 7 alto N - 1 - t-DICAM_MatricePesce6_PesceForca": "vetrina_7_alto_n_1_t",
156 "S5-Vetrina 7 alto N 1-FICLIT_PesceForca": "vetrina_7_alto_n_1",
157 "S5-Vetrina 7 alto N 2-FICLIT_PesceScatola": "vetrina_7_alto_n_2",
158 "S5-Vetrina 7 alto S - 1 - t-DICAM_MatricePesce11_PesceSpada": "vetrina_7_alto_s_1_t",
159 "S5-Vetrina 7 alto S - 3 - t-DICAM_MatricePesce10_PesceVolante": "vetrina_7_alto_s_3_t",
160 "S5-Vetrina 7 alto S - 3-FICLIT_PesceVolante": "vetrina_7_alto_s_3",
161 "S5-Vetrina 7 alto S 2-FICLIT_SorcioMarino": "vetrina_7_alto_s_2",
162 "S5-Vetrina 7 alto S-1-DA-Xiphias sp., Pesci spada": "vetrina_7_alto_s_1",
163 "S5-Vetrina 7 basso - t-DICAM_MatricePesce13_PescePalla2": "vetrina_7_basso_t",
164 "S5-Vetrina 7 basso - t-DICAM_MatricePesce9_PescePalla1": "vetrina_7_basso_t",
165 "S5-Vetrina 7 basso-DICAM_PescePalla": "vetrina_7_basso",
166 # Vetrina 8
167 "S5-Vetrina 8 alto N - 1-FICLIT_ScazzoneMarino": "vetrina_8_alto_n_1",
168 "S5-Vetrina 8 alto N - 3 - t-DICAM_MatricePesce4_Bocca1": "vetrina_8_alto_n_3_t",
169 "S5-Vetrina 8 alto N - 3 - t-DICAM_MatricePesce8_Bocca2": "vetrina_8_alto_n_3_t",
170 "S5-Vetrina 8 alto N - 3-DA-Apparato dentale di pesce cartilagineo (Elasmobrnchii)": "vetrina_8_alto_n_3",
171 "S5-Vetrina 8 alto N-2-DA-Lophius piscatorius (Linnaeus, 1758), rana pescatrice": "vetrina_8_alto_n_2",
172 "S5-Vetrina 8 alto S - 1 - t-DICAM_MatricePesce1_PesceChitarra": "vetrina_8_alto_s_1_t",
173 "S5-Vetrina 8 alto S - 1-DA-Rhinobatos rhinobatos": "vetrina_8_alto_s_1",
174 "S5-Vetrina 8 alto S - 2 - t-DICAM_MatricePesce2_Smeriglio": "vetrina_8_alto_s_2_t",
175 "S5-Vetrina 8 alto S - 2-DA-Lamna nasus (Bonnaterre, 1788), smeriglio": "vetrina_8_alto_s_2",
176 "S5-Vetrina 8 alto S - 3 - t-DICAM_MatricePesce3_RanaPescatrice": "vetrina_8_alto_s_3_t",
177 "S5-Vetrina 8 alto S.3-DA-Lophius piscatorius (Linnaeus, 1758), apparato boccale rana pescatrice": "vetrina_8_alto_s_3",
178 "S5-Vetrina 8 basso - t1-DICAM_MatricePesce7_DentiPesceSega": "vetrina_8_basso_2_t1",
179 "S5-Vetrina 8 basso - t2-DICAM_MatricePesce5_PesceSega": "vetrina_8_basso_2_t2",
180 "S5-Vetrina 8 basso-DA-Preparati zoologici, Pescecane": "vetrina_8_basso",
181 "S5-Vetrina 8 basso_2_DA-Preparati zoologici, pesce sega": "vetrina_8_basso_2",
182 "S5-Vetrina 8 basso_3_DA-Pesce Martello": "vetrina_8_basso_3",
183 # Manoscritti
184 "S5-Manoscritto-FICLIT_AdnotationesVariaePraesertimDeAnimalibus": "m1",
185 "S5-Manoscritto-FICLIT_VulgataProverbia": "m2",
186 "S5-Manoscritto-FICLIT_PandechionEpistemonicon": "m3",
187 "S5-Manoscritto-FICLIT_LexiconRerumInanimatarum": "m4",
188 "S5-Manoscritto-FICLIT_BibliothecaSecundumNominaAuthorum": "m5",
189 "S5-Manoscritto-FICLIT_TheatrumBiblicumNaturale": "m6",
190 "S5-Manoscritto-FICLIT_LibroDeiVisitatori": "m7",
191 "S5-Manoscritto-FICLIT_DiscorsoNaturaleAldrovandi": "m8",
192}
195def load_kg(path: Path) -> Graph:
196 graph = Graph()
197 graph.parse(path, format="turtle")
198 return graph
201def extract_id_from_folder_name(folder_name: str) -> str:
202 if folder_name in FOLDER_TO_ID:
203 return FOLDER_TO_ID[folder_name]
204 match = re.match(r"S\d+-(\d+[a-z]?) ?[-_]", folder_name)
205 if not match:
206 raise ValueError(f"Cannot extract ID from folder name: {folder_name}")
207 return match.group(1).lstrip("0") or "0"
210P1_IS_IDENTIFIED_BY = URIRef("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by")
213def extract_metadata_for_stage(graph: Graph, nr: str, stage: str) -> Graph:
214 result = Graph()
215 for prefix, namespace in graph.namespace_manager.namespaces():
216 result.namespace_manager.bind(prefix, namespace)
218 steps = STAGE_STEPS[stage]
220 for s, p, o in graph:
221 s_str = str(s)
222 step_match = re.search(rf"/{nr}/(\d{{2}})/1$", s_str)
223 if step_match:
224 step = step_match.group(1)
225 if step in steps:
226 result.add((s, p, o))
227 if isinstance(o, URIRef):
228 for s2, p2, o2 in graph.triples((o, None, None)):
229 result.add((s2, p2, o2))
230 continue
232 ob_match = re.search(rf"/{nr}/ob\d+/1$", s_str)
233 if ob_match:
234 result.add((s, p, o))
235 if isinstance(o, URIRef):
236 for s2, p2, o2 in graph.triples((o, None, None)):
237 result.add((s2, p2, o2))
239 appellation_uris = {o for _, p, o in result if p == P1_IS_IDENTIFIED_BY and isinstance(o, URIRef)}
240 for uri in appellation_uris:
241 for s2, p2, o2 in graph.triples((uri, None, None)):
242 result.add((s2, p2, o2))
244 return result
247def validate_metadata(data_graph: Graph, shapes_graph: Graph) -> tuple[bool, str]:
248 conforms, _, results_text = pyshacl.validate(
249 data_graph,
250 shacl_graph=shapes_graph,
251 )
252 return bool(conforms), str(results_text)
255def scan_folder_structure(root_path: Path) -> dict:
256 structure = {}
257 for sala_dir in root_path.iterdir():
258 if not sala_dir.is_dir():
259 continue
260 sala_name = sala_dir.name
261 structure[sala_name] = {}
262 for folder_dir in sala_dir.iterdir():
263 if not folder_dir.is_dir():
264 continue
265 folder_name = folder_dir.name
266 structure[sala_name][folder_name] = {}
267 for stage_dir in folder_dir.iterdir():
268 if not stage_dir.is_dir():
269 continue
270 stage_name = stage_dir.name
271 files = [f.name for f in stage_dir.iterdir() if f.is_file()]
272 structure[sala_name][folder_name][stage_name] = {"_files": files}
273 return {"structure": structure}
276def process_all_folders(
277 root: Path,
278 kg_path: Path = KG_PATH,
279 shapes_path: Path = SHAPES_PATH,
280 validate: bool = True,
281) -> list[tuple[str, str]]:
282 structure = scan_folder_structure(root)
283 kg = load_kg(kg_path)
284 shapes_graph = load_kg(shapes_path) if validate else None
286 console = Console()
287 validation_errors = []
289 folders = [
290 (sala_name, folder_name, subfolders)
291 for sala_name, sala_items in structure["structure"].items()
292 for folder_name, subfolders in sala_items.items()
293 if folder_name not in SKIP_FOLDERS
294 ]
296 with Progress(
297 SpinnerColumn(),
298 TextColumn("[progress.description]{task.description}"),
299 BarColumn(),
300 MofNCompleteColumn(),
301 ) as progress:
302 task = progress.add_task("Processing folders", total=len(folders))
304 for sala_name, folder_name, subfolders in folders:
305 nr = extract_id_from_folder_name(folder_name)
306 progress.update(task, description=f"{folder_name}")
308 existing_stages = [
309 s for s in subfolders.keys()
310 if s.lower() in STAGE_STEPS
311 ]
313 for stage_name in existing_stages:
314 stage_key = stage_name.lower()
315 stage_dir = root / sala_name / folder_name / stage_name
316 stage_dir.mkdir(parents=True, exist_ok=True)
318 metadata = extract_metadata_for_stage(kg, nr, stage_key)
319 metadata.add((URIRef(""), DCTERMS.license, CC0))
321 if shapes_graph is not None:
322 conforms, results_text = validate_metadata(metadata, shapes_graph)
323 if not conforms:
324 label = f"{folder_name}/{stage_name}"
325 validation_errors.append((label, results_text))
327 meta_path = stage_dir / "meta.ttl"
328 metadata.serialize(destination=str(meta_path), format="turtle")
330 prov_path = stage_dir / "prov.trig"
331 generate_provenance_snapshots(
332 input_directory=str(stage_dir),
333 output_file=str(prov_path),
334 output_format="trig",
335 agent_orcid=RESP_AGENT,
336 primary_source=PRIMARY_SOURCE,
337 )
339 progress.advance(task)
341 if shapes_graph is not None:
342 if validation_errors:
343 console.print(f"\n[bold red]SHACL validation failed for {len(validation_errors)} stage(s):[/bold red]")
344 for label, results_text in validation_errors:
345 console.print(f"\n[bold yellow]{label}[/bold yellow]")
346 console.print(results_text)
347 else:
348 console.print("\n[bold green]All metadata passed SHACL validation.[/bold green]")
350 return validation_errors
353def merge_provenance_files(root: Path, output_path: Path) -> None:
354 merged = Dataset(default_union=True)
355 for prov_file in sorted(root.rglob("prov.trig")):
356 merged.parse(str(prov_file), format="trig")
357 merged.serialize(destination=str(output_path), format="trig")
360def parse_arguments(): # pragma: no cover
361 parser = argparse.ArgumentParser(
362 description="Generate metadata and provenance files for folder structure"
363 )
364 parser.add_argument(
365 "root",
366 type=Path,
367 help="Root directory containing Sala/Folder/Stage structure",
368 )
369 parser.add_argument(
370 "--no-validate",
371 action="store_true",
372 help="Skip SHACL validation",
373 )
374 parser.add_argument(
375 "--merge-provenance",
376 type=Path,
377 default=None,
378 help="Output path for merged provenance file (TriG format)",
379 )
380 return parser.parse_args()
383def main(): # pragma: no cover
384 args = parse_arguments()
385 process_all_folders(root=args.root, validate=not args.no_validate)
386 if args.merge_provenance:
387 merge_provenance_files(args.root, args.merge_provenance)
390if __name__ == "__main__": # pragma: no cover
391 main()