Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion hermes.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
[harvest]
sources = [ "cff", "toml" ] # ordered priority (first one is most important)

[curate]
method = "accept"

[deposit]
target = "invenio_rdm"

Expand All @@ -18,4 +21,3 @@ record_id = 13221384
depositions = "api/deposit/depositions"
licenses = "api/vocabularies/licenses"
communities = "api/communities"

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ hermes-marketplace = "hermes.commands.marketplace:main"
cff = "hermes.commands.harvest.cff:CffHarvestPlugin"
codemeta = "hermes.commands.harvest.codemeta:CodeMetaHarvestPlugin"

[project.entry-points."hermes.curate"]
accept = "hermes.commands.curate.accept:AcceptCuratePlugin"
Comment on lines +61 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we've made a decision to let the default plugins for each command live in hermes, but that's fine for now (#360).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean directly in hermes, i.e. accept = "hermes:AcceptCuratePlugin"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, as a separate repository you mean. I also missed the first "don't" 🤦🏻‍♂️


[project.entry-points."hermes.deposit"]
file = "hermes.commands.deposit.file:FileDepositPlugin"
invenio = "hermes.commands.deposit.invenio:InvenioDepositPlugin"
Expand Down
36 changes: 36 additions & 0 deletions src/hermes/commands/curate/accept.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR), 2025 Helmholtz-Zentrum Dresden-Rossendorf (HZDR)
#
# SPDX-License-Identifier: Apache-2.0

# SPDX-FileContributor: Michael Meinel
# SPDX-FileContributor: David Pape

import os
import shutil

from hermes.commands.curate.base import BaseCuratePlugin


class AcceptCuratePlugin(BaseCuratePlugin):
"""Accept plugin for the curation step.
This plugin creates a positive curation result, i.e. it accepts the produced
metadata as correct and lets the execution continue without human intervention. It
also copies the metadata produced in the process step to the "curate" directory.
"""
Comment on lines +14 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a plugin and not the default implementation in the base plugin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, this felt like the right way to do this. Accepting everything without check is one curation strategy, and this strategy is the default choice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could make the base class an ABC to make this clearer.


def get_decision(self):
"""Simulate positive curation result."""
return True

def process_decision_positive(self):
"""In case of positive curation result, copy files to next step."""
process_output = (
self.ctx.hermes_dir / "process" / (self.ctx.hermes_name + ".json")
)

os.makedirs(self.ctx.hermes_dir / "curate", exist_ok=True)
shutil.copy(
process_output,
self.ctx.hermes_dir / "curate" / (self.ctx.hermes_name + ".json"),
)
120 changes: 106 additions & 14 deletions src/hermes/commands/curate/base.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,106 @@
# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR), 2025 Helmholtz-Zentrum Dresden-Rossendorf (HZDR)
#
# SPDX-License-Identifier: Apache-2.0

# SPDX-FileContributor: Michael Meinel
# SPDX-FileContributor: David Pape

import argparse
import os
import shutil
import json
import sys

from pydantic import BaseModel

from hermes.commands.base import HermesCommand
from hermes.commands.base import HermesCommand, HermesPlugin
from hermes.model.context import CodeMetaContext
from hermes.model.errors import HermesValidationError
from hermes.model.path import ContextPath


class _CurateSettings(BaseModel):
"""Generic deposition settings."""
"""Generic curation settings."""

pass
#: Parameter by which the plugin is selected. By default, the accept plugin is used.
method: str = "accept"
Comment on lines +23 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above re plugin. method: str may be enough.



class BaseCuratePlugin(HermesPlugin):
"""Base class for curation plugins.

Objects of this class are callables.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll move it 😄

"""

def __init__(self, command, ctx):
self.command = command
self.ctx = ctx

def __call__(self, command: HermesCommand) -> None:
"""Entry point of the callable.

This method runs the main logic of the plugin. It calls the other methods of the
object in the correct order. Depending on the result of ``get_decision`` the
corresponding ``process_decision_*()`` method is called, based on the curation
decision.
"""
self.prepare()
self.validate()
self.create_report()
if self.get_decision():
self.process_decision_positive()
else:
self.process_decision_negative()

def prepare(self):
"""Prepare the plugin.

This method may be used to perform preparatory tasks such as configuration
checks, token permission checks, loading of resources, etc.
"""
pass

def validate(self):
"""Validate the metadata.

This method performs the validation of the metadata from the data model.
"""
pass

def create_report(self):
"""Create a curation report.

This method is responsible for creating any number of reports about the curation
process. These reports may be machine-readable, human-readable, or both.
"""
pass

def get_decision(self) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def get_decision(self) -> bool:
def is_publication_ready(self) -> bool:

Better naming, alternatively is_publication_allowed, is_approved, or similar.

"""Return the publication decision made through the curation process.

If publication is allowed, this method must return ``True``. By default,
``False`` is returned.
"""
return False

def process_decision_positive(self):
"""Process a positive curation decision.

This method is called if a positive publication decision was made in the
curation process.
"""
pass

def process_decision_negative(self):
"""Process a negative curation decision.

This method is called if a negative publication decision was made in the
curation process. By default, a ``RuntimeError`` is raised, halting the
execution.
"""
raise RuntimeError("Curation declined further processing")


class HermesCurateCommand(HermesCommand):
""" Curate the unified metadata before deposition. """
"""Curate the processed metadata before deposition."""

command_name = "curate"
settings_class = _CurateSettings
Expand All @@ -31,17 +109,31 @@ def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
pass

def __call__(self, args: argparse.Namespace) -> None:

self.log.info("# Metadata curation")
self.args = args
plugin_name = self.settings.method

ctx = CodeMetaContext()
process_output = ctx.hermes_dir / 'process' / (ctx.hermes_name + ".json")

if not process_output.is_file():
process_output = ctx.get_cache("process", ctx.hermes_name)
if not process_output.exists():
self.log.error(
"No processed metadata found. Please run `hermes process` before curation."
)
sys.exit(1)

os.makedirs(ctx.hermes_dir / 'curate', exist_ok=True)
shutil.copy(process_output, ctx.hermes_dir / 'curate' / (ctx.hermes_name + '.json'))
curate_path = ContextPath("curate")
with open(process_output) as process_output_fh:
ctx.update(curate_path, json.load(process_output_fh))

try:
plugin_func = self.plugins[plugin_name](self, ctx)

except KeyError as e:
self.log.error("Plugin '%s' not found.", plugin_name)
self.errors.append(e)

try:
plugin_func(self)

except HermesValidationError as e:
self.log.error("Error while executing %s: %s", plugin_name, e)
self.errors.append(e)