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