Coverage for changes_metadata_manager / folder_metadata_builder.py: 96%
116 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-21 12:19 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-21 12:19 +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.18190642"
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 "materials",
36 "sala 4",
37 "_files",
38}
40FOLDER_TO_ID = {
41 "S3-PT-DICAM_VetrinaMatriciXilografiche": "ptb",
42 "S3-PT-DICAM_Matrice Xilografica Fiore": "ptb_1",
43 "S3-PT-DICAM_Matrice Xilografica Pianta": "ptb_2",
44 "S3-PT-DICAM_Matrice Xilografica Serpente": "ptb_3",
45 "S3-VS6-DBC_Matrice 1 egizia": "ptb_4",
46 "S5-s.n.-DBC_Busto di Ulisse Aldrovandi": "s_n",
47 "S4-ManicoColtelloZoomorfo": "50",
48 "S5-CNR-AAltoCentro_TestamentoUlisseAldrovandi": "a_alto_centro",
49 "S5-B alto destra 1-FICLIT_Mammuthus1": "b_alto_destra_1",
50 "S5-B alto destra 1-FICLIT_Mammuthus2": "b_alto_destra_2",
51 "S5-B basso-DICAM_FanoneBalenaBasso": "b_basso",
52 "S5-A_alto_sinistra_1-FICLIT_Medaglia-Archeologico": "a_alto_sinistra_1",
53 "S5-A alto sinistra\xa0- 2-FICLIT_MedagliaCommemorativa": "a_alto_sinistra_2",
54 "S5-A alto sinistra 3- DBC_Calchi in gesso": "a_alto_sinistra_3",
55 "S5-B alto centro 1-FICLIT_HarpactocarcinusPunctulatus": "b_alto_centro_1",
56 "S5-B alto centro 2-DBC_Harpactocarcinus sp": "b_alto_centro_2",
57 "S5-B alto centro - 3-FICLIT_LophoraninaAldrovandi": "b_alto_centro_3",
58 "S5-B alto sinistra - 1-FICLIT_Carbonifero": "b_alto_sinistra_1",
59 "S5-B alto sinistra 2-CNR_Miocene": "b_alto_sinistra_2",
60 "S5-B alto sinistra 3-FICLIT_DentiDiPesci": "b_alto_sinistra_3",
61 "S5-B alto destra 3-FICLIT_Hippopotamus": "b_alto_destra_3",
62 # Vetrina 1
63 "S5-Vetrina 1 alto N - 3-FICLIT_SonaglioThevetiaPeruviana": "vetrina_1_alto_n_3",
64 "S5-Vetrina 1 alto N 1-FICLIT_CollanaMesoamericana": "vetrina_1_alto_n_1",
65 "S5-Vetrina 1 alto N-2-DBC_Bambù_lavorato": "vetrina_1_alto_n_2",
66 "S5-Vetrina 1 alto N-2-t-TavolettaConBambù": "vetrina_1_alto_n_2_t",
67 "S5-Vetrina 1 alto S-1-FICLIT-Statuetta ushabti in faïence": "vetrina_1_alto_s_1",
68 "S5-Vetrina 1 alto S-10-DICAM_GemmaInDiasproGialloConCinocefaloeIscrizione": "vetrina_1_alto_s_10",
69 "S5-Vetrina 1 alto S-11-DICAM_GemmaInDiasproConScorpione": "vetrina_1_alto_s_11",
70 "S5-Vetrina 1 alto S-12-DICAM_GemmaInDiasproNeroConIscrizioneAraba": "vetrina_1_alto_s_12",
71 "S5-Vetrina 1 alto S-14-FICLIT_PuntaFrecciaNeolitico": "vetrina_1_alto_s_14",
72 "S5-Vetrina 1 alto S-15-DBC_Corno_lavorato": "vetrina_1_alto_s_15",
73 "S5-Vetrina 1 alto S-16-DBC_Ascia_di_giadeite": "vetrina_1_alto_s_16",
74 "S5-Vetrina 1 alto S-3-DICAM_ScarabeoInStileEgizio": "vetrina_1_alto_s_3",
75 "S5-Vetrina 1 alto S-4-DICAM_GemmaInAgataConCapraPressoAlbero": "vetrina_1_alto_s_4",
76 "S5-Vetrina 1 alto S-5-DICAM_GemmaInAgataconMascheraDellaCommediaDell’Arte": "vetrina_1_alto_s_5",
77 "S5-Vetrina 1 alto S-6-DICAM_GemmaInAgataConSerapideInTrono": "vetrina_1_alto_s_6",
78 "S5-Vetrina 1 alto S-7-DICAM_GemmaInAgataConUccello": "vetrina_1_alto_s_7",
79 "S5-Vetrina 1 alto S-8-DICAM_GemmaInAgataConMercurioeFontana": "vetrina_1_alto_s_8",
80 "S5-Vetrina 1 alto S-9-DICAM_GemmainPastaVitreaStratificataconFiguraAppoggiataAunBastone": "vetrina_1_alto_s_9",
81 "S5-Vetrina 1 basso-DICAM_Carapaci": "vetrina_1_basso",
82 "S5-vetrina_1_alto_s_2-FICLIT_Lucerna fittile a volute con spalla decorata": "vetrina_1_alto_s_2",
83 "S5-vetrina_1_alto_s_13-FICLIT_Sferule di avorio e calcare": "vetrina_1_alto_s_13",
84 # Vetrina 2
85 "S5-Vetrina 2 ALTO N 3-FICLIT_SezioneDenteDiElefante": "vetrina_2_alto_n_3",
86 "S5-Vetrina 2 alto N - 1-DICAM_Calcoli": "vetrina_2_alto_n_1",
87 "S5-Vetrina 2 alto N - 3 - t-DICAM_MatriceElefante": "vetrina_2_alto_n_3_t",
88 "S5-Vetrina 2 alto N-1-t1-DBC_Tavoletta_con_calcolo_1": "vetrina_2_alto_n_1_t1",
89 "S5-Vetrina 2 alto N-1-t2-DBC_Tavoletta_con_calcolo_2": "vetrina_2_alto_n_1_t2",
90 "S5-Vetrina 2 alto S - 1 - t-DICAM_MatriceZanna": "vetrina_2_alto_s_1_t",
91 "S5-Vetrina 2 alto S - 1-DICAM_ZanneDiElefante": "vetrina_2_alto_s_1",
92 "S5-Vetrina 2 alto S - 2-DICAM_CornaDiBovidiECervidi": "vetrina_2_alto_s_2",
93 "S5-Vetrina 2 alto S-2-t-DBC_Tavoletta_con_cervo": "vetrina_2_alto_s_2_t",
94 "S5-Vetrina 2 basso t-DICAM_MatriceRinoceronte": "vetrina_2_basso_t",
95 "S5-Vetrina 2 basso-DICAM_Alce": "vetrina_2_basso",
96 "S5-Vetrina_2_alto_n2_BezoarGazzella": "vetrina_2_alto_n_2",
97 "S5-Vetrina_2_alto_s_3_ghiandoleCastoroeCapra": "vetrina_2_alto_s_3",
98 # Vetrina 3
99 "S5-Vetrina 3 alto N - 1-DICAM_UovaDiStruzzo": "vetrina_3_alto_n_1",
100 "S5-Vetrina 3 alto N - 3-DA_Graminacea Subfossile": "vetrina_3_alto_n_3",
101 "S5-Vetrina 3 alto N-4- DA- Apice vegetativo di palma": "vetrina_3_alto_n_4",
102 "S5-Vetrina 3 alto S-1-t-TavolettaConBaccelli": "vetrina_3_alto_s_1_t",
103 "S5-Vetrina 3 alto S-2-t-DBC_Tavoletta_con_nido": "vetrina_3_alto_s_2_t",
104 "S5-Vetrina 3 alto S-4-FICLIT_NidoDiPendolino": "vetrina_3_alto_s_4",
105 "S5-Vetrina 3 alto sinistra-1-DA-BaccelloConSeme": "vetrina_3_alto_s_1",
106 "S5-Vetrina 3 basso-DICAM_Preparati di terre e Terre sigillate": "vetrina_3_basso",
107 "S5-Vetrina_3_alto_n2_uovamostruosedipolloefagiano": "vetrina_3_alto_n_2",
108 "S5-vetrina_3_alto_s_3_nidipreparativegetali": "vetrina_3_alto_s_3",
109 # Vetrina 4
110 "S5-Vetrina 4 alto N - 10-DICAM_Echinoidifossilin.10": "vetrina_4_alto_n_10",
111 "S5-Vetrina 4 alto N - 11-DICAM_EchinoidiFossilin.11": "vetrina_4_alto_n_11",
112 "S5-Vetrina 4 alto N - 7-DICAM_EchinoidiFossilin.7": "vetrina_4_alto_n_7",
113 "S5-Vetrina 4 alto N - 8-DICAM_EchinoidiFossilin.8": "vetrina_4_alto_n_8",
114 "S5-Vetrina 4 alto N - 9-FICLIT_EchinoidiFossilin.9": "vetrina_4_alto_n_9",
115 "S5-Vetrina 4 alto N-1-DBC_Conoclypeus_conoideus": "vetrina_4_alto_n_1",
116 "S5-Vetrina 4 alto N-2-DBC_Dollaro_di_mare": "vetrina_4_alto_n_2",
117 "S5-Vetrina 4 alto N-3-DBC_Clypeaster_marginatus": "vetrina_4_alto_n_3",
118 "S5-Vetrina 4 alto N-4-DBC_Mazettia_pareti": "vetrina_4_alto_n_4",
119 "S5-Vetrina 4 alto N-5-DBC_Discoidea sp": "vetrina_4_alto_n_5",
120 "S5-Vetrina 4 alto N-6-DBC_Macropneustes_sp": "vetrina_4_alto_n_6",
121 "S5-Vetrina 4 alto S-1-DBC_Calcare a coralli lavorati": "vetrina_4_alto_s_1",
122 "S5-Vetrina 4 alto S-10-DBC_Productus_geinitzianus": "vetrina_4_alto_s_10",
123 "S5-Vetrina 4 alto S-11-DBC_Stephanoceras_bayleanus": "vetrina_4_alto_s_11",
124 "S5-Vetrina 4 alto S-12-DBC_Tellina_planata": "vetrina_4_alto_s_12",
125 "S5-Vetrina 4 alto S-13-DBC_Glossus_humanus": "vetrina_4_alto_s_13",
126 "S5-Vetrina 4 alto S-2-DBC_Coralli": "vetrina_4_alto_s_2",
127 "S5-Vetrina 4 alto S-3-DBC_Lumachella_a_Helicidi": "vetrina_4_alto_s_3",
128 "S5-Vetrina 4 alto S-4-DBC_Calcare_a_lumachella": "vetrina_4_alto_s_4",
129 "S5-Vetrina 4 alto S-5-DBC_Dentalium_elephantinum": "vetrina_4_alto_s_5",
130 "S5-Vetrina 4 alto S-6-DBC_Glycymeris_glycymeris": "vetrina_4_alto_s_6",
131 "S5-Vetrina 4 alto S-7-DBC_Bivalvi_indet": "vetrina_4_alto_s_7",
132 "S5-Vetrina 4 alto S-8-DBC_Lytoceras_sp": "vetrina_4_alto_s_8",
133 "S5-Vetrina 4 alto S-9-DBC_Megalodon_sp": "vetrina_4_alto_s_9",
134 "S5-Vetrina 4 basso-FICLIT_Campioni di rocce levigate": "vetrina_4_basso",
135 # Vetrina 5
136 "S5-Vetrina 5 alto N - t-DICAM_MatriceBotroide1": "vetrina_5_alto_n_t",
137 "S5-Vetrina 5 alto N-DICAM_Botroide1": "vetrina_5_alto_n",
138 "S5-Vetrina 5 alto S - 1-DICAM_Botroide2": "vetrina_5_alto_s_1",
139 "S5-Vetrina 5 alto S - 2 - t-DICAM_MatriceBotroide2": "vetrina_5_alto_s_2_t",
140 "S5-Vetrina 5 alto S-2-DICAM_Botroide_triorchites": "vetrina_5_alto_s_2",
141 "S5-Vetrina 5 basso t-DICAM_MatriceBotroide3": "vetrina_5_basso_t",
142 "S5-Vetrina 5 basso-DICAM_Botroide3": "vetrina_5_basso",
143 # Vetrina 6
144 "S5-Vetrina 6 alto N-1-DBC_Bufo sp., Rospo": "vetrina_6_alto_n_1",
145 "S5-Vetrina 6 alto N-1-t-TavolettaBufo": "vetrina_6_alto_n_1_t",
146 "S5-Vetrina 6 alto N-2-DBC_Cordylus sp., lucertola": "vetrina_6_alto_n_2",
147 "S5-Vetrina 6 alto N-2-t-TavolettaConLucertola": "vetrina_6_alto_n_2_t",
148 "S5-Vetrina 6 alto S - 2 - t-DICAM_MatricePesce12_Pesce": "vetrina_6_alto_s_2_t",
149 "S5-Vetrina 6 basso 2-ScheletroDiDelfino": "vetrina_6_basso_2",
150 "S5-Vetrina 6 basso 2-t-TavolettaConDelfino": "vetrina_6_basso_2_t",
151 "S5-Vetrina 6 basso-DBC_Scapola di balena": "vetrina_6_basso",
152 "S5-vetrina_6_alto_s_1_Chamaleo": "vetrina_6_alto_s_1",
153 "S5-vetrina_6_alto_s_1_Scincus_Pescedellesabbie": "vetrina_6_alto_s_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_t1",
179 "S5-Vetrina 8 basso - t2-DICAM_MatricePesce5_PesceSega": "vetrina_8_basso_t2",
180 "S5-Vetrina 8 basso-DA-Pesce Martello": "vetrina_8_basso",
181 "S5-Vetrina 8 basso-DA-Preparati zoologici, Pescecane": "vetrina_8_basso",
182 "S5-Vetrina 8 basso-DA-Preparati zoologici, pesce sega": "vetrina_8_basso",
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"
210def extract_metadata_for_stage(graph: Graph, nr: str, stage: str) -> Graph:
211 result = Graph()
212 for prefix, namespace in graph.namespace_manager.namespaces():
213 result.namespace_manager.bind(prefix, namespace)
215 steps = STAGE_STEPS[stage]
217 for s, p, o in graph:
218 s_str = str(s)
219 step_match = re.search(rf"/{nr}/(\d{{2}})/1$", s_str)
220 if step_match:
221 step = step_match.group(1)
222 if step in steps:
223 result.add((s, p, o))
224 if isinstance(o, URIRef):
225 for s2, p2, o2 in graph.triples((o, None, None)):
226 result.add((s2, p2, o2))
227 continue
229 ob_match = re.search(rf"/{nr}/ob\d+/1$", s_str)
230 if ob_match:
231 result.add((s, p, o))
232 if isinstance(o, URIRef):
233 for s2, p2, o2 in graph.triples((o, None, None)):
234 result.add((s2, p2, o2))
236 return result
239def validate_metadata(data_graph: Graph, shapes_graph: Graph) -> tuple[bool, str]:
240 conforms, _, results_text = pyshacl.validate(
241 data_graph,
242 shacl_graph=shapes_graph,
243 )
244 return bool(conforms), str(results_text)
247def scan_folder_structure(root_path: Path) -> dict:
248 structure = {}
249 for sala_dir in root_path.iterdir():
250 if not sala_dir.is_dir():
251 continue
252 sala_name = sala_dir.name
253 structure[sala_name] = {}
254 for folder_dir in sala_dir.iterdir():
255 if not folder_dir.is_dir():
256 continue
257 folder_name = folder_dir.name
258 structure[sala_name][folder_name] = {}
259 for stage_dir in folder_dir.iterdir():
260 if not stage_dir.is_dir():
261 continue
262 stage_name = stage_dir.name
263 files = [f.name for f in stage_dir.iterdir() if f.is_file()]
264 structure[sala_name][folder_name][stage_name] = {"_files": files}
265 return {"structure": structure}
268def process_all_folders(
269 root: Path,
270 kg_path: Path = KG_PATH,
271 shapes_path: Path = SHAPES_PATH,
272 validate: bool = True,
273) -> list[tuple[str, str]]:
274 structure = scan_folder_structure(root)
275 kg = load_kg(kg_path)
276 shapes_graph = load_kg(shapes_path) if validate else None
278 console = Console()
279 validation_errors = []
281 folders = [
282 (sala_name, folder_name, subfolders)
283 for sala_name, sala_items in structure["structure"].items()
284 for folder_name, subfolders in sala_items.items()
285 if folder_name not in SKIP_FOLDERS
286 ]
288 with Progress(
289 SpinnerColumn(),
290 TextColumn("[progress.description]{task.description}"),
291 BarColumn(),
292 MofNCompleteColumn(),
293 ) as progress:
294 task = progress.add_task("Processing folders", total=len(folders))
296 for sala_name, folder_name, subfolders in folders:
297 nr = extract_id_from_folder_name(folder_name)
298 progress.update(task, description=f"{folder_name}")
300 existing_stages = [
301 s for s in subfolders.keys()
302 if s.lower() in STAGE_STEPS
303 ]
305 for stage_name in existing_stages:
306 stage_key = stage_name.lower()
307 stage_dir = root / sala_name / folder_name / stage_name
308 stage_dir.mkdir(parents=True, exist_ok=True)
310 metadata = extract_metadata_for_stage(kg, nr, stage_key)
311 metadata.add((URIRef(""), DCTERMS.license, CC0))
313 if shapes_graph is not None:
314 conforms, results_text = validate_metadata(metadata, shapes_graph)
315 if not conforms:
316 label = f"{folder_name}/{stage_name}"
317 validation_errors.append((label, results_text))
319 meta_path = stage_dir / "meta.ttl"
320 metadata.serialize(destination=str(meta_path), format="turtle")
322 prov_path = stage_dir / "prov.trig"
323 generate_provenance_snapshots(
324 input_directory=str(stage_dir),
325 output_file=str(prov_path),
326 output_format="trig",
327 agent_orcid=RESP_AGENT,
328 primary_source=PRIMARY_SOURCE,
329 )
331 progress.advance(task)
333 if shapes_graph is not None:
334 if validation_errors:
335 console.print(f"\n[bold red]SHACL validation failed for {len(validation_errors)} stage(s):[/bold red]")
336 for label, results_text in validation_errors:
337 console.print(f"\n[bold yellow]{label}[/bold yellow]")
338 console.print(results_text)
339 else:
340 console.print("\n[bold green]All metadata passed SHACL validation.[/bold green]")
342 return validation_errors
345def merge_provenance_files(root: Path, output_path: Path) -> None:
346 merged = Dataset(default_union=True)
347 for prov_file in sorted(root.rglob("prov.trig")):
348 merged.parse(str(prov_file), format="trig")
349 merged.serialize(destination=str(output_path), format="trig")
352def parse_arguments(): # pragma: no cover
353 parser = argparse.ArgumentParser(
354 description="Generate metadata and provenance files for folder structure"
355 )
356 parser.add_argument(
357 "root",
358 type=Path,
359 help="Root directory containing Sala/Folder/Stage structure",
360 )
361 parser.add_argument(
362 "--no-validate",
363 action="store_true",
364 help="Skip SHACL validation",
365 )
366 parser.add_argument(
367 "--merge-provenance",
368 type=Path,
369 default=None,
370 help="Output path for merged provenance file (TriG format)",
371 )
372 return parser.parse_args()
375def main(): # pragma: no cover
376 args = parse_arguments()
377 process_all_folders(root=args.root, validate=not args.no_validate)
378 if args.merge_provenance:
379 merge_provenance_files(args.root, args.merge_provenance)
382if __name__ == "__main__": # pragma: no cover
383 main()