Skip to content

Commit 907679b

Browse files
committed
Update plugins to have a common class interface and to use the new data model.
1 parent d9a5326 commit 907679b

13 files changed

Lines changed: 348 additions & 203 deletions

File tree

src/hermes/commands/base.py

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from pydantic import BaseModel
1616
from pydantic_settings import BaseSettings, SettingsConfigDict
1717

18+
from hermes import utils
19+
from hermes.model.prov.ld_prov import ld_prov, ld_prov_node
20+
from hermes.model.types import ld_context
21+
1822

1923
class HermesSettings(BaseSettings):
2024
"""Root class for HERMES configuration model."""
@@ -33,15 +37,53 @@ class HermesCommand(abc.ABC):
3337
command_name: str = ""
3438
settings_class: Type = HermesSettings
3539

40+
class prov:
41+
@classmethod
42+
def hermes_software(cls):
43+
return {
44+
"@type": "schema:SoftwareApplication",
45+
"schema:name": utils.hermes_name,
46+
"schema:version": utils.hermes_version,
47+
"schema:url": utils.hermes_homepage,
48+
}
49+
50+
@classmethod
51+
def hermes_command(cls, cmd, app):
52+
return {
53+
"@type": "schema:SoftwareApplication",
54+
"schema:name": cmd.command_name,
55+
"schema:isPartOf": app.ref,
56+
}
57+
58+
@classmethod
59+
def hermes_plugin_run(cls, plugin, command):
60+
return {
61+
"prov:wasStaredBy": command.ref,
62+
}
63+
64+
@classmethod
65+
def hermes_json_data(cls, name, data):
66+
return {
67+
"@type": "schema:PropertyValue",
68+
"schema:name": name,
69+
"schema:value": {"@type": "@json", "@value": data.compact()},
70+
}
71+
72+
3673
def __init__(self, parser: argparse.ArgumentParser):
3774
"""Initialize a new instance of any HERMES command.
3875
3976
:param parser: The command line parser used for reading command line arguments.
4077
"""
78+
self.prov_doc = ld_prov(context=ld_context.ALL_CONTEXTS)
79+
self.app_entity = self.prov_doc.make_node('Entity', self.prov.hermes_software())
80+
4181
self.parser = parser
4282
self.plugins = self.init_plugins()
4383
self.settings = None
4484

85+
self.app_entity.commit()
86+
4587
self.log = logging.getLogger(f"hermes.{self.command_name}")
4688
self.errors = []
4789

@@ -50,17 +92,20 @@ def init_plugins(self):
5092

5193
# Collect all entry points for this group (i.e., all valid plug-ins for the step)
5294
entry_point_group = f"hermes.{self.command_name}"
53-
group_plugins = {
54-
entry_point.name: entry_point.load()
55-
for entry_point in metadata.entry_points(group=entry_point_group)
56-
}
95+
group_plugins = {}
96+
group_settings = {}
97+
98+
for entry_point in metadata.entry_points(group=entry_point_group):
99+
plugin_cls = entry_point.load()
100+
ep_metadata = plugin_cls.get_metadata(entry_point)
101+
102+
plugin_cls.plugin_node = self.app_entity.add_related("schema:hasPart", "Entity", ep_metadata)
57103

58-
# Collect the plug-in specific configurations
59-
self.derive_settings_class({
60-
plugin_name: plugin_class.settings_class
61-
for plugin_name, plugin_class in group_plugins.items()
62-
if hasattr(plugin_class, "settings_class") and plugin_class.settings_class is not None
63-
})
104+
group_plugins[entry_point.name] = plugin_cls
105+
if hasattr(plugin_cls, 'settings_class') and plugin_cls.settings_class is not None:
106+
group_settings[entry_point.name] = plugin_cls.settings_class
107+
108+
self.derive_settings_class(group_settings)
64109

65110
return group_plugins
66111

@@ -160,8 +205,22 @@ def __call__(self, args: argparse.Namespace):
160205
class HermesPlugin(abc.ABC):
161206
"""Base class for all HERMES plugins."""
162207

208+
pluing_node = None
209+
163210
settings_class: Optional[Type] = None
164211

212+
@classmethod
213+
def get_metadata(cls, entry_point):
214+
cls.entry_point = entry_point
215+
216+
return {
217+
"@type": "schema:EntryPoint",
218+
"schema:name": entry_point.name,
219+
}
220+
221+
def __init__(self, plugin_prov):
222+
self.prov_doc = plugin_prov
223+
165224
@abc.abstractmethod
166225
def __call__(self, command: HermesCommand) -> None:
167226
"""Execute the plugin.

src/hermes/commands/clean/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ def __call__(self, args: argparse.Namespace) -> None:
2727
self.log.info("Removing HERMES caches...")
2828

2929
# Naive implementation for now... check errors, validate directory, don't construct the path ourselves, etc.
30-
shutil.rmtree(args.path / '.hermes')
30+
cache_path = args.path / '.hermes'
31+
if cache_path.exists():
32+
shutil.rmtree(cache_path)

src/hermes/commands/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def main() -> None:
7272

7373
log.info("Run subcommand %s", args.command.command_name)
7474
args.command(args)
75-
except Exception as e:
75+
except RuntimeError as e:
7676
log.error("An error occurred during execution of %s", args.command.command_name)
7777
log.debug("Original exception was: %s", e)
7878

src/hermes/commands/curate/base.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
# SPDX-FileContributor: Michael Meinel
66

77
import argparse
8+
import json
89
import os
910
import shutil
1011
import sys
1112

1213
from pydantic import BaseModel
1314

1415
from hermes.commands.base import HermesCommand
15-
from hermes.model.context import CodeMetaContext
16+
from hermes.model.context_manager import HermesContext
17+
from hermes.model.types import ld_dict, ld_list
1618

1719

1820
class CurateSettings(BaseModel):
@@ -34,14 +36,33 @@ def __call__(self, args: argparse.Namespace) -> None:
3436

3537
self.log.info("# Metadata curation")
3638

37-
ctx = CodeMetaContext()
38-
process_output = ctx.hermes_dir / 'process' / (ctx.hermes_name + ".json")
39+
ctx = HermesContext()
40+
ctx.prepare_step("curate")
3941

40-
if not process_output.is_file():
41-
self.log.error(
42-
"No processed metadata found. Please run `hermes process` before curation."
43-
)
44-
sys.exit(1)
42+
ctx.prepare_step("process")
43+
with ctx["result"] as process_ctx:
44+
expanded_data = process_ctx["expanded"]
45+
context_data = process_ctx["context"]
46+
prov_data = process_ctx["prov"]
47+
ctx.finalize_step("process")
4548

46-
os.makedirs(ctx.hermes_dir / 'curate', exist_ok=True)
47-
shutil.copy(process_output, ctx.hermes_dir / 'curate' / (ctx.hermes_name + '.json'))
49+
prov_doc = ld_dict.from_dict({"hermes-rt:graph": prov_data, "@context": prov_data["@context"]})
50+
51+
nodes = {}
52+
edges = {}
53+
54+
for node in prov_doc["hermes-rt:graph"]:
55+
nodes[node["@id"]] = node
56+
57+
for rel in ('schema:isPartOf', "schema:hasPart", "prov:used", "prov:generated", "prov:wasStartedBy"):
58+
if rel in node:
59+
rel_ids = node[rel]
60+
if not isinstance(rel_ids, ld_list):
61+
rel_ids = [rel_ids]
62+
edges[rel] = edges.get(rel, []) + [(node["@id"], rel_id) for rel_id in rel_ids]
63+
64+
with ctx["result"] as curate_ctx:
65+
curate_ctx["expanded"] = expanded_data
66+
curate_ctx["context"] = context_data
67+
68+
ctx.finalize_step("curate")

src/hermes/commands/deposit/base.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77

88
import abc
99
import argparse
10-
import json
11-
import sys
1210

1311
from pydantic import BaseModel
1412

1513
from hermes.commands.base import HermesCommand, HermesPlugin
16-
from hermes.model.context import CodeMetaContext
17-
from hermes.model.path import ContextPath
14+
from hermes.model.context_manager import HermesContext
15+
from hermes.model.types import ld_dict
1816
from hermes.model.errors import HermesValidationError
1917

2018

@@ -24,16 +22,22 @@ class BaseDepositPlugin(HermesPlugin):
2422
TODO: describe workflow... needs refactoring to be less stateful!
2523
"""
2624

27-
def __init__(self, command, ctx):
28-
self.command = command
29-
self.ctx = ctx
30-
3125
def __call__(self, command: HermesCommand) -> None:
3226
"""Initiate the deposition process.
3327
3428
This calls a list of additional methods on the class, none of which need to be implemented.
3529
"""
3630
self.command = command
31+
ctx = HermesContext()
32+
ctx.prepare_step("deposit")
33+
34+
ctx.prepare_step("curate")
35+
with ctx["result"] as curate_ctx:
36+
expanded_data = curate_ctx["expanded"]
37+
context_data = curate_ctx["context"]
38+
ctx.finalize_step("curate")
39+
40+
self.ctx = ld_dict(expanded_data, context=context_data)
3741

3842
self.prepare()
3943
self.map_metadata()
@@ -128,26 +132,12 @@ def __call__(self, args: argparse.Namespace) -> None:
128132
self.args = args
129133
plugin_name = self.settings.target
130134

131-
ctx = CodeMetaContext()
132-
codemeta_file = ctx.get_cache("curate", ctx.hermes_name)
133-
if not codemeta_file.exists():
134-
self.log.error("You must run the 'curate' command before deposit")
135-
sys.exit(1)
136-
137-
codemeta_path = ContextPath("codemeta")
138-
with open(codemeta_file) as codemeta_fh:
139-
ctx.update(codemeta_path, json.load(codemeta_fh))
140-
141135
try:
142-
plugin_func = self.plugins[plugin_name](self, ctx)
143-
136+
plugin_func = self.plugins[plugin_name](self.prov_doc)
137+
plugin_func(self)
144138
except KeyError as e:
145139
self.log.error("Plugin '%s' not found.", plugin_name)
146140
self.errors.append(e)
147-
148-
try:
149-
plugin_func(self)
150-
151141
except HermesValidationError as e:
152142
self.log.error("Error while executing %s: %s", plugin_name, e)
153143
self.errors.append(e)

src/hermes/commands/deposit/file.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,34 @@
1111
from pydantic import BaseModel
1212

1313
from hermes.commands.deposit.base import BaseDepositPlugin
14-
from hermes.model.path import ContextPath
14+
from hermes.model.types import ld_list, ld_dict, ld_context
1515

1616

1717
class FileDepositSettings(BaseModel):
18-
filename: str = 'hermes.json'
18+
filename: str = 'codemeta.json'
19+
include_prov: bool = False
1920

2021

2122
class FileDepositPlugin(BaseDepositPlugin):
2223
settings_class = FileDepositSettings
2324

2425
def map_metadata(self) -> None:
25-
self.ctx.update(ContextPath.parse('deposit.file'), self.ctx['codemeta'])
26+
if not self.command.settings.file.include_prov:
27+
self._strip_namespace(self.ctx, ld_context.HERMES_RT_PREFIX)
28+
29+
def _strip_namespace(self, ctx, ns):
30+
if isinstance(ctx, ld_dict):
31+
for key, value in [*ctx.items()]:
32+
if key.startswith(ns):
33+
del ctx[key]
34+
else:
35+
self._strip_namespace(value, ns)
36+
elif isinstance(ctx, ld_list):
37+
for item in ctx:
38+
self._strip_namespace(item, ns)
2639

2740
def publish(self) -> None:
2841
file_config = self.command.settings.file
29-
output_data = self.ctx['deposit.file']
3042

3143
with open(file_config.filename, 'w') as deposition_file:
32-
json.dump(output_data, deposition_file, indent=2)
44+
json.dump(self.ctx.compact(), deposition_file, indent=2)

0 commit comments

Comments
 (0)