Skip to content

Commit 040415b

Browse files
author
notactuallyfinn
committed
first part of the provenance collection draft
1 parent 23a11b3 commit 040415b

3 files changed

Lines changed: 225 additions & 2 deletions

File tree

src/hermes/commands/harvest/base.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,28 @@
1212
from hermes.error import HermesPluginRunError, MisconfigurationError
1313
from hermes.model.context_manager import HermesContext
1414
from hermes.model import SoftwareMetadata
15+
from hermes.model.provenance.ld_prov import ld_prov_list, ld_prov_node
16+
from hermes.model.types.ld_context import ALL_CONTEXTS
1517

1618

1719
class HermesHarvestPlugin(HermesPlugin):
1820
"""Base plugin that does harvesting.
1921
2022
TODO: describe the harvesting process and how this is mapped to this plugin.
2123
"""
24+
def __init__(self):
25+
self.io_operations: list[tuple[dict, dict, dict]] = []
26+
super().__init__()
2227

2328
def __call__(self, command: HermesCommand) -> SoftwareMetadata:
2429
pass
2530

31+
def load():
32+
pass
33+
34+
def write():
35+
pass
36+
2637

2738
class HarvestSettings(BaseModel):
2839
"""Generic harvesting settings."""
@@ -37,9 +48,11 @@ class HermesHarvestCommand(HermesCommand):
3748
settings_class = HarvestSettings
3849

3950
def __call__(self, args: argparse.Namespace) -> None:
40-
self.log.info("# Metadata harvesting")
4151
self.args = args
52+
self.log.info("# Load provenance from old harvest or create new document.")
53+
prov_doc, base_plugin = self.init_provenance_document()
4254

55+
self.log.info("# Metadata harvesting")
4356
if len(self.settings.sources) == 0:
4457
self.log.critical("# No harvest plugin was configured to be run and loaded.")
4558
raise MisconfigurationError("No harvest plugin was configured to be run and loaded.")
@@ -66,6 +79,24 @@ def __call__(self, args: argparse.Namespace) -> None:
6679
except Exception:
6780
self.log.exception(f"### Unknown error while executing the {plugin_name} plugin, skipping it now.")
6881
continue
82+
self.remove_provenance_info_for_plugin(prov_doc, plugin_name)
83+
84+
plugin = prov_doc.add_hermes_plugin("harvest", plugin_name)
85+
plugin_io_operations = plugin_func.io_operations # liste von drei Tupeln (input_file, load_function, output)
86+
for plugin_io_operation in plugin_io_operations:
87+
loaded_source = prov_doc.add_entity()
88+
loaded_source.update(plugin_io_operation[0])
89+
io_op = prov_doc.add_activity()
90+
plugin_io_operation[1]["prov:wasAssociatedWith"] = [base_plugin.ref, plugin.ref]
91+
plugin_io_operation[1]["prov:used"] = loaded_source.ref
92+
io_op.update(plugin_io_operation[1])
93+
loaded_data = prov_doc.add_entity()
94+
plugin_io_operation[2].update({
95+
"prov:wasAttributedTo": plugin.ref,
96+
"prov:wasDerivedFrom": loaded_source.ref,
97+
"prov:wasGeneratedBy": io_op.ref
98+
})
99+
loaded_data.update(plugin_io_operations[2])
69100

70101
self.log.info(f"### Store metadata harvested by {plugin_name} plugin")
71102
# store harvested data
@@ -76,3 +107,37 @@ def __call__(self, args: argparse.Namespace) -> None:
76107
if not harvested_any:
77108
self.log.critical("No harvest plugin ran successfully.")
78109
raise HermesPluginRunError("No harvest plugin ran successfully.")
110+
111+
def init_provenance_document(self) -> tuple[ld_prov_list, ld_prov_node]:
112+
ctx = HermesContext()
113+
ctx.prepare_step("harvest")
114+
with ctx["provenance"] as cache:
115+
try:
116+
ld_prov_doc = ld_prov_list.from_list(cache["codemeta"], container_type="@graph", context=ALL_CONTEXTS)
117+
return ld_prov_doc, ld_prov_doc.shallow_search({"schema:name": lambda doc, node: node["schema:name"][0].find("harvest base plugin") != -1})[0]
118+
except KeyError:
119+
pass
120+
prov_doc = ld_prov_list()
121+
prov_doc.init_hermes_agents()
122+
return prov_doc, prov_doc.add_hermes_base_plugin("harvest")
123+
124+
def remove_provenance_info_for_plugin(self, prov_doc, plugin) -> None:
125+
plugin = prov_doc.shallow_search({
126+
"schema:name": (lambda doc, node: f"harvest plugin {plugin}" in node["schema:name"]),
127+
})
128+
if len(plugin) == 0:
129+
return
130+
# two passes are needed because the nodes are nested exactly two levels
131+
related = prov_doc.shallow_search({
132+
"prov:wasAssociatedWith": (lambda doc, node: plugin.ref in node["prov:wasAssociatedWith"]),
133+
"prov:wasAttributedTo": (lambda doc, node: plugin.ref in node["prov:wasAttributedTo"])
134+
})
135+
ids = [plugin.ref, *(rel.ref for rel in related)]
136+
related = prov_doc.shallow_search({
137+
f"prov:{key}": (lambda doc, node: any(id in node[f"prov:{key}"] for id in ids)) for key in [
138+
"wasAssociatedWith", "wasAttributedTo", "wasGeneratedBy", "used", "wasDerivedFrom", "wasInformedBy"
139+
]
140+
})
141+
for item in related:
142+
items = prov_doc.shallow_search({"@id": (lambda doc, node: node["@id"] == item["@id"])})
143+
del prov_doc[items[0].index]
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# SPDX-FileCopyrightText: 2026 German Aerospace Center (DLR)
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# SPDX-FileContributor: Michael Fritzsche
6+
7+
from typing import Optional, Union
8+
from typing_extensions import Self
9+
import uuid
10+
11+
from hermes import utils
12+
from hermes.model.types import ld_dict, ld_list
13+
from hermes.model.types.ld_container import BASIC_TYPE, EXPANDED_JSON_LD_VALUE, JSON_LD_CONTEXT_DICT, TIME_TYPE
14+
from hermes.model.types.ld_context import ALL_CONTEXTS, iri_map
15+
16+
17+
class ld_prov_container:
18+
def _to_python(
19+
self: Self,
20+
full_iri: str,
21+
ld_value: Union[EXPANDED_JSON_LD_VALUE, dict[str, EXPANDED_JSON_LD_VALUE], list[str], str]
22+
) -> Union["ld_prov_node", "ld_prov_list", BASIC_TYPE, TIME_TYPE]:
23+
item = super()._to_python(full_iri, ld_value)
24+
if isinstance(item, ld_list):
25+
return ld_prov_list(
26+
data=item._data, parent=item.parent, key=item.key, index=item.index, context=item.context
27+
)
28+
elif isinstance(item, ld_dict):
29+
return ld_prov_node(
30+
data=item._data, parent=item.parent, key=item.key, index=item.index, context=item.context
31+
)
32+
return item
33+
34+
35+
class ld_prov_list(ld_list):
36+
NODE_IRI_FORMAT = "graph://{uuid}/{index}"
37+
PROV_DOC_IRI = iri_map['hermes-rt', "graph"]
38+
39+
def __init__(
40+
self: Self,
41+
*,
42+
data: EXPANDED_JSON_LD_VALUE = [{"@graph": []}],
43+
parent: Optional[Union[ld_dict, ld_list]] = None,
44+
key: Optional[str] = PROV_DOC_IRI,
45+
index: Optional[int] = None,
46+
context: Optional[list[Union[str, JSON_LD_CONTEXT_DICT]]] = ALL_CONTEXTS
47+
) -> None:
48+
self.id = uuid.uuid1()
49+
self.node_index = 0
50+
super().__init__([{"@graph": []}], parent=parent, key=key, index=index, context=context)
51+
52+
def __getitem__(
53+
self: Self, index: Union[int, slice]
54+
) -> Union[
55+
BASIC_TYPE,
56+
TIME_TYPE,
57+
"ld_prov_node",
58+
"ld_prov_list",
59+
list[Union[BASIC_TYPE, TIME_TYPE, "ld_prov_node", "ld_prov_list"]]
60+
]:
61+
item = super().__getitem__(index)
62+
if isinstance(item, ld_list):
63+
return ld_prov_list(
64+
data=item._data, parent=item.parent, key=item.key, index=item.index, context=item.context
65+
)
66+
elif isinstance(item, ld_dict):
67+
return ld_prov_node(
68+
data=item._data, parent=item.parent, key=item.key, index=item.index, context=item.context
69+
)
70+
return item
71+
72+
def next_node_iri(self) -> str:
73+
self.node_index += 1
74+
return self.NODE_IRI_FORMAT.format(uuid=self.id, index=self.node_index)
75+
76+
def add_activity(self) -> "ld_prov_node":
77+
self.append({"@id": self.next_node_iri(), "@type": "prov:Activity"})
78+
return self[-1]
79+
80+
def add_agent(self) -> "ld_prov_node":
81+
self.append({"@id": self.next_node_iri(), "@type": "prov:Agent"})
82+
return self[-1]
83+
84+
def add_entity(self) -> "ld_prov_node":
85+
self.append({"@id": self.next_node_iri(), "@type": "prov:Entity"})
86+
return self[-1]
87+
88+
def init_hermes_agents(self) -> "ld_prov_node":
89+
hermes = self.add_agent()
90+
hermes.update({
91+
"schema:name": utils.hermes_name,
92+
"schema:version": utils.hermes_version,
93+
"schema:url": utils.hermes_urls,
94+
})
95+
hermes["@type"].append("schema:SoftwareApplication")
96+
node = self.add_agent()
97+
node.update({
98+
"schema:name": utils.hermes_name + " cache",
99+
"schema:version": utils.hermes_version,
100+
"prov:actedOnBehalfOf": hermes.ref
101+
})
102+
node["@type"].append("schema:SoftwareApplication")
103+
return node
104+
105+
def add_hermes_command(self, step) -> "ld_prov_node":
106+
node = self.add_agent()
107+
node.update({
108+
"schema:name": f"{utils.hermes_name} {step} command",
109+
"schema:version": utils.hermes_version,
110+
"prov:actedOnBehalfOf": self.shallow_search(
111+
{"schema:name": (lambda doc, node: node["schema:name"] == utils.hermes_name)}
112+
)
113+
})
114+
node["@type"].append("schema:SoftwareApplication")
115+
return node
116+
117+
def add_hermes_base_plugin(self, step) -> "ld_prov_node":
118+
node = self.add_agent()
119+
node.update({
120+
"schema:name": f"{utils.hermes_name} {step} base plugin",
121+
"schema:version": utils.hermes_version,
122+
"prov:actedOnBehalfOf": self.shallow_search(
123+
{"schema:name": (lambda doc, node: node["schema:name"] == f"{utils.hermes_name} {step} command")}
124+
)
125+
})
126+
node["@type"].append("schema:SoftwareApplication")
127+
return node
128+
129+
def add_hermes_plugin(self, step, name) -> "ld_prov_node":
130+
node = self.add_agent()
131+
# TODO: add version
132+
node.update({
133+
"schema:name": f"{utils.hermes_name} {step} plugin '{name}'",
134+
"prov:actedOnBehalfOf": self.shallow_search(
135+
{"schema:name": (lambda doc, node: node["schema:name"] == f"{utils.hermes_name} {step} base plugin")}
136+
)
137+
})
138+
node["@type"].append("schema:SoftwareApplication")
139+
return node
140+
141+
def shallow_search(self, query: dict) -> list["ld_prov_node"]:
142+
return [
143+
item for item in self for key, test in query.items() if key in item and test(self, item)
144+
]
145+
146+
147+
class ld_prov_node(ld_dict):
148+
def __init__(
149+
self: Self,
150+
data: list[dict[str, EXPANDED_JSON_LD_VALUE]],
151+
*,
152+
parent: Optional[Union[ld_dict, ld_list]] = None,
153+
key: Optional[str] = None,
154+
index: Optional[int] = None,
155+
context: Optional[list[Union[str, JSON_LD_CONTEXT_DICT]]] = ALL_CONTEXTS
156+
) -> None:
157+
self.id = uuid.uuid1()
158+
super().__init__(data, parent=parent, key=key, index=index, context=context)

src/hermes/model/types/ld_list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ def from_list(
589589
key: Optional[str] = None,
590590
context: Optional[Union[str, JSON_LD_CONTEXT_DICT, list[Union[str, JSON_LD_CONTEXT_DICT]]]] = None,
591591
container_type: str = "@set"
592-
) -> ld_list:
592+
) -> Self:
593593
"""
594594
Creates a ld_list from the given list with the given parent, key, context and container_type.\n
595595
Note that only container_type '@set' is valid for key '@type'.\n

0 commit comments

Comments
 (0)