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