From 7d0cd30a216814ae53b4e7ca56613b745afbd020 Mon Sep 17 00:00:00 2001 From: David Pape Date: Fri, 26 Jun 2026 15:45:55 +0200 Subject: [PATCH 1/7] Make the plugin ready for the new data model --- hermes.toml | 2 +- pyproject.toml | 2 +- src/hermes_plugin_software_card/curate.py | 58 ++++++++++++++++------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/hermes.toml b/hermes.toml index c83d9fa..8957f87 100644 --- a/hermes.toml +++ b/hermes.toml @@ -7,4 +7,4 @@ sources = ["cff"] [curate] -method = "software_card" +plugin = "software_card" diff --git a/pyproject.toml b/pyproject.toml index 1b16f2e..f67851b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ ] requires-python = ">=3.11" dependencies = [ - "hermes @ git+https://github.com/softwarepub/hermes.git@develop", + "hermes @ git+https://github.com/softwarepub/hermes.git@refactor/data-model", "software-card-policies @ git+https://github.com/softwarepub/software-card-policies.git", ] diff --git a/src/hermes_plugin_software_card/curate.py b/src/hermes_plugin_software_card/curate.py index 3fe7f70..22d24dd 100644 --- a/src/hermes_plugin_software_card/curate.py +++ b/src/hermes_plugin_software_card/curate.py @@ -6,9 +6,11 @@ """Module containing the Software CaRD curation plugin for HERMES.""" import json -from pathlib import Path -from hermes.commands.curate.base import BaseCuratePlugin +from hermes.commands.base import HermesCommand +from hermes.commands.curate.base import HermesCuratePlugin +from hermes.model import SoftwareMetadata +from hermes.model.hermes_cache import HermesCacheManager from software_card_policies.config import Config from software_card_policies.data_model import ( make_shacl_graph, @@ -20,12 +22,12 @@ from hermes_plugin_software_card import environment -class SoftwareCaRDCuratePlugin(BaseCuratePlugin): +class SoftwareCaRDCuratePlugin(HermesCuratePlugin): """Software CaRD curation plugin.""" - def __init__(self, command, ctx): + def __init__(self): """Initialize the plugin.""" - super().__init__(command, ctx) + super().__init__() self._data_graph = None self._shacl_graph = None self._conforms = False @@ -65,14 +67,34 @@ def __init__(self, command, ctx): } } - def prepare(self): + def __call__( + self, command: HermesCommand, metadata: SoftwareMetadata + ) -> SoftwareMetadata: + """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 + ``is_publication_approved`` the corresponding ``process_decision_*()`` method is + called, based on the curation decision. + """ + self.prepare(metadata) + self.validate() + self.create_report(metadata) + + if not self.is_publication_approved(): + return SoftwareMetadata() + + return metadata + + def prepare(self, metadata: SoftwareMetadata): """Prepare the validation. The metadata given in the context is parsed as an RDF graph and then validated using the Software CaRD validation. """ - text = json.dumps(self.ctx.get_data()["curate"]) - self._data_graph = read_rdf_resource(format="json-ld", data=text) + self._data_graph = read_rdf_resource( + format="json-ld", data=json.dumps(metadata.ld_value) + ) self._shacl_graph = make_shacl_graph(Config.from_dict(self._validation_config)) def validate(self): @@ -81,8 +103,17 @@ def validate(self): self._conforms = conforms self._validation_graph = validation_graph - def create_report(self): - """Create basic text report.""" + def create_report(self, metadata: SoftwareMetadata): + """Create validation report. + + This creates the report both as a machine-readble JSON-LD file, and prints the + URL to the Software CaRD web app to the screen. + """ + ctx = HermesCacheManager() + validation_file = ctx.cache_dir / "curate" / "validation.json" + validation_file.parent.mkdir(exist_ok=True, parents=True) + self._validation_graph.serialize(validation_file, format="json-ld") + self._report = create_report(self._validation_graph) if self._environment is None: print("Software CaRD plugin not running in CI environment.") @@ -95,10 +126,3 @@ def create_report(self): def is_publication_approved(self) -> bool: """Decide whether the publication of the software is approved.""" return self._conforms - - def process_decision_positive(self): - """Write the given metadata into the curate directory.""" - curate_output = Path(self.ctx.get_cache("curate", self.ctx.hermes_name)) - Path.mkdir(curate_output.parent) - with open(curate_output, "w") as curate_output_fh: - json.dump(self.ctx.get_data(), curate_output_fh) From 22df30e1236545f4ceaa508a6650b7d58af52779 Mon Sep 17 00:00:00 2001 From: David Pape Date: Fri, 26 Jun 2026 15:53:41 +0200 Subject: [PATCH 2/7] Update docstring --- src/hermes_plugin_software_card/curate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hermes_plugin_software_card/curate.py b/src/hermes_plugin_software_card/curate.py index 22d24dd..c1481a7 100644 --- a/src/hermes_plugin_software_card/curate.py +++ b/src/hermes_plugin_software_card/curate.py @@ -74,8 +74,8 @@ def __call__( 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 - ``is_publication_approved`` the corresponding ``process_decision_*()`` method is - called, based on the curation decision. + ``is_publication_approved`` either the valid metadata or a new, empty + ``SoftwareMetadata`` object is returned. """ self.prepare(metadata) self.validate() From df8f6ce0717d71796025622a0e226f423d59f3a0 Mon Sep 17 00:00:00 2001 From: David Pape Date: Tue, 30 Jun 2026 08:08:49 +0200 Subject: [PATCH 3/7] Apply suggestions from review --- src/hermes_plugin_software_card/curate.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/hermes_plugin_software_card/curate.py b/src/hermes_plugin_software_card/curate.py index c1481a7..7658f36 100644 --- a/src/hermes_plugin_software_card/curate.py +++ b/src/hermes_plugin_software_card/curate.py @@ -7,8 +7,7 @@ import json -from hermes.commands.base import HermesCommand -from hermes.commands.curate.base import HermesCuratePlugin +from hermes.commands.curate.base import HermesCurateCommand, HermesCuratePlugin from hermes.model import SoftwareMetadata from hermes.model.hermes_cache import HermesCacheManager from software_card_policies.config import Config @@ -32,7 +31,6 @@ def __init__(self): self._shacl_graph = None self._conforms = False self._validation_graph = None - self._report = None self._environment = environment.get() self._app_base_url = "https://software-metadata.pub/software-card/" @@ -68,7 +66,7 @@ def __init__(self): } def __call__( - self, command: HermesCommand, metadata: SoftwareMetadata + self, command: HermesCurateCommand, metadata: SoftwareMetadata ) -> SoftwareMetadata: """Entry point of the callable. @@ -103,7 +101,7 @@ def validate(self): self._conforms = conforms self._validation_graph = validation_graph - def create_report(self, metadata: SoftwareMetadata): + def create_report(self): """Create validation report. This creates the report both as a machine-readble JSON-LD file, and prints the @@ -114,7 +112,7 @@ def create_report(self, metadata: SoftwareMetadata): validation_file.parent.mkdir(exist_ok=True, parents=True) self._validation_graph.serialize(validation_file, format="json-ld") - self._report = create_report(self._validation_graph) + print(create_report(self._validation_graph), end="\n\n") if self._environment is None: print("Software CaRD plugin not running in CI environment.") else: From 09a023656201ca546a479b6b00c77c20ac0a0107 Mon Sep 17 00:00:00 2001 From: David Pape Date: Tue, 30 Jun 2026 08:13:50 +0200 Subject: [PATCH 4/7] `noqa` annotation for unused argument in method adhering to API --- src/hermes_plugin_software_card/curate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hermes_plugin_software_card/curate.py b/src/hermes_plugin_software_card/curate.py index 7658f36..bc9b249 100644 --- a/src/hermes_plugin_software_card/curate.py +++ b/src/hermes_plugin_software_card/curate.py @@ -66,7 +66,9 @@ def __init__(self): } def __call__( - self, command: HermesCurateCommand, metadata: SoftwareMetadata + self, + command: HermesCurateCommand, # noqa: ARG002 + metadata: SoftwareMetadata, ) -> SoftwareMetadata: """Entry point of the callable. From f06467f64fe8fbb830e0bea878f5af669c7b769d Mon Sep 17 00:00:00 2001 From: David Pape Date: Tue, 30 Jun 2026 08:36:23 +0200 Subject: [PATCH 5/7] Fix spurious argument --- src/hermes_plugin_software_card/curate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hermes_plugin_software_card/curate.py b/src/hermes_plugin_software_card/curate.py index bc9b249..42cfdc6 100644 --- a/src/hermes_plugin_software_card/curate.py +++ b/src/hermes_plugin_software_card/curate.py @@ -79,7 +79,7 @@ def __call__( """ self.prepare(metadata) self.validate() - self.create_report(metadata) + self.create_report() if not self.is_publication_approved(): return SoftwareMetadata() From ecd924cd8f74adf2e699c260e138be54bb61bfb4 Mon Sep 17 00:00:00 2001 From: David Pape Date: Tue, 30 Jun 2026 08:59:14 +0200 Subject: [PATCH 6/7] Update docstrings --- src/hermes_plugin_software_card/curate.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/hermes_plugin_software_card/curate.py b/src/hermes_plugin_software_card/curate.py index 42cfdc6..370e22b 100644 --- a/src/hermes_plugin_software_card/curate.py +++ b/src/hermes_plugin_software_card/curate.py @@ -89,8 +89,7 @@ def __call__( def prepare(self, metadata: SoftwareMetadata): """Prepare the validation. - The metadata given in the context is parsed as an RDF graph and then validated - using the Software CaRD validation. + The metadata given in ``metadata`` is parsed as an RDF graph for validation. """ self._data_graph = read_rdf_resource( format="json-ld", data=json.dumps(metadata.ld_value) @@ -98,7 +97,7 @@ def prepare(self, metadata: SoftwareMetadata): self._shacl_graph = make_shacl_graph(Config.from_dict(self._validation_config)) def validate(self): - """Run Software CaRD validation.""" + """Run Software CaRD validation on the given software metadata.""" conforms, validation_graph = validate_graph(self._data_graph, self._shacl_graph) self._conforms = conforms self._validation_graph = validation_graph @@ -106,8 +105,8 @@ def validate(self): def create_report(self): """Create validation report. - This creates the report both as a machine-readble JSON-LD file, and prints the - URL to the Software CaRD web app to the screen. + This creates the report both as a machine-readble JSON-LD file, and prints a + human-readable report, and the URL to the Software CaRD web app to the screen. """ ctx = HermesCacheManager() validation_file = ctx.cache_dir / "curate" / "validation.json" From e5c25b0c4bd9758eb97d7edfff73663b024185df Mon Sep 17 00:00:00 2001 From: David Pape Date: Thu, 2 Jul 2026 11:03:18 +0200 Subject: [PATCH 7/7] Use `HermesCacheManager` the intended way --- src/hermes_plugin_software_card/curate.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/hermes_plugin_software_card/curate.py b/src/hermes_plugin_software_card/curate.py index 370e22b..a089c6b 100644 --- a/src/hermes_plugin_software_card/curate.py +++ b/src/hermes_plugin_software_card/curate.py @@ -6,6 +6,7 @@ """Module containing the Software CaRD curation plugin for HERMES.""" import json +from io import StringIO from hermes.commands.curate.base import HermesCurateCommand, HermesCuratePlugin from hermes.model import SoftwareMetadata @@ -109,9 +110,13 @@ def create_report(self): human-readable report, and the URL to the Software CaRD web app to the screen. """ ctx = HermesCacheManager() - validation_file = ctx.cache_dir / "curate" / "validation.json" - validation_file.parent.mkdir(exist_ok=True, parents=True) - self._validation_graph.serialize(validation_file, format="json-ld") + ctx.prepare_step("curate") + with ctx["result"] as cache: + graph_io = StringIO() + self._validation_graph.print(format="json-ld", out=graph_io) + cache["validation"] = json.loads(graph_io.getvalue()) + graph_io.close() + ctx.finalize_step("curate") print(create_report(self._validation_graph), end="\n\n") if self._environment is None: