Skip to content

Commit 630dd00

Browse files
author
notactuallyfinn
committed
updated documentation for plugin development, made curate pluginizable and changed the way commands handle errors in plugin runs
1 parent 5e296cb commit 630dd00

17 files changed

Lines changed: 475 additions & 124 deletions

File tree

docs/source/_static/custom.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,14 @@ img {
4545
}
4646

4747
.bd-sidebar-primary.bd-sidebar {
48-
max-width: 340px;
48+
max-width: min-content;
49+
}
50+
51+
.bd-docs-nav {
52+
min-width: max-content;
4953
}
5054

5155
.bd-sidebar-secondary{
52-
max-width: min-content;
56+
max-width: 15%;
57+
width: max-content;
5358
}

docs/source/tutorials/automated-publication-with-ci.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Each step in the publication workflow has its own section.
110110

111111
Configure HERMES to:
112112

113-
- harvest metadata from Git and `CITATION.cff`
113+
- harvest metadata from `CITATION.cff`
114114
- deposit on Zenodo Sandbox (which is built on the InvenioRDM)
115115
- use Zenodo Sandbox as the target publication repository
116116

docs/source/tutorials/writing-a-plugin-for-hermes.md

Lines changed: 280 additions & 61 deletions
Large diffs are not rendered by default.

hermes.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
[harvest]
66
sources = [ "cff", "toml" ] # ordered priority (first one is most important)
77

8+
[curate]
9+
plugin = "pass_curate"
10+
811
[deposit]
912
target = "invenio_rdm"
1013

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ cff_doi = "hermes.commands.postprocess.invenio:cff_doi"
7474
[project.entry-points."hermes.process"]
7575
codemeta = "hermes.commands.process.standard_merge:CodemetaProcessPlugin"
7676

77+
[project.entry-points."hermes.curate"]
78+
pass_curate = "hermes.commands.curate.pass_curate:DoNothingCuratePlugin"
79+
7780
[tool.poetry.group.dev.dependencies]
7881
pytest = "^7.1.1"
7982
pytest-cov = "^3.0.0"

src/hermes/commands/cli.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
HermesProcessCommand, HermesVersionCommand
2222
)
2323
from hermes.commands.base import HermesCommand
24+
from hermes.error import HermesPluginRunError
2425

2526

2627
def main() -> None:
@@ -79,16 +80,15 @@ def main() -> None:
7980

8081
log.info("Run subcommand %s", args.command.command_name)
8182
args.command(args)
83+
except HermesPluginRunError as e:
84+
log.error("An error occurred during the execution of a plugin %s (Find details in './hermes.log')",
85+
args.command.command_name)
86+
log.debug("Original exception was: %s", e)
87+
sys.exit(2)
8288
except Exception as e:
8389
log.error("An error occurred during execution of %s (Find details in './hermes.log')",
8490
args.command.command_name)
8591
log.debug("Original exception was: %s", e)
86-
87-
sys.exit(2)
88-
89-
if args.command.errors:
90-
for e in args.command.errors:
91-
log.error(e)
9292
sys.exit(1)
9393

9494
sys.exit(0)

src/hermes/commands/curate/base.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,24 @@
88

99
from pydantic import BaseModel
1010

11-
from hermes.commands.base import HermesCommand
11+
from hermes.commands.base import HermesCommand, HermesPlugin
12+
from hermes.error import HermesPluginRunError, MisconfigurationError
1213
from hermes.model import SoftwareMetadata
1314
from hermes.model.context_manager import HermesContext
1415
from hermes.model.error import HermesValidationError
1516

1617

18+
class HermesCuratePlugin(HermesPlugin):
19+
""" Base plugin for curate plugins. """
20+
21+
def __call__(self, command: HermesCommand, metadata: SoftwareMetadata) -> SoftwareMetadata:
22+
pass
23+
24+
1725
class CurateSettings(BaseModel):
1826
"""Generic deposition settings."""
1927

20-
pass
28+
plugin: str = ""
2129

2230

2331
class HermesCurateCommand(HermesCommand):
@@ -26,28 +34,41 @@ class HermesCurateCommand(HermesCommand):
2634
command_name = "curate"
2735
settings_class = CurateSettings
2836

29-
def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
30-
pass
31-
3237
def __call__(self, args: argparse.Namespace) -> None:
3338
self.log.info("# Metadata curation")
39+
plugin_name = self.settings.plugin
3440

3541
ctx = HermesContext()
3642
ctx.prepare_step("curate")
3743

44+
self.log.info("## Load processed metadata")
45+
# load processed data
3846
ctx.prepare_step("process")
39-
with ctx["result"] as process_ctx:
40-
expanded_data = process_ctx["expanded"]
41-
context_data = process_ctx["context"]
47+
try:
48+
metadata = SoftwareMetadata.load_from_cache(ctx, "result")
49+
except Exception as e:
50+
self.log.error("The data from the process step could not be loaded or is invalid for some reason.")
51+
raise HermesValidationError("The results of the process step are invalid.") from e
4252
ctx.finalize_step("process")
4353

54+
self.log.info("## Load curation plugin")
55+
# load plugin
4456
try:
45-
data = SoftwareMetadata(expanded_data[0], context_data["@context"][1])
57+
plugin_func = self.plugins[plugin_name]()
58+
except KeyError as e:
59+
self.log.error(f"Plugin {plugin_name} not found.")
60+
raise MisconfigurationError(f"Curate plugin {plugin_name} not found.")
61+
62+
self.log.info("## Run curation plugin")
63+
# run plugin
64+
try:
65+
curated_metadata = plugin_func(self, metadata)
4666
except Exception as e:
47-
raise HermesValidationError("The results of the process step are invalid.") from e
67+
self.log.error(f"Unknown error while executing the {plugin_name} plugin.")
68+
raise HermesPluginRunError(f"Something went wrong while running the curate plugin {plugin_name}") from e
4869

49-
with ctx["result"] as curate_ctx:
50-
curate_ctx["expanded"] = data.ld_value
51-
curate_ctx["context"] = {"@context": data.full_context}
70+
self.log.info("## Store curated data")
71+
# store metadata
72+
curated_metadata.write_to_cache(ctx, "result")
5273

5374
ctx.finalize_step("curate")
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from pydantic import BaseModel
2+
3+
from hermes.model import SoftwareMetadata
4+
from .base import HermesCurateCommand, HermesCuratePlugin
5+
6+
7+
class DoNothingCurateSettings(BaseModel):
8+
pass
9+
10+
11+
class DoNothingCuratePlugin(HermesCuratePlugin):
12+
settings_class = DoNothingCurateSettings
13+
14+
def __call__(self, command: HermesCurateCommand, metadata: SoftwareMetadata) -> SoftwareMetadata:
15+
return metadata

src/hermes/commands/deposit/base.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pydantic import BaseModel
1212

1313
from hermes.commands.base import HermesCommand, HermesPlugin
14+
from hermes.error import HermesPluginRunError, MisconfigurationError
1415
from hermes.model.context_manager import HermesContext
1516
from hermes.model import SoftwareMetadata
1617
from hermes.model.error import HermesValidationError
@@ -29,25 +30,26 @@ def __call__(self, command: HermesCommand) -> None:
2930
"""
3031
self.command = command
3132
self.ctx = HermesContext()
33+
self.ctx.prepare_step("deposit")
3234

3335
self.ctx.prepare_step("curate")
34-
self.metadata = SoftwareMetadata.load_from_cache(self.ctx, "result")
36+
try:
37+
self.metadata = SoftwareMetadata.load_from_cache(self.ctx, "result")
38+
except Exception as e:
39+
raise HermesValidationError("The results of the curate step are invalid.") from e
3540
self.ctx.finalize_step("curate")
3641

3742
self.prepare()
3843
deposit = self.map_metadata()
39-
self.ctx.prepare_step("deposit")
4044
with self.ctx[command.settings.target] as cache:
4145
cache["deposit"] = deposit
42-
self.ctx.finalize_step("deposit")
4346

4447
if self.is_initial_publication():
4548
self.create_initial_version()
4649
else:
4750
self.create_new_version()
4851

4952
deposit = self.update_metadata()
50-
self.ctx.prepare_step("deposit")
5153
with self.ctx[command.settings.target] as cache:
5254
cache["result"] = deposit
5355
self.ctx.finalize_step("deposit")
@@ -133,16 +135,24 @@ def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
133135
help="Allow initial deposition (i.e., minting a new PID).")
134136

135137
def __call__(self, args: argparse.Namespace) -> None:
138+
self.log.info("# Metadata deposition")
136139
self.args = args
137140
plugin_name = self.settings.target
138141

142+
self.log.info("## Load deposit plugin")
143+
# load plugin
139144
try:
140145
plugin_func = self.plugins[plugin_name]()
141146
except KeyError as e:
142-
self.log.error("Plugin '%s' not found.", plugin_name)
143-
self.errors.append(e)
147+
self.log.error(f"Plugin {plugin_name} not found.")
148+
raise MisconfigurationError(f"Deposit plugin {self.settings.plugin} not found.")
149+
150+
self.log.info("## Run deposit plugin")
151+
# run plugin
144152
try:
145153
plugin_func(self)
146154
except HermesValidationError as e:
147-
self.log.error("Error while executing %s: %s", plugin_name, e)
148-
self.errors.append(e)
155+
self.log.error(f"Error while executing {plugin_name}: {e}")
156+
raise HermesPluginRunError(
157+
f"Something went wrong while running the curate plugin {self.settings.plugin}"
158+
) from e

src/hermes/commands/harvest/base.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pydantic import BaseModel
1010

1111
from hermes.commands.base import HermesCommand, HermesPlugin
12+
from hermes.error import HermesPluginRunError, MisconfigurationError
1213
from hermes.model.context_manager import HermesContext
1314
from hermes.model.error import HermesValidationError
1415
from hermes.model import SoftwareMetadata
@@ -20,7 +21,7 @@ class HermesHarvestPlugin(HermesPlugin):
2021
TODO: describe the harvesting process and how this is mapped to this plugin.
2122
"""
2223

23-
def __call__(self, command: HermesCommand) -> tuple[SoftwareMetadata, dict]:
24+
def __call__(self, command: HermesCommand) -> SoftwareMetadata:
2425
pass
2526

2627

@@ -37,28 +38,35 @@ class HermesHarvestCommand(HermesCommand):
3738
settings_class = HarvestSettings
3839

3940
def __call__(self, args: argparse.Namespace) -> None:
41+
self.log.info("# Metadata harvesting")
4042
self.args = args
4143

4244
# Initialize the harvest cache directory here to indicate the step ran
4345
ctx = HermesContext()
4446
ctx.prepare_step('harvest')
4547

48+
self.log.info("## Load and run the plugins")
4649
for plugin_name in self.settings.sources:
47-
plugin_cls = self.plugins[plugin_name]
50+
self.log.info(f"### Load {plugin_name} plugin")
51+
# load plugin
52+
try:
53+
plugin_func = self.plugins[plugin_name]()
54+
except KeyError as e:
55+
self.log.error(f"Plugin {plugin_name} not found.")
56+
raise MisconfigurationError(f"Harvest plugin {plugin_name} not found.")
4857

58+
self.log.info(f"### Run {plugin_name} plugin")
59+
# run plugin
4960
try:
50-
# Load plugin and run the harvester
51-
plugin_func = plugin_cls()
5261
harvested_data = plugin_func(self)
53-
54-
with ctx[plugin_name] as plugin_ctx:
55-
plugin_ctx["codemeta"] = harvested_data[0].compact()
56-
plugin_ctx["context"] = {"@context": harvested_data[0].full_context}
57-
58-
plugin_ctx["expanded"] = harvested_data[0].ld_value
59-
60-
except HermesValidationError as e:
61-
self.log.error("Error while executing %s: %s", plugin_name, e)
62-
self.errors.append(e)
62+
except Exception as e:
63+
self.log.error(f"Unknown error while executing the {plugin_name} plugin.")
64+
raise HermesPluginRunError(
65+
f"Something went wrong while running the harvest plugin {plugin_name}"
66+
) from e
67+
68+
self.log.info(f"### Store metadata harvested by {plugin_name} plugin")
69+
# store harvested data
70+
harvested_data.write_to_cache(ctx, plugin_name)
6371

6472
ctx.finalize_step('harvest')

0 commit comments

Comments
 (0)