hermes 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. hermes/__init__.py +3 -0
  2. hermes/__main__.py +11 -0
  3. hermes/commands/__init__.py +17 -0
  4. hermes/commands/base.py +199 -0
  5. hermes/commands/clean/__init__.py +0 -0
  6. hermes/commands/clean/base.py +30 -0
  7. hermes/commands/cli.py +61 -0
  8. hermes/commands/curate/__init__.py +0 -0
  9. hermes/commands/curate/base.py +47 -0
  10. hermes/commands/deposit/__init__.py +3 -0
  11. hermes/commands/deposit/base.py +145 -0
  12. hermes/commands/deposit/error.py +8 -0
  13. hermes/commands/deposit/file.py +32 -0
  14. hermes/commands/deposit/invenio.py +665 -0
  15. hermes/commands/deposit/invenio_rdm.py +86 -0
  16. hermes/commands/harvest/__init__.py +3 -0
  17. hermes/commands/harvest/base.py +66 -0
  18. hermes/commands/harvest/cff.py +123 -0
  19. hermes/commands/harvest/codemeta.py +68 -0
  20. hermes/commands/harvest/util/__init__.py +7 -0
  21. hermes/commands/harvest/util/validate_codemeta.py +51 -0
  22. hermes/commands/postprocess/__init__.py +0 -0
  23. hermes/commands/postprocess/base.py +31 -0
  24. hermes/commands/postprocess/invenio.py +49 -0
  25. hermes/commands/postprocess/invenio_rdm.py +26 -0
  26. hermes/commands/process/__init__.py +0 -0
  27. hermes/commands/process/base.py +73 -0
  28. hermes/error.py +8 -0
  29. hermes/logger.py +82 -0
  30. hermes/model/__init__.py +3 -0
  31. hermes/model/context.py +445 -0
  32. hermes/model/errors.py +46 -0
  33. hermes/model/merge.py +179 -0
  34. hermes/model/path.py +399 -0
  35. hermes/settings.py +44 -0
  36. hermes/utils.py +16 -0
  37. hermes-0.1.0.dist-info/LICENSE.md +22 -0
  38. hermes-0.1.0.dist-info/LICENSES/Apache-2.0.txt +73 -0
  39. hermes-0.1.0.dist-info/LICENSES/CC-BY-ND-4.0.txt +154 -0
  40. hermes-0.1.0.dist-info/LICENSES/CC-BY-SA-4.0.txt +170 -0
  41. hermes-0.1.0.dist-info/LICENSES/CC0-1.0.txt +121 -0
  42. hermes-0.1.0.dist-info/LICENSES/LicenseRef-BSD-Caltech.txt +28 -0
  43. hermes-0.1.0.dist-info/METADATA +113 -0
  44. hermes-0.1.0.dist-info/RECORD +46 -0
  45. hermes-0.1.0.dist-info/WHEEL +4 -0
  46. hermes-0.1.0.dist-info/entry_points.txt +17 -0
hermes/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
hermes/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: Michael Meinel
6
+
7
+ from hermes.commands.cli import main
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,17 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: Michael Meinel
6
+
7
+ # This is an interface file that only provides a public interface, hence linter is disabled to avoid
8
+ # "unused import" errors.
9
+ # flake8: noqa
10
+
11
+ from hermes.commands.base import HermesHelpCommand
12
+ from hermes.commands.clean.base import HermesCleanCommand
13
+ from hermes.commands.curate.base import HermesCurateCommand
14
+ from hermes.commands.harvest.base import HermesHarvestCommand
15
+ from hermes.commands.process.base import HermesProcessCommand
16
+ from hermes.commands.deposit.base import HermesDepositCommand
17
+ from hermes.commands.postprocess.base import HermesPostprocessCommand
@@ -0,0 +1,199 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: Michael Meinel
6
+
7
+ import abc
8
+ import argparse
9
+ import logging
10
+ import pathlib
11
+ from importlib import metadata
12
+ from typing import Dict, Optional, Type
13
+
14
+ import toml
15
+ from pydantic import BaseModel
16
+ from pydantic_settings import BaseSettings, SettingsConfigDict
17
+
18
+
19
+ class HermesSettings(BaseSettings):
20
+ """Root class for HERMES configuration model."""
21
+
22
+ model_config = SettingsConfigDict(env_file_encoding='utf-8')
23
+
24
+ logging: Dict = {}
25
+
26
+
27
+ class HermesCommand(abc.ABC):
28
+ """Base class for a HERMES workflow command.
29
+
30
+ :cvar NAME: The name of the sub-command that is defined here.
31
+ """
32
+
33
+ command_name: str = ""
34
+ settings_class: Type = HermesSettings
35
+
36
+ def __init__(self, parser: argparse.ArgumentParser):
37
+ """Initialize a new instance of any HERMES command.
38
+
39
+ :param parser: The command line parser used for reading command line arguments.
40
+ """
41
+ self.parser = parser
42
+ self.plugins = self.init_plugins()
43
+ self.settings = None
44
+
45
+ self.log = logging.getLogger(f"hermes.{self.command_name}")
46
+
47
+ def init_plugins(self):
48
+ """Collect and initialize the plugins available for the HERMES command."""
49
+
50
+ # Collect all entry points for this group (i.e., all valid plug-ins for the step)
51
+ entry_point_group = f"hermes.{self.command_name}"
52
+ group_plugins = {
53
+ entry_point.name: entry_point.load()
54
+ for entry_point in metadata.entry_points(group=entry_point_group)
55
+ }
56
+
57
+ # Collect the plug-in specific configurations
58
+ self.derive_settings_class({
59
+ plugin_name: plugin_class.settings_class
60
+ for plugin_name, plugin_class in group_plugins.items()
61
+ if hasattr(plugin_class, "settings_class") and plugin_class.settings_class is not None
62
+ })
63
+
64
+ return group_plugins
65
+
66
+ @classmethod
67
+ def derive_settings_class(cls, setting_types: Dict[str, Type]) -> None:
68
+ """Build a new Pydantic data model class for configuration.
69
+
70
+ This will create a new class that includes all settings from the plugins available.
71
+ """
72
+
73
+ if cls.settings_class is not None:
74
+ # Derive a new settings model class that contains all the plug-in extensions
75
+ cls.settings_class = type(
76
+ f"{cls.__name__}Settings",
77
+ (cls.settings_class, ),
78
+ {
79
+ **{
80
+ plugin_name: plugin_settings()
81
+ for plugin_name, plugin_settings in setting_types.items()
82
+ if plugin_settings is not None
83
+ },
84
+ "__annotations__": setting_types,
85
+ },
86
+ )
87
+ elif setting_types:
88
+ raise ValueError(f"Command {cls.command_name} has no settings, hence plugin must not have settings, too.")
89
+
90
+ def init_common_parser(self, parser: argparse.ArgumentParser) -> None:
91
+ """Initialize the common command line arguments available for all HERMES sub-commands.
92
+
93
+ :param parser: The base command line parser used as entry point when reading command line arguments.
94
+ """
95
+
96
+ parser.add_argument(
97
+ "--path", default=pathlib.Path(), type=pathlib.Path, help="Working path"
98
+ )
99
+ parser.add_argument(
100
+ "--config",
101
+ default=pathlib.Path("hermes.toml"),
102
+ type=pathlib.Path,
103
+ help="Configuration file in TOML format",
104
+ )
105
+
106
+ plugin_args = parser.add_argument_group("Extra options")
107
+ plugin_args.add_argument(
108
+ "-O",
109
+ nargs=2,
110
+ action="append",
111
+ default=[],
112
+ metavar=("NAME", "VALUE"),
113
+ dest="options",
114
+ help="Configuration values to override hermes.toml options. "
115
+ "NAME is the dotted name / path to the option in the TOML file, "
116
+ "VALUE is the actual value.",
117
+ )
118
+
119
+ def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
120
+ """Initialize the command line arguments available for this specific HERMES sub-commands.
121
+
122
+ You should override this method to add your custom arguments to the command line parser of
123
+ the respective sub-command.
124
+
125
+ :param command_parser: The command line sub-parser responsible for the HERMES sub-command.
126
+ """
127
+
128
+ pass
129
+
130
+ def load_settings(self, args: argparse.Namespace):
131
+ """Load settings from the configuration file (passed in from command line)."""
132
+
133
+ toml_data = toml.load(args.path / args.config)
134
+ self.root_settings = HermesCommand.settings_class.model_validate(toml_data)
135
+ self.settings = getattr(self.root_settings, self.command_name)
136
+
137
+ def patch_settings(self, args: argparse.Namespace):
138
+ """Process command line options for the settings."""
139
+
140
+ for key, value in args.options:
141
+ target = self.settings
142
+ sub_keys = key.split('.')
143
+ for sub_key in sub_keys[:-1]:
144
+ target = getattr(target, sub_key)
145
+
146
+ # TODO: Transform the value accordingly before setting it
147
+ setattr(target, sub_keys[-1], value)
148
+
149
+ @abc.abstractmethod
150
+ def __call__(self, args: argparse.Namespace):
151
+ """Execute the HERMES sub-command.
152
+
153
+ :param args: The namespace that was returned by the command line parser when reading the arguments.
154
+ """
155
+
156
+ pass
157
+
158
+
159
+ class HermesPlugin(abc.ABC):
160
+ """Base class for all HERMES plugins."""
161
+
162
+ settings_class: Optional[Type] = None
163
+
164
+ @abc.abstractmethod
165
+ def __call__(self, command: HermesCommand) -> None:
166
+ """Execute the plugin.
167
+
168
+ :param command: The command that triggered this plugin to run.
169
+ """
170
+
171
+ pass
172
+
173
+
174
+ class HermesHelpSettings(BaseModel):
175
+ pass
176
+
177
+
178
+ class HermesHelpCommand(HermesCommand):
179
+ """Show help page and exit."""
180
+
181
+ command_name = "help"
182
+ settings_class = HermesHelpSettings
183
+
184
+ def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
185
+ command_parser.add_argument(
186
+ "subcommand",
187
+ nargs="?",
188
+ metavar="COMMAND",
189
+ help="The HERMES sub-command to get help for.",
190
+ )
191
+
192
+ def __call__(self, args: argparse.Namespace) -> None:
193
+ if args.subcommand:
194
+ # When a sub-command is given, show its help page (i.e., by "running" the command with "-h" flag).
195
+ self.parser.parse_args([args.subcommand, "-h"])
196
+ else:
197
+ # Otherwise, simply show the general help and exit (cleanly).
198
+ self.parser.print_help()
199
+ self.parser.exit()
File without changes
@@ -0,0 +1,30 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: Michael Meinel
6
+
7
+ import argparse
8
+ import shutil
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from hermes.commands.base import HermesCommand
13
+
14
+
15
+ class HermesCleanSettings(BaseModel):
16
+ """Configuration of the ``clean`` command."""
17
+ pass
18
+
19
+
20
+ class HermesCleanCommand(HermesCommand):
21
+ """ Clean up caches from previous HERMES runs. """
22
+
23
+ command_name = "clean"
24
+ settings_class = HermesCleanSettings
25
+
26
+ def __call__(self, args: argparse.Namespace) -> None:
27
+ self.log.info("Removing HERMES caches...")
28
+
29
+ # Naive implementation for now... check errors, validate directory, don't construct the path ourselves, etc.
30
+ shutil.rmtree(args.path / '.hermes')
hermes/commands/cli.py ADDED
@@ -0,0 +1,61 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: Stephan Druskat
6
+ # SPDX-FileContributor: Michael Meinel
7
+
8
+ """
9
+ This module provides the main entry point for the HERMES command line application.
10
+ """
11
+ import argparse
12
+
13
+ from hermes.commands import HermesHelpCommand, HermesCleanCommand, HermesHarvestCommand, HermesProcessCommand, \
14
+ HermesCurateCommand, HermesDepositCommand, HermesPostprocessCommand
15
+ from hermes.commands.base import HermesCommand
16
+
17
+
18
+ def main() -> None:
19
+ """
20
+ HERMES main entry point (i.e., run the CLI).
21
+
22
+ This command runs the selected HERMES sub-command.
23
+ """
24
+ parser = argparse.ArgumentParser(
25
+ prog="hermes",
26
+ description="This command runs HERMES workflow steps.",
27
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
28
+ )
29
+
30
+ # Register all sub-commands to a new sub-parser each.
31
+ subparsers = parser.add_subparsers(dest="subcommand", required=True,
32
+ help="Available subcommands")
33
+ setting_types = {}
34
+
35
+ for command in (
36
+ HermesHelpCommand(parser),
37
+ HermesCleanCommand(parser),
38
+ HermesHarvestCommand(parser),
39
+ HermesProcessCommand(parser),
40
+ HermesCurateCommand(parser),
41
+ HermesDepositCommand(parser),
42
+ HermesPostprocessCommand(parser),
43
+ ):
44
+ if command.settings_class is not None:
45
+ setting_types[command.command_name] = command.settings_class
46
+
47
+ command_parser = subparsers.add_parser(command.command_name, help=command.__doc__)
48
+ command_parser.set_defaults(command=command)
49
+
50
+ command.init_common_parser(command_parser)
51
+ command.init_command_parser(command_parser)
52
+
53
+ # Construct the Pydantic Settings root model
54
+ HermesCommand.derive_settings_class(setting_types)
55
+
56
+ # Actually parse the command line, configure it and execute the selected sub-command.
57
+ args = parser.parse_args()
58
+
59
+ args.command.load_settings(args)
60
+ args.command.patch_settings(args)
61
+ args.command(args)
File without changes
@@ -0,0 +1,47 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: Michael Meinel
6
+
7
+ import argparse
8
+ import os
9
+ import shutil
10
+ import sys
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from hermes.commands.base import HermesCommand
15
+ from hermes.model.context import CodeMetaContext
16
+
17
+
18
+ class CurateSettings(BaseModel):
19
+ """Generic deposition settings."""
20
+
21
+ pass
22
+
23
+
24
+ class HermesCurateCommand(HermesCommand):
25
+ """ Curate the unified metadata before deposition. """
26
+
27
+ command_name = "curate"
28
+ settings_class = CurateSettings
29
+
30
+ def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
31
+ pass
32
+
33
+ def __call__(self, args: argparse.Namespace) -> None:
34
+
35
+ self.log.info("# Metadata curation")
36
+
37
+ ctx = CodeMetaContext()
38
+ process_output = ctx.hermes_dir / 'process' / (ctx.hermes_name + ".json")
39
+
40
+ if not process_output.is_file():
41
+ self.log.error(
42
+ "No processed metadata found. Please run `hermes process` before curation."
43
+ )
44
+ sys.exit(1)
45
+
46
+ os.makedirs(ctx.hermes_dir / 'curate', exist_ok=True)
47
+ shutil.copy(process_output, ctx.hermes_dir / 'curate' / (ctx.hermes_name + '.json'))
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2023 Helmholtz-Zentrum Dresden-Rossendorf (HZDR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,145 @@
1
+ # SPDX-FileCopyrightText: 2023 Helmholtz-Zentrum Dresden-Rossendorf (HZDR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: David Pape
6
+ # SPDX-FileContributor: Michael Meinel
7
+
8
+ import abc
9
+ import argparse
10
+ import json
11
+ import sys
12
+
13
+ from pydantic import BaseModel
14
+
15
+ 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
19
+
20
+
21
+ class BaseDepositPlugin(HermesPlugin):
22
+ """Base class that implements the generic deposition workflow.
23
+
24
+ TODO: describe workflow... needs refactoring to be less stateful!
25
+ """
26
+
27
+ def __init__(self, command, ctx):
28
+ self.command = command
29
+ self.ctx = ctx
30
+
31
+ def __call__(self, command: HermesCommand) -> None:
32
+ """Initiate the deposition process.
33
+
34
+ This calls a list of additional methods on the class, none of which need to be implemented.
35
+ """
36
+ self.command = command
37
+
38
+ self.prepare()
39
+ self.map_metadata()
40
+
41
+ if self.is_initial_publication():
42
+ self.create_initial_version()
43
+ else:
44
+ self.create_new_version()
45
+
46
+ self.update_metadata()
47
+ self.delete_artifacts()
48
+ self.upload_artifacts()
49
+ self.publish()
50
+
51
+ def prepare(self) -> None:
52
+ """Prepare the deposition.
53
+
54
+ This method may be implemented to check whether config and context match some initial conditions.
55
+
56
+ If no exceptions are raised, execution continues.
57
+ """
58
+ pass
59
+
60
+ @abc.abstractmethod
61
+ def map_metadata(self) -> None:
62
+ """Map the given metadata to the target schema of the deposition platform."""
63
+ pass
64
+
65
+ def is_initial_publication(self) -> bool:
66
+ """Decide whether to do an initial publication or publish a new version.
67
+
68
+ Returning ``True`` indicates that publication of an initial version will be executed, resulting in a call of
69
+ :meth:`create_initial_version`. ``False`` indicates a new version of an existing publication, leading to a call
70
+ of :meth:`create_new_version`.
71
+
72
+ By default, this returns ``True``.
73
+ """
74
+ return True
75
+
76
+ def create_initial_version(self) -> None:
77
+ """Create an initial version of the publication on the target platform."""
78
+ pass
79
+
80
+ def create_new_version(self) -> None:
81
+ """Create a new version of an existing publication on the target platform."""
82
+ pass
83
+
84
+ def update_metadata(self) -> None:
85
+ """Update the metadata of the newly created version."""
86
+ pass
87
+
88
+ def delete_artifacts(self) -> None:
89
+ """Delete any superfluous artifacts taken from the previous version of the publication."""
90
+ pass
91
+
92
+ def upload_artifacts(self) -> None:
93
+ """Upload new artifacts to the target platform."""
94
+ pass
95
+
96
+ @abc.abstractmethod
97
+ def publish(self) -> None:
98
+ """Publish the newly created deposit on the target platform."""
99
+ pass
100
+
101
+
102
+ class DepositSettings(BaseModel):
103
+ """Generic deposition settings."""
104
+
105
+ target: str = ""
106
+
107
+
108
+ class HermesDepositCommand(HermesCommand):
109
+ """ Deposit the curated metadata to repositories. """
110
+
111
+ command_name = "deposit"
112
+ settings_class = DepositSettings
113
+
114
+ def init_command_parser(self, command_parser: argparse.ArgumentParser) -> None:
115
+ command_parser.add_argument('--file', '-f', nargs=1, action='append',
116
+ help="File that should be part of the deposition.")
117
+ command_parser.add_argument('--initial', action='store_true', default=False,
118
+ help="Allow initial deposition (i.e., minting a new PID).")
119
+
120
+ def __call__(self, args: argparse.Namespace) -> None:
121
+ self.args = args
122
+ plugin_name = self.settings.target
123
+ print(self.args)
124
+
125
+ ctx = CodeMetaContext()
126
+ codemeta_file = ctx.get_cache("curate", ctx.hermes_name)
127
+ if not codemeta_file.exists():
128
+ self.log.error("You must run the 'curate' command before deposit")
129
+ sys.exit(1)
130
+
131
+ codemeta_path = ContextPath("codemeta")
132
+ with open(codemeta_file) as codemeta_fh:
133
+ ctx.update(codemeta_path, json.load(codemeta_fh))
134
+
135
+ try:
136
+ plugin_func = self.plugins[plugin_name](self, ctx)
137
+
138
+ except KeyError:
139
+ self.log.error("Plugin '%s' not found.", plugin_name)
140
+
141
+ try:
142
+ plugin_func(self)
143
+
144
+ except HermesValidationError as e:
145
+ self.log.error("Error while executing %s: %s", plugin_name, e)
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2023 Helmholtz-Zentrum Dresden-Rossendorf (HZDR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: David Pape
6
+
7
+ class DepositionUnauthorizedError(Exception):
8
+ pass
@@ -0,0 +1,32 @@
1
+ # SPDX-FileCopyrightText: 2023 German Aerospace Center (DLR), Helmholtz-Zentrum Dresden-Rossendorf (HZDR)
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # SPDX-FileContributor: David Pape
6
+ # SPDX-FileContributor: Michael Meinel
7
+ # SPDX-FileContributor: Stephan Druskat
8
+
9
+ import json
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from hermes.commands.deposit.base import BaseDepositPlugin
14
+ from hermes.model.path import ContextPath
15
+
16
+
17
+ class FileDepositSettings(BaseModel):
18
+ filename: str = 'hermes.json'
19
+
20
+
21
+ class FileDepositPlugin(BaseDepositPlugin):
22
+ settings_class = FileDepositSettings
23
+
24
+ def map_metadata(self) -> None:
25
+ self.ctx.update(ContextPath.parse('deposit.file'), self.ctx['codemeta'])
26
+
27
+ def publish(self) -> None:
28
+ file_config = self.command.settings.file
29
+ output_data = self.ctx['deposit.file']
30
+
31
+ with open(file_config.filename, 'w') as deposition_file:
32
+ json.dump(output_data, deposition_file, indent=2)