Skip to content

Commit a637cc3

Browse files
authored
Merge pull request #441 from softwarepub/feature/440-curation-step-base-class
Create more elaborate base class for curation plugins
2 parents 66076ed + d162a61 commit a637cc3

5 files changed

Lines changed: 151 additions & 15 deletions

File tree

hermes.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ sources = [ "cff", "toml", "file_exists" ] # ordered priority (first one is most
88
[harvest.file_exists.search_patterns]
99
community = ["contributing.md", "governance.md"]
1010

11+
[curate]
12+
method = "accept"
13+
1114
[deposit]
1215
target = "invenio_rdm"
1316

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ cff = "hermes.commands.harvest.cff:CffHarvestPlugin"
5959
codemeta = "hermes.commands.harvest.codemeta:CodeMetaHarvestPlugin"
6060
file_exists = "hermes.commands.harvest.file_exists:FileExistsHarvestPlugin"
6161

62+
[project.entry-points."hermes.curate"]
63+
accept = "hermes.commands.curate.accept:AcceptCuratePlugin"
64+
6265
[project.entry-points."hermes.deposit"]
6366
file = "hermes.commands.deposit.file:FileDepositPlugin"
6467
invenio = "hermes.commands.deposit.invenio:InvenioDepositPlugin"

src/hermes/commands/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ def __call__(self, args: argparse.Namespace):
162162

163163

164164
class HermesPlugin(abc.ABC):
165-
"""Base class for all HERMES plugins."""
165+
"""Base class for all HERMES plugins.
166+
167+
Objects of this class are callables.
168+
"""
166169

167170
settings_class: Optional[Type] = None
168171

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR), 2025 Helmholtz-Zentrum Dresden-Rossendorf (HZDR)
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# SPDX-FileContributor: Michael Meinel
6+
# SPDX-FileContributor: David Pape
7+
8+
import os
9+
import shutil
10+
11+
from hermes.commands.curate.base import BaseCuratePlugin
12+
13+
14+
class AcceptCuratePlugin(BaseCuratePlugin):
15+
"""Accept plugin for the curation step.
16+
17+
This plugin creates a positive curation result, i.e. it accepts the produced
18+
metadata as correct and lets the execution continue without human intervention. It
19+
also copies the metadata produced in the process step to the "curate" directory.
20+
"""
21+
22+
def is_publication_approved(self):
23+
"""Simulate positive curation result."""
24+
return True
25+
26+
def process_decision_positive(self):
27+
"""In case of positive curation result, copy files to next step."""
28+
process_output = (
29+
self.ctx.hermes_dir / "process" / (self.ctx.hermes_name + ".json")
30+
)
31+
32+
os.makedirs(self.ctx.hermes_dir / "curate", exist_ok=True)
33+
shutil.copy(
34+
process_output,
35+
self.ctx.hermes_dir / "curate" / (self.ctx.hermes_name + ".json"),
36+
)

src/hermes/commands/curate/base.py

Lines changed: 105 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,105 @@
1-
# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
1+
# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR), 2025 Helmholtz-Zentrum Dresden-Rossendorf (HZDR)
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

55
# SPDX-FileContributor: Michael Meinel
6+
# SPDX-FileContributor: David Pape
67

8+
from abc import abstractmethod
79
import argparse
8-
import os
9-
import shutil
10+
import json
1011
import sys
1112

1213
from pydantic import BaseModel
1314

14-
from hermes.commands.base import HermesCommand
15+
from hermes.commands.base import HermesCommand, HermesPlugin
1516
from hermes.model.context import CodeMetaContext
17+
from hermes.model.errors import HermesValidationError
18+
from hermes.model.path import ContextPath
1619

1720

1821
class _CurateSettings(BaseModel):
19-
"""Generic deposition settings."""
22+
"""Generic curation settings."""
2023

21-
pass
24+
#: Parameter by which the plugin is selected. By default, the accept plugin is used.
25+
method: str = "accept"
26+
27+
28+
class BaseCuratePlugin(HermesPlugin):
29+
"""Base class for curation plugins."""
30+
31+
def __init__(self, command, ctx):
32+
self.command = command
33+
self.ctx = ctx
34+
35+
def __call__(self, command: HermesCommand) -> None:
36+
"""Entry point of the callable.
37+
38+
This method runs the main logic of the plugin. It calls the other methods of the
39+
object in the correct order. Depending on the result of
40+
``is_publication_approved`` the corresponding ``process_decision_*()`` method is
41+
called, based on the curation decision.
42+
"""
43+
self.prepare()
44+
self.validate()
45+
self.create_report()
46+
if self.is_publication_approved():
47+
self.process_decision_positive()
48+
else:
49+
self.process_decision_negative()
50+
51+
def prepare(self):
52+
"""Prepare the plugin.
53+
54+
This method may be used to perform preparatory tasks such as configuration
55+
checks, token permission checks, loading of resources, etc.
56+
"""
57+
pass
58+
59+
def validate(self):
60+
"""Validate the metadata.
61+
62+
This method performs the validation of the metadata from the data model.
63+
"""
64+
pass
65+
66+
def create_report(self):
67+
"""Create a curation report.
68+
69+
This method is responsible for creating any number of reports about the curation
70+
process. These reports may be machine-readable, human-readable, or both.
71+
"""
72+
pass
73+
74+
@abstractmethod
75+
def is_publication_approved(self) -> bool:
76+
"""Return the publication decision made through the curation process.
77+
78+
If publication is allowed, this method must return ``True``, otherwise
79+
``False``.
80+
"""
81+
pass
82+
83+
def process_decision_positive(self):
84+
"""Process a positive curation decision.
85+
86+
This method is called if a positive publication decision was made in the
87+
curation process.
88+
"""
89+
pass
90+
91+
def process_decision_negative(self):
92+
"""Process a negative curation decision.
93+
94+
This method is called if a negative publication decision was made in the
95+
curation process. By default, a ``RuntimeError`` is raised, halting the
96+
execution.
97+
"""
98+
raise RuntimeError("Curation declined further processing")
2299

23100

24101
class HermesCurateCommand(HermesCommand):
25-
""" Curate the unified metadata before deposition. """
102+
"""Curate the processed metadata before deposition."""
26103

27104
command_name = "curate"
28105
settings_class = _CurateSettings
@@ -31,17 +108,31 @@ def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
31108
pass
32109

33110
def __call__(self, args: argparse.Namespace) -> None:
34-
35-
self.log.info("# Metadata curation")
111+
self.args = args
112+
plugin_name = self.settings.method
36113

37114
ctx = CodeMetaContext()
38-
process_output = ctx.hermes_dir / 'process' / (ctx.hermes_name + ".json")
39-
40-
if not process_output.is_file():
115+
process_output = ctx.get_cache("process", ctx.hermes_name)
116+
if not process_output.exists():
41117
self.log.error(
42118
"No processed metadata found. Please run `hermes process` before curation."
43119
)
44120
sys.exit(1)
45121

46-
os.makedirs(ctx.hermes_dir / 'curate', exist_ok=True)
47-
shutil.copy(process_output, ctx.hermes_dir / 'curate' / (ctx.hermes_name + '.json'))
122+
curate_path = ContextPath("curate")
123+
with open(process_output) as process_output_fh:
124+
ctx.update(curate_path, json.load(process_output_fh))
125+
126+
try:
127+
plugin_func = self.plugins[plugin_name](self, ctx)
128+
129+
except KeyError as e:
130+
self.log.error("Plugin '%s' not found.", plugin_name)
131+
self.errors.append(e)
132+
133+
try:
134+
plugin_func(self)
135+
136+
except HermesValidationError as e:
137+
self.log.error("Error while executing %s: %s", plugin_name, e)
138+
self.errors.append(e)

0 commit comments

Comments
 (0)