Skip to content

Commit 6c3ba13

Browse files
author
Michael Fritzsche
committed
started to add support for deposit step and added useful method for SoftwareMetadata
1 parent ddcd26a commit 6c3ba13

7 files changed

Lines changed: 72 additions & 75 deletions

File tree

src/hermes/commands/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@
1515
# from hermes.commands.curate.base import HermesCurateCommand
1616
from hermes.commands.harvest.base import HermesHarvestCommand
1717
# from hermes.commands.process.base import HermesProcessCommand
18-
# from hermes.commands.deposit.base import HermesDepositCommand
18+
from hermes.commands.deposit.base import HermesDepositCommand
1919
# from hermes.commands.postprocess.base import HermesPostprocessCommand

src/hermes/commands/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# from hermes.commands import (HermesHelpCommand, HermesVersionCommand, HermesCleanCommand,
1717
# HermesHarvestCommand, HermesProcessCommand, HermesCurateCommand,
1818
# HermesDepositCommand, HermesPostprocessCommand, HermesInitCommand)
19-
from hermes.commands import HermesHarvestCommand
19+
from hermes.commands import HermesDepositCommand, HermesHarvestCommand
2020
from hermes.commands.base import HermesCommand
2121

2222

@@ -45,7 +45,7 @@ def main() -> None:
4545
HermesHarvestCommand(parser),
4646
# HermesProcessCommand(parser),
4747
# HermesCurateCommand(parser),
48-
# HermesDepositCommand(parser),
48+
HermesDepositCommand(parser),
4949
# HermesPostprocessCommand(parser),
5050
):
5151
if command.settings_class is not None:

src/hermes/commands/deposit/base.py

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@
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
18-
from hermes.model.errors import HermesValidationError
14+
from hermes.model.context_manager import HermesContext
15+
from hermes.model import SoftwareMetadata
16+
from hermes.model.error import HermesValidationError
1917

2018

2119
class BaseDepositPlugin(HermesPlugin):
@@ -24,16 +22,19 @@ 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+
self.ctx = HermesContext()
32+
33+
self.ctx.prepare_step("curate")
34+
self.metadata = SoftwareMetadata.load_from_cache(self.ctx, "result")
35+
self.ctx.finalize_step("curate")
36+
37+
self.ctx.prepare_step("deposit")
3738

3839
self.prepare()
3940
self.map_metadata()
@@ -106,7 +107,7 @@ def publish(self) -> None:
106107
pass
107108

108109

109-
class _DepositSettings(BaseModel):
110+
class DepositSettings(BaseModel):
110111
"""Generic deposition settings."""
111112

112113
target: str = ""
@@ -116,7 +117,7 @@ class HermesDepositCommand(HermesCommand):
116117
""" Deposit the curated metadata to repositories. """
117118

118119
command_name = "deposit"
119-
settings_class = _DepositSettings
120+
settings_class = DepositSettings
120121

121122
def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
122123
command_parser.add_argument('--file', '-f', nargs=1, action='append',
@@ -128,26 +129,12 @@ def __call__(self, args: argparse.Namespace) -> None:
128129
self.args = args
129130
plugin_name = self.settings.target
130131

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-
141132
try:
142-
plugin_func = self.plugins[plugin_name](self, ctx)
143-
133+
plugin_func = self.plugins[plugin_name]()
134+
plugin_func(self)
144135
except KeyError as e:
145136
self.log.error("Plugin '%s' not found.", plugin_name)
146137
self.errors.append(e)
147-
148-
try:
149-
plugin_func(self)
150-
151138
except HermesValidationError as e:
152139
self.log.error("Error while executing %s: %s", plugin_name, e)
153140
self.errors.append(e)

src/hermes/commands/deposit/file.py

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

1313
from hermes.commands.deposit.base import BaseDepositPlugin
14-
from hermes.model.path import ContextPath
1514

1615

1716
class FileDepositSettings(BaseModel):
18-
filename: str = 'hermes.json'
17+
filename: str = 'codemeta.json'
1918

2019

2120
class FileDepositPlugin(BaseDepositPlugin):
2221
settings_class = FileDepositSettings
2322

24-
def map_metadata(self) -> None:
25-
self.ctx.update(ContextPath.parse('deposit.file'), self.ctx['codemeta'])
26-
2723
def publish(self) -> None:
2824
file_config = self.command.settings.file
29-
output_data = self.ctx['deposit.file']
3025

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

src/hermes/commands/deposit/invenio.py

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
import requests
1818
from pydantic import BaseModel
1919

20-
from hermes.commands.deposit.base import BaseDepositPlugin, HermesDepositCommand
20+
from hermes.commands.deposit.base import BaseDepositPlugin
2121
from hermes.commands.deposit.error import DepositionUnauthorizedError
2222
from hermes.error import MisconfigurationError
23-
from hermes.model.context import CodeMetaContext
24-
from hermes.model.path import ContextPath
23+
from hermes.model.context_manager import HermesContext
2524
from hermes.utils import hermes_doi, hermes_user_agent
2625

2726

@@ -258,11 +257,13 @@ class InvenioDepositPlugin(BaseDepositPlugin):
258257
invenio_resolver_class = InvenioResolver
259258
settings_class = InvenioDepositSettings
260259

261-
def __init__(self, command: HermesDepositCommand, ctx: CodeMetaContext, client=None, resolver=None) -> None:
262-
super().__init__(command, ctx)
260+
def __init__(self) -> None:
261+
super().__init__()
263262

264-
self.invenio_context_path = ContextPath.parse(f"deposit.{self.platform_name}")
265263
self.invenio_ctx = None
264+
265+
def __call__(self, command, *, client=None, resolver=None):
266+
self.command = command
266267
self.config = getattr(self.command.settings, self.platform_name)
267268

268269
if client is None:
@@ -292,7 +293,9 @@ def __init__(self, command: HermesDepositCommand, ctx: CodeMetaContext, client=N
292293
self.resolver = resolver or self.invenio_resolver_class(self.client)
293294
self.links = {}
294295

295-
# TODO: Populate some data structure here? Or move more of this into __init__?
296+
super().__call__(command)
297+
298+
# TODO: Populate some data structure here? Or move more of this into __init__.py?
296299
def prepare(self) -> None:
297300
"""Prepare the deposition on an Invenio-based platform.
298301
@@ -305,49 +308,42 @@ def prepare(self) -> None:
305308
- check access modalities (access right, access conditions, embargo data, existence
306309
of license)
307310
- check whether required configuration options are present
308-
- update ``self.ctx`` with metadata collected during the checks
311+
- update ``self.metadata`` with metadata collected during the checks
309312
"""
310313

311314
rec_id = self.config.record_id
312315
doi = self.config.doi
313316

314-
try:
315-
codemeta_identifier = self.ctx["codemeta.identifier"]
316-
except KeyError:
317-
codemeta_identifier = None
318-
317+
codemeta_identifier = self.metadata.get("identifier", None)
319318
rec_id, rec_meta = self.resolver.resolve_latest_id(
320319
record_id=rec_id, doi=doi, codemeta_identifier=codemeta_identifier
321320
)
322321

323-
version = self.ctx["codemeta"].get("version")
322+
version = self.metadata["version"]
324323
if rec_meta and (version == rec_meta.get("version")):
325324
raise ValueError(f"Version {version} already deposited.")
326325

327-
self.ctx.update(self.invenio_context_path['latestRecord'], {'id': rec_id, 'metadata': rec_meta})
328-
329-
license = self._get_license_identifier()
330-
self.ctx.update(self.invenio_context_path["license"], license)
331-
332-
communities = self._get_community_identifiers()
333-
self.ctx.update(self.invenio_context_path["communities"], communities)
326+
deposition_data = {}
327+
deposition_data["latestRecord"] = {'id': rec_id, 'metadata': rec_meta}
328+
deposition_data["license"] = self._get_license_identifier()
329+
deposition_data["communities"] = self._get_community_identifiers()
334330

335331
access_right, embargo_date, access_conditions = self._get_access_modalities(license)
336-
self.ctx.update(self.invenio_context_path["access_right"], access_right)
337-
self.ctx.update(self.invenio_context_path["embargo_date"], embargo_date)
338-
self.ctx.update(self.invenio_context_path["access_conditions"], access_conditions)
332+
deposition_data["access_right"] = access_right
333+
deposition_data["embargo_date"] = embargo_date
334+
deposition_data["access_conditions"] = access_conditions
339335

340-
self.invenio_ctx = self.ctx[self.invenio_context_path]
336+
self.invenio_ctx = deposition_data
341337

342338
def map_metadata(self) -> None:
343339
"""Map the harvested metadata onto the Invenio schema."""
344340

345341
deposition_metadata = self._codemeta_to_invenio_deposition()
346-
self.ctx.update(self.invenio_context_path["depositionMetadata"], deposition_metadata)
347-
348-
# Store a snapshot of the mapped data within the cache, useful for analysis, debugging, etc
349-
with open(self.ctx.get_cache("deposit", self.platform_name, create=True), 'w') as invenio_json:
350-
json.dump(deposition_metadata, invenio_json, indent=' ')
342+
ctx = HermesContext()
343+
ctx.prepare_step("deposit")
344+
with ctx[self.platform_name] as deposit_ctx:
345+
deposit_ctx["deposit"] = deposition_metadata
346+
ctx.finalize_step("deposit")
351347

352348
def is_initial_publication(self) -> bool:
353349
latest_record_id = self.invenio_ctx.get("latestRecord", {}).get("id")
@@ -426,7 +422,7 @@ def update_metadata(self) -> None:
426422
self.links.update(deposit["links"])
427423

428424
_log.debug("Created new version deposit: %s", self.links["html"])
429-
with open(self.ctx.get_cache('deposit', 'deposit', create=True), 'w') as deposit_file:
425+
with open(self.metadata.get_cache('deposit', 'deposit', create=True), 'w') as deposit_file:
430426
json.dump(deposit, deposit_file, indent=4)
431427

432428
def delete_artifacts(self) -> None:
@@ -505,7 +501,7 @@ def _codemeta_to_invenio_deposition(self) -> dict:
505501
differences between Invenio-based platforms.
506502
"""
507503

508-
metadata = self.ctx["codemeta"]
504+
metadata = self.metadata
509505
license = self.invenio_ctx["license"]
510506
communities = self.invenio_ctx["communities"]
511507
access_right = self.invenio_ctx["access_right"]
@@ -520,7 +516,7 @@ def _codemeta_to_invenio_deposition(self) -> dict:
520516
"affiliation": author.get("affiliation", {"legalName": None}).get("legalName"),
521517
# Invenio wants "family, given". author.get("name") might not have this format.
522518
"name": f"{author.get('familyName')}, {author.get('givenName')}"
523-
if author.get("familyName") and author.get("givenName")
519+
if "familyName" in author and "givenName" in author
524520
else author.get("name"),
525521
# Invenio expects the ORCID without the URL part
526522
"orcid": author.get("@id", "").replace("https://orcid.org/", "") or None,
@@ -538,7 +534,7 @@ def _codemeta_to_invenio_deposition(self) -> dict:
538534
"affiliation": contributor.get("affiliation", {"legalName": None}).get("legalName"),
539535
# Invenio wants "family, given". contributor.get("name") might not have this format.
540536
"name": f"{contributor.get('familyName')}, {contributor.get('givenName')}"
541-
if contributor.get("familyName") and contributor.get("givenName")
537+
if "familyName" in contributor and "givenName" in contributor
542538
else contributor.get("name"),
543539
# Invenio expects the ORCID without the URL part
544540
"orcid": contributor.get("@id", "").replace("https://orcid.org/", "") or None,
@@ -604,15 +600,15 @@ def _get_license_identifier(self) -> t.Optional[str]:
604600
605601
If no license is configured, ``None`` will be returned.
606602
"""
607-
license_url = self.ctx["codemeta"].get("license")
603+
license_url = self.metadata["license"]
608604
return self.resolver.resolve_license_id(license_url)
609605

610606
def _get_community_identifiers(self):
611607
"""Get Invenio community identifiers from config.
612608
613609
This function gets the communities to be used for the deposition on an Invenio-based
614610
site from the config and checks their validity against the site's API. If one of the
615-
identifiers can not be found on the site, a :class:`HermesMisconfigurationError` is
611+
identifiers can not be found on the site, a :class:`MisconfigurationError` is
616612
raised.
617613
"""
618614

src/hermes/error.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55
# SPDX-FileContributor: David Pape
66

7-
class HermesMisconfigurationError(Exception):
7+
class MisconfigurationError(Exception):
88
pass

src/hermes/model/api.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
1+
from hermes.model.context_manager import HermesContext, HermesContexError
12
from hermes.model.types import ld_dict
2-
33
from hermes.model.types.ld_context import ALL_CONTEXTS
4+
from hermes.model.types.ld_dict import bundled_loader
45

56

67
class SoftwareMetadata(ld_dict):
78

89
def __init__(self, data: dict = None, extra_vocabs: dict[str, str] = None) -> None:
910
ctx = ALL_CONTEXTS + [{**extra_vocabs}] if extra_vocabs is not None else ALL_CONTEXTS
1011
super().__init__([ld_dict.from_dict(data, context=ctx).data_dict if data else {}], context=ctx)
12+
13+
@classmethod
14+
def load_from_cache(cls, ctx: HermesContext, source: str) -> "SoftwareMetadata":
15+
with ctx[source] as cache:
16+
try:
17+
return SoftwareMetadata(cache["codemeta"])
18+
except Exception:
19+
pass
20+
try:
21+
context = cache["context"]["@context"]
22+
data = SoftwareMetadata()
23+
data.active_ctx = data.ld_proc.initial_ctx(context, {"documentLoader": bundled_loader})
24+
data.context = context
25+
for key, value in cache["expanded"][0]:
26+
data[key] = value
27+
return data
28+
except Exception as e:
29+
raise HermesContexError("There is no (valid) data stored in the cache.") from e

0 commit comments

Comments
 (0)