Coverage for changes_metadata_manager / generate_provenance.py: 98%
49 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
5"""
6Module to generate provenance snapshots from RDF data.
7Loads RDF data in various formats from all files in a directory,
8extracts all subjects, and creates provenance snapshots as named graphs
9with type prov:Entity.
10"""
12import os
13import datetime
14from rdflib import Dataset, URIRef, Namespace, Literal
15from rdflib.namespace import RDF, XSD, DCTERMS
17CC0 = URIRef("https://creativecommons.org/publicdomain/zero/1.0/")
20def generate_provenance_snapshots(
21 input_directory: str,
22 output_file: str,
23 input_format: str | None = None,
24 output_format: str = "json-ld",
25 agent_orcid: str = "",
26 primary_source: str = "",
27):
28 """
29 Generate provenance snapshots from RDF data.
31 Args:
32 input_directory: Path to directory containing RDF files
33 output_file: Path to output file with provenance snapshots (N-Quads format)
34 input_format: Optional format to use for all input files (overrides auto-detection)
35 output_format: Format to use for output file (default: nquads)
36 agent_orcid: ORCID of the responsible agent
37 primary_source: URI of the primary source for the data
38 """
40 input_graph = Dataset()
41 default_graph = input_graph.graph()
43 file_count = 0
45 rdf_extensions = {
46 ".ttl": "turtle",
47 ".nt": "nt",
48 ".n3": "n3",
49 ".xml": "xml",
50 ".rdf": "xml",
51 ".jsonld": "json-ld",
52 ".nq": "nquads",
53 ".trig": "trig",
54 }
56 for filename in os.listdir(input_directory):
57 file_path = os.path.join(input_directory, filename)
59 if input_format:
60 format_name = input_format
61 else:
62 _, ext = os.path.splitext(filename.lower())
63 if ext not in rdf_extensions:
64 continue
65 format_name = rdf_extensions[ext]
67 default_graph.parse(file_path, format=format_name)
68 file_count += 1
70 if file_count == 0:
71 return
73 dataset = Dataset()
75 PROV = Namespace("http://www.w3.org/ns/prov#")
76 dataset.namespace_manager.bind("prov", PROV)
77 dataset.namespace_manager.bind("dcterms", DCTERMS)
79 dataset.default_graph.add((URIRef(""), DCTERMS.license, CC0))
81 for prefix, namespace in input_graph.namespace_manager.namespaces():
82 dataset.namespace_manager.bind(prefix, namespace)
84 license_subjects = {s for s, p, _ in default_graph if p == DCTERMS.license}
85 subjects = set()
86 for s, _, _ in default_graph:
87 if isinstance(s, URIRef) and s not in license_subjects:
88 subjects.add(s)
90 generation_time = (
91 datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat()
92 )
94 responsible_agent = URIRef(agent_orcid)
95 primary_source_uri = URIRef(primary_source)
97 for subject in subjects:
98 prov_graph_uri = URIRef(f"{subject}/prov/")
100 snapshot_uri = URIRef(f"{subject}/prov/se/1")
102 prov_graph = dataset.graph(identifier=prov_graph_uri)
104 prov_graph.add((snapshot_uri, RDF.type, PROV.Entity))
105 prov_graph.add((snapshot_uri, PROV.specializationOf, subject))
107 prov_graph.add(
108 (
109 snapshot_uri,
110 PROV.generatedAtTime,
111 Literal(generation_time, datatype=XSD.dateTime),
112 )
113 )
115 prov_graph.add((snapshot_uri, PROV.wasAttributedTo, responsible_agent))
117 prov_graph.add((snapshot_uri, PROV.hadPrimarySource, primary_source_uri))
119 description = f"Entity <{str(subject)}> was created"
120 prov_graph.add(
121 (snapshot_uri, DCTERMS.description, Literal(description, lang="en"))
122 )
124 dataset.serialize(destination=output_file, format=output_format)