lockss-turtles 0.6.0.dev2__tar.gz → 0.6.0.dev3__tar.gz

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 (23) hide show
  1. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/PKG-INFO +2 -3
  2. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/pyproject.toml +4 -4
  3. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/src/lockss/turtles/__init__.py +1 -1
  4. lockss_turtles-0.6.0.dev3/src/lockss/turtles/app.py +245 -0
  5. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/src/lockss/turtles/cli.py +83 -42
  6. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/src/lockss/turtles/plugin.py +18 -13
  7. lockss_turtles-0.6.0.dev3/src/lockss/turtles/plugin_registry.py +238 -0
  8. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/src/lockss/turtles/plugin_set.py +102 -137
  9. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/src/lockss/turtles/util.py +28 -17
  10. lockss_turtles-0.6.0.dev3/tests/unittest/lockss/turtles/__init__.py +65 -0
  11. lockss_turtles-0.6.0.dev3/tests/unittest/lockss/turtles/test_plugin_set.py +62 -0
  12. lockss_turtles-0.6.0.dev2/src/lockss/turtles/app.py +0 -192
  13. lockss_turtles-0.6.0.dev2/src/lockss/turtles/plugin_registry.py +0 -263
  14. lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/__init__.py +0 -29
  15. lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-registry-catalog-schema.json +0 -27
  16. lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-registry-schema.json +0 -115
  17. lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-set-catalog-schema.json +0 -27
  18. lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-set-schema.json +0 -92
  19. lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-signing-credentials-schema.json +0 -27
  20. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/CHANGELOG.rst +0 -0
  21. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/LICENSE +0 -0
  22. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/README.rst +0 -0
  23. {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev3}/src/lockss/turtles/__main__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lockss-turtles
3
- Version: 0.6.0.dev2
3
+ Version: 0.6.0.dev3
4
4
  Summary: Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries
5
5
  License: BSD-3-Clause
6
6
  Author: Thib Guicherd-Callin
@@ -19,9 +19,8 @@ Classifier: Topic :: Software Development :: Libraries
19
19
  Classifier: Topic :: System :: Archiving
20
20
  Classifier: Topic :: Utilities
21
21
  Requires-Dist: java-manifest (>=1.1.0,<1.2.0)
22
- Requires-Dist: jsonschema (>=4.24.0,<4.25.0)
23
22
  Requires-Dist: lockss-pybasic (>=0.1.0,<0.2.0)
24
- Requires-Dist: pydantic (>=2.11.7,<3.0.0)
23
+ Requires-Dist: pydantic (>=2.11.0,<3.0.0)
25
24
  Requires-Dist: pyyaml (>=6.0.0,<6.1.0)
26
25
  Requires-Dist: xdg (>=6.0.0,<6.1.0)
27
26
  Project-URL: Documentation, https://docs.lockss.org/en/latest/software/turtles
@@ -28,7 +28,7 @@
28
28
 
29
29
  [project]
30
30
  name = "lockss-turtles"
31
- version = "0.6.0-dev2" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
31
+ version = "0.6.0-dev3" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
32
32
  description = "Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries"
33
33
  license = { text = "BSD-3-Clause" }
34
34
  readme = "README.rst"
@@ -41,11 +41,10 @@ maintainers = [
41
41
  ]
42
42
  dependencies = [
43
43
  "java-manifest (>=1.1.0,<1.2.0)",
44
- "jsonschema (>=4.24.0,<4.25.0)",
45
44
  "lockss-pybasic (>=0.1.0,<0.2.0)",
46
45
  "pyyaml (>=6.0.0,<6.1.0)",
47
46
  "xdg (>=6.0.0,<6.1.0)",
48
- "pydantic (>=2.11.7,<3.0.0)",
47
+ "pydantic (>=2.11.0,<3.0.0)",
49
48
  ]
50
49
  classifiers = [
51
50
  "Development Status :: 5 - Production/Stable",
@@ -74,7 +73,8 @@ include = [
74
73
  "README.rst",
75
74
  ]
76
75
  packages = [
77
- { include = "lockss", from = "src" }
76
+ { include = "lockss", from = "src" },
77
+ { include = "unittest", from = "tests" },
78
78
  ]
79
79
 
80
80
  [tool.poetry.scripts]
@@ -5,7 +5,7 @@ Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin
5
5
  registries.
6
6
  """
7
7
 
8
- __version__ = '0.6.0-dev2'
8
+ __version__ = '0.6.0-dev3'
9
9
 
10
10
  __copyright__ = '''
11
11
  Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # 3. Neither the name of the copyright holder nor the names of its contributors
16
+ # may be used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ # POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ # Remove in Python 3.14
32
+ # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
33
+ from __future__ import annotations
34
+
35
+ from collections.abc import Callable, Iterable
36
+ from pathlib import Path
37
+ from typing import ClassVar, Literal, Optional, Union
38
+
39
+ import yaml
40
+ from lockss.pybasic.fileutil import path
41
+ from pydantic import BaseModel, Field
42
+ import xdg
43
+
44
+ from .plugin import Plugin, PluginIdentifier
45
+ from .plugin_registry import PluginRegistry, PluginRegistryCatalog, PluginRegistryCatalogKind, PluginRegistryKind, PluginRegistryLayerIdentifier
46
+ from .plugin_set import PluginSet, PluginSetCatalog, PluginSetCatalogKind, PluginSetKind
47
+ from .util import BaseModelWithRoot, PathOrStr
48
+
49
+
50
+
51
+ PluginSigningCredentialsKind = Literal['PluginSigningCredentials']
52
+
53
+
54
+ class PluginSigningCredentials(BaseModelWithRoot):
55
+ kind: PluginSigningCredentialsKind = Field(description="This object's kind")
56
+ plugin_signing_keystore: str = Field(title='Plugin Signing Keystore', description='A path to the plugin signing keystore')
57
+ plugin_signing_alias: str = Field(title='Plugin Signing Alias', description='The plugin signing alias to use')
58
+
59
+ def get_plugin_signing_alias(self) -> str:
60
+ return self.plugin_signing_alias
61
+
62
+ def get_plugin_signing_keystore(self) -> Path:
63
+ return self.get_root().joinpath(self.plugin_signing_keystore)
64
+
65
+
66
+ class TurtlesApp(object):
67
+
68
+ CONFIG_DIR_NAME: ClassVar[str] = 'lockss-turtles'
69
+
70
+ XDG_CONFIG_DIR: ClassVar[Path] = Path(xdg.xdg_config_home(), CONFIG_DIR_NAME)
71
+
72
+ USR_CONFIG_DIR: ClassVar[Path] = Path('/usr/local/share', CONFIG_DIR_NAME)
73
+
74
+ ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
75
+
76
+ CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, USR_CONFIG_DIR, ETC_CONFIG_DIR)
77
+
78
+ PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
79
+
80
+ PLUGIN_SET_CATALOG: ClassVar[str] = 'plugin-set-catalog.yaml'
81
+
82
+ PLUGIN_SIGNING_CREDENTIALS: ClassVar[str] = 'plugin-signing-credentials.yaml'
83
+
84
+ def __init__(self) -> None:
85
+ super().__init__()
86
+ self._password: Optional[Callable[[], str]] = None
87
+ self._plugin_registries: list[PluginRegistry] = list()
88
+ self._plugin_registry_catalogs: list[PluginRegistryCatalog] = list()
89
+ self._plugin_set_catalogs: list[PluginSetCatalog] = list()
90
+ self._plugin_sets: list[PluginSet] = list()
91
+ self._plugin_signing_credentials: Optional[PluginSigningCredentials] = None
92
+
93
+ def build_plugin(self, plugin_ids: list[PluginIdentifier]) -> dict[str, tuple[str, Path, Plugin]]:
94
+ return {plugin_id: self._build_one_plugin(plugin_id) for plugin_id in plugin_ids}
95
+
96
+ def deploy_plugin(self, src_paths: list[Path], layer_ids: list[PluginRegistryLayerIdentifier], interactive: bool=False) -> dict[tuple[Path, str], list[tuple[str, str, Optional[Path], Optional[Plugin]]]]:
97
+ plugin_ids = [Plugin.id_from_jar(src_path) for src_path in src_paths]
98
+ return {(src_path, plugin_id): self._deploy_one_plugin(src_path,
99
+ plugin_id,
100
+ layer_ids,
101
+ interactive=interactive) for src_path, plugin_id in zip(src_paths, plugin_ids)}
102
+
103
+ def load_plugin_registries(self, plugin_registry_path_or_str: PathOrStr) -> TurtlesApp:
104
+ plugin_registry_path = path(plugin_registry_path_or_str)
105
+ if plugin_registry_path in map(lambda pr: pr.get_root(), self._plugin_registries):
106
+ raise ValueError(f'Plugin registries already loaded from: {plugin_registry_path!s}')
107
+ with plugin_registry_path.open('r') as fpr:
108
+ for yaml_obj in yaml.safe_load_all(fpr):
109
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginRegistryKind.__args__:
110
+ plugin_registry = PluginRegistry(**yaml_obj).initialize(plugin_registry_path)
111
+ self._plugin_registries.append(plugin_registry)
112
+ return self
113
+
114
+ def load_plugin_registry_catalogs(self, plugin_registry_catalog_path_or_str: PathOrStr) -> TurtlesApp:
115
+ plugin_registry_catalog_path = path(plugin_registry_catalog_path_or_str)
116
+ if plugin_registry_catalog_path in map(lambda prc: prc.get_root(), self._plugin_registry_catalogs):
117
+ raise ValueError(f'Plugin registry catalogs already loaded from: {plugin_registry_catalog_path!s}')
118
+ with plugin_registry_catalog_path.open('r') as fprc:
119
+ for yaml_obj in yaml.safe_load_all(fprc):
120
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginRegistryCatalogKind.__args__:
121
+ plugin_registry_catalog = PluginRegistryCatalog(**yaml_obj).initialize(plugin_registry_catalog_path)
122
+ self._plugin_registry_catalogs.append(plugin_registry_catalog)
123
+ for plugin_registry_file in plugin_registry_catalog.get_plugin_registry_files():
124
+ self.load_plugin_registries(plugin_registry_catalog_path.joinpath(plugin_registry_file))
125
+ return self
126
+
127
+ def load_plugin_set_catalogs(self, plugin_set_catalog_path_or_str: PathOrStr) -> TurtlesApp:
128
+ plugin_set_catalog_path = path(plugin_set_catalog_path_or_str)
129
+ if plugin_set_catalog_path in map(lambda psc: psc.get_root(), self._plugin_set_catalogs):
130
+ raise ValueError(f'Plugin set catalogs already loaded from: {plugin_set_catalog_path!s}')
131
+ with plugin_set_catalog_path.open('r') as fpsc:
132
+ for yaml_obj in yaml.safe_load_all(fpsc):
133
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSetCatalogKind.__args__:
134
+ plugin_set_catalog = PluginSetCatalog(**yaml_obj).initialize(plugin_set_catalog_path)
135
+ self._plugin_set_catalogs.append(plugin_set_catalog)
136
+ for plugin_set_file in plugin_set_catalog.get_plugin_set_files():
137
+ self.load_plugin_sets(plugin_set_catalog_path.joinpath(plugin_set_file))
138
+ return self
139
+
140
+ def load_plugin_sets(self, plugin_set_path_or_str: PathOrStr) -> TurtlesApp:
141
+ plugin_set_path = path(plugin_set_path_or_str)
142
+ if plugin_set_path in map(lambda ps: ps.get_root(), self._plugin_sets):
143
+ raise ValueError(f'Plugin sets already loaded from: {plugin_set_path!s}')
144
+ with plugin_set_path.open('r') as fps:
145
+ for yaml_obj in yaml.safe_load_all(fps):
146
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSetKind.__args__:
147
+ plugin_set = PluginSet(**yaml_obj).initialize(plugin_set_path)
148
+ self._plugin_sets.append(plugin_set)
149
+ return self
150
+
151
+ def load_plugin_signing_credentials(self, plugin_signing_credentials_path_or_str: PathOrStr) -> TurtlesApp:
152
+ plugin_signing_credentials_path = path(plugin_signing_credentials_path_or_str)
153
+ if self._plugin_signing_credentials:
154
+ raise ValueError(f'Plugin signing credentials already loaded from: {self._plugin_signing_credentials.get_root()!s}')
155
+ with plugin_signing_credentials_path.open('r') as fpsc:
156
+ self._plugin_signing_credentials = PluginSigningCredentials(**yaml.safe_load(fpsc)).initialize(plugin_signing_credentials_path)
157
+ return self
158
+
159
+ def release_plugin(self, plugin_ids: list[PluginIdentifier], layer_ids: list[PluginRegistryLayerIdentifier], interactive: bool=False) -> dict[str, list[tuple[str, str, Path, Plugin]]]:
160
+ # ... plugin_id -> (set_id, jar_path, plugin)
161
+ ret1 = self.build_plugin(plugin_ids)
162
+ jar_paths = [jar_path for set_id, jar_path, plugin in ret1.values()]
163
+ # ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
164
+ ret2 = self.deploy_plugin(jar_paths,
165
+ layer_ids,
166
+ interactive=interactive)
167
+ return {plugin_id: val for (jar_path, plugin_id), val in ret2.items()}
168
+
169
+ def set_password(self, pw: Union[Callable[[], str], str]) -> None:
170
+ self._password = pw if callable(pw) else lambda: pw
171
+
172
+ def _build_one_plugin(self, plugin_id: str) -> tuple[str, Optional[Path], Optional[Plugin]]:
173
+ for plugin_set in self._plugin_sets:
174
+ if plugin_set.has_plugin(plugin_id):
175
+ bp = plugin_set.build_plugin(plugin_id,
176
+ self._get_plugin_signing_keystore(),
177
+ self._get_plugin_signing_alias(),
178
+ self._get_plugin_signing_password())
179
+ return plugin_set.get_id(), bp[0] if bp else None, bp[1] if bp else None
180
+ raise Exception(f'{plugin_id}: not found in any plugin set')
181
+
182
+ def _deploy_one_plugin(self, src_jar: Path, plugin_id: PluginIdentifier, layer_ids: list[PluginRegistryLayerIdentifier], interactive: bool=False) -> list[tuple[str, str, Optional[Path], Optional[Plugin]]]:
183
+ ret = list()
184
+ for plugin_registry in self._plugin_registries:
185
+ if plugin_registry.has_plugin(plugin_id):
186
+ for layer_id in layer_ids:
187
+ layer = plugin_registry.get_layer(layer_id)
188
+ if layer is not None:
189
+ dp = layer.deploy_plugin(plugin_id,
190
+ src_jar,
191
+ interactive=interactive)
192
+ ret.append((plugin_registry.get_id(),
193
+ layer.get_id(),
194
+ dp[0] if dp else None,
195
+ dp[1] if dp else None))
196
+ if len(ret) == 0:
197
+ raise Exception(f'{src_jar}: {plugin_id} not declared in any plugin registry')
198
+ return ret
199
+
200
+ def _get_password(self) -> Optional[str]:
201
+ return self._password() if self._password else None
202
+
203
+ def _get_plugin_signing_alias(self) -> str:
204
+ return self._plugin_signing_credentials.get_plugin_signing_alias()
205
+
206
+ def _get_plugin_signing_keystore(self) -> Path:
207
+ return self._plugin_signing_credentials.get_plugin_signing_keystore()
208
+
209
+ def _get_plugin_signing_password(self) -> str:
210
+ return self._get_password()
211
+
212
+ @staticmethod
213
+ def default_plugin_registry_catalog_choices() -> tuple[Path, ...]:
214
+ return TurtlesApp._default_files(TurtlesApp.PLUGIN_REGISTRY_CATALOG)
215
+
216
+ @staticmethod
217
+ def default_plugin_set_catalog_choices() -> tuple[Path, ...]:
218
+ return TurtlesApp._default_files(TurtlesApp.PLUGIN_SET_CATALOG)
219
+
220
+ @staticmethod
221
+ def default_plugin_signing_credentials_choices() -> tuple[Path, ...]:
222
+ return TurtlesApp._default_files(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS)
223
+
224
+ @staticmethod
225
+ def select_default_plugin_registry_catalog() -> Optional[Path]:
226
+ return TurtlesApp._select_file(TurtlesApp.default_plugin_registry_catalog_choices())
227
+
228
+ @staticmethod
229
+ def select_default_plugin_set_catalog() -> Optional[Path]:
230
+ return TurtlesApp._select_file(TurtlesApp.default_plugin_set_catalog_choices())
231
+
232
+ @staticmethod
233
+ def select_default_plugin_signing_credentials() -> Optional[Path]:
234
+ return TurtlesApp._select_file(TurtlesApp.default_plugin_signing_credentials_choices())
235
+
236
+ @staticmethod
237
+ def _default_files(file_str) -> tuple[Path, ...]:
238
+ return tuple(dir_path.joinpath(file_str) for dir_path in TurtlesApp.CONFIG_DIRS)
239
+
240
+ @staticmethod
241
+ def _select_file(choices: Iterable[Path]) -> Optional[Path]:
242
+ for p in choices:
243
+ if p.is_file():
244
+ return p
245
+ return None
@@ -41,73 +41,105 @@ from lockss.pybasic.outpututil import OutputFormatOptions
41
41
  from pydantic.v1 import BaseModel, Field, FilePath
42
42
  from pydantic.v1.class_validators import validator
43
43
  import tabulate
44
- from typing import List, Optional
44
+ from typing import Optional
45
45
 
46
+ from lockss.turtles.plugin_registry import PluginRegistryLayer
46
47
  from . import __copyright__, __license__, __version__
47
48
  from .app import TurtlesApp
49
+ from .plugin_registry import PluginRegistryLayerIdentifier
50
+ from .util import file_or
48
51
 
49
52
 
50
53
  class PluginBuildingOptions(BaseModel):
51
- plugin_set_catalog: Optional[FilePath] = Field(aliases=['-s'], description=f'(plugin set catalog) load the plugin set catalog from the given file, or if none, from {" or ".join(map(str, TurtlesApp.default_plugin_set_catalogs()))}')
52
- plugin_signing_credentials: Optional[FilePath] = Field(aliases=['-c'], description=f'(plugin signing credentials) load the plugin signing credentials from the given file, or if none, from {" or ".join(map(str, TurtlesApp.default_plugin_signing_credentials()))}')
54
+ plugin_set: Optional[list[FilePath]] = Field(aliases=['-s'], description=f'(plugin sets) add one or more plugin sets to the loaded plugin sets')
55
+ plugin_set_catalog: Optional[list[FilePath]] = Field(aliases=['-S'], description=f'(plugin sets) add one or more plugin set catalogs to the loaded plugin set catalogs; if no plugin set catalogs or plugin sets are specified, load {file_or(TurtlesApp.default_plugin_set_catalog_choices())}')
56
+ plugin_signing_credentials: Optional[FilePath] = Field(aliases=['-c'], description=f'(plugin signing credentials) load the plugin signing credentials from the given file, or if none, from {file_or(TurtlesApp.default_plugin_signing_credentials_choices())}')
53
57
  plugin_signing_password: Optional[str] = Field(description='(plugin signing credentials) set the plugin signing password, or if none, prompt interactively')
54
58
 
59
+ def get_plugin_sets(self) -> list[Path]:
60
+ return [path(p) for p in self.plugin_set or []]
61
+
62
+ def get_plugin_set_catalogs(self) -> list[Path]:
63
+ if self.plugin_set or self.plugin_set_catalog:
64
+ return [path(p) for p in self.plugin_set_catalog or []]
65
+ if single := TurtlesApp.select_default_plugin_set_catalog():
66
+ return [single]
67
+ raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
68
+
69
+ def get_plugin_signing_credentials(self) -> Path:
70
+ if self.plugin_signing_credentials:
71
+ return path(self.plugin_signing_credentials)
72
+ if ret := TurtlesApp.select_default_plugin_signing_credentials():
73
+ return ret
74
+ raise FileNotFoundError(file_or(TurtlesApp.default_plugin_signing_credentials_choices()))
75
+
55
76
 
56
77
  class PluginDeploymentOptions(BaseModel):
57
- plugin_registry_catalog: Optional[FilePath] = Field(aliases=['-r'], description=f'(plugin registry catalog) load the plugin registry catalog from the given file, or if none, from {" or ".join(map(str, TurtlesApp.default_plugin_registry_catalogs()))}')
58
- plugin_registry_layer: Optional[List[str]] = Field(aliases=['-l'], description='(plugin registry layers) add one or more plugin registry layers to the set of plugin registry layers to process')
59
- plugin_registry_layers: Optional[List[FilePath]] = Field(aliases=['-L'], description='(plugin registry layers) add the plugin registry layers listed in one or more files to the set of plugin registry layers to process')
78
+ plugin_registry: Optional[list[FilePath]] = Field(aliases=['-r'], description=f'(plugin registry) add one or more plugin registries to the loaded plugin registries')
79
+ plugin_registry_catalog: Optional[list[FilePath]] = Field(aliases=['-R'], description=f'(plugin registry) add one or more plugin registry catalogs to the loaded plugin registry catalogs; if no plugin registry catalogs or plugin registries are specified, load {file_or(TurtlesApp.default_plugin_registry_catalog_choices())}')
80
+ plugin_registry_layer: Optional[list[str]] = Field(aliases=['-l'], description='(plugin registry layers) add one or more plugin registry layers to the set of plugin registry layers to process')
81
+ plugin_registry_layers: Optional[list[FilePath]] = Field(aliases=['-L'], description='(plugin registry layers) add the plugin registry layers listed in one or more files to the set of plugin registry layers to process')
60
82
  testing: Optional[bool] = Field(False, aliases=['-t'], description='(plugin registry layers) synonym for --plugin-registry-layer testing (i.e. add "testing" to the list of plugin registry layers to process)')
61
83
  production: Optional[bool] = Field(False, aliases=['-p'], description='(plugin registry layers) synonym for --plugin-registry-layer production (i.e. add "production" to the list of plugin registry layers to process)')
62
84
 
63
85
  @validator('plugin_registry_layers', each_item=True, pre=True)
64
- def _expand_each_plugin_registry_layers_path(cls, v: Path):
86
+ def _expand_each_plugin_registry_layers_path(cls, v: Path) -> Path:
65
87
  return path(v)
66
88
 
67
- def get_plugin_registry_layers(self):
68
- ret = [*self.plugin_registry_layer[:], *[file_lines(file_path) for file_path in self.plugin_registry_layers]]
89
+ def get_plugin_registries(self) -> list[Path]:
90
+ return [path(p) for p in self.plugin_registry or []]
91
+
92
+ def get_plugin_registry_catalogs(self) -> list[Path]:
93
+ if self.plugin_registry or self.plugin_registry_catalog:
94
+ return [path(p) for p in self.plugin_registry_catalog or []]
95
+ if single := TurtlesApp.select_default_plugin_registry_catalog():
96
+ return [single]
97
+ raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
98
+
99
+ def get_plugin_registry_layers(self) -> list[PluginRegistryLayerIdentifier]:
100
+ ret = [*(self.plugin_registry_layer or []), *[file_lines(file_path) for file_path in self.plugin_registry_layers or []]]
69
101
  for layer in reversed(['testing', 'production']):
70
102
  if getattr(self, layer, False):
71
103
  ret.insert(0, layer)
72
- if len(ret) == 0:
73
- raise RuntimeError('empty list of plugin registry layers')
74
- return ret
104
+ if ret:
105
+ return ret
106
+ raise ValueError('Empty list of plugin registry layers')
75
107
 
76
108
 
77
109
  class PluginIdentifierOptions(BaseModel):
78
110
  """
79
111
  The --identifier/-i and --identifiers/-I options.
80
112
  """
81
- plugin_identifier: Optional[List[str]] = Field([], aliases=['-i'], description='(plugin identifiers) add one or more plugin identifiers to the set of plugin identifiers to process')
82
- plugin_identifiers: Optional[List[FilePath]] = Field([], aliases=['-I'], description='(plugin identifiers) add the plugin identifiers listed in one or more files to the set of plugin identifiers to process')
113
+ plugin_identifier: Optional[list[str]] = Field(aliases=['-i'], description='(plugin identifiers) add one or more plugin identifiers to the set of plugin identifiers to process')
114
+ plugin_identifiers: Optional[list[FilePath]] = Field(aliases=['-I'], description='(plugin identifiers) add the plugin identifiers listed in one or more files to the set of plugin identifiers to process')
83
115
 
84
116
  @validator('plugin_identifiers', each_item=True, pre=True)
85
- def _expand_each_plugin_identifiers_path(cls, v: Path):
117
+ def _expand_each_plugin_identifiers_path(cls, v: Path) -> Path:
86
118
  return path(v)
87
119
 
88
- def get_plugin_identifiers(self) -> List[str]:
89
- ret = [*self.plugin_identifier[:], *[file_lines(file_path) for file_path in self.plugin_identifiers]]
90
- if len(ret) == 0:
91
- raise RuntimeError('empty list of plugin identifiers')
92
- return ret
120
+ def get_plugin_identifiers(self) -> list[str]:
121
+ ret = [*(self.plugin_identifier or []), *[file_lines(file_path) for file_path in self.plugin_identifiers or []]]
122
+ if ret:
123
+ return ret
124
+ raise ValueError('Empty list of plugin identifiers')
93
125
 
94
126
 
95
127
  class PluginJarOptions(BaseModel):
96
128
  """
97
129
  The --plugin-jar/-j and --plugin-jars/-J options.
98
130
  """
99
- plugin_jar: Optional[List[FilePath]] = Field([], aliases=['-j'], description='(plugin JARs) add one or more plugin JARs to the set of plugin JARs to process')
100
- plugin_jars: Optional[List[FilePath]] = Field([], aliases=['-J'], description='(plugin JARs) add the plugin JARs listed in one or more files to the set of plugin JARs to process')
131
+ plugin_jar: Optional[list[FilePath]] = Field(aliases=['-j'], description='(plugin JARs) add one or more plugin JARs to the set of plugin JARs to process')
132
+ plugin_jars: Optional[list[FilePath]] = Field(aliases=['-J'], description='(plugin JARs) add the plugin JARs listed in one or more files to the set of plugin JARs to process')
101
133
 
102
134
  @validator('plugin_jar', 'plugin_jars', each_item=True, pre=True)
103
- def _expand_each_plugin_jars_path(cls, v: Path):
135
+ def _expand_each_plugin_jars_path(cls, v: Path) -> Path:
104
136
  return path(v)
105
137
 
106
138
  def get_plugin_jars(self):
107
- ret = [*self.plugin_jar[:], *[file_lines(file_path) for file_path in self.plugin_jars]]
108
- if len(ret) == 0:
109
- raise RuntimeError('empty list of plugin JARs')
110
- return ret
139
+ ret = [*(self.plugin_jar or []), *[file_lines(file_path) for file_path in self.plugin_jars or []]]
140
+ if len(ret):
141
+ return ret
142
+ raise ValueError('Empty list of plugin JARs')
111
143
 
112
144
 
113
145
  class NonInteractiveOptions(BaseModel):
@@ -208,10 +240,12 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
208
240
  return self._build_plugin(build_plugin_command)
209
241
 
210
242
  def _build_plugin(self, build_plugin_command: BuildPluginCommand) -> None:
211
- # Prerequisites
212
- self._app.load_plugin_sets(build_plugin_command.plugin_set_catalog)
213
- self._app.load_plugin_signing_credentials(build_plugin_command.plugin_signing_credentials)
214
- self._obtain_password(build_plugin_command)
243
+ for psc in build_plugin_command.get_plugin_set_catalogs():
244
+ self._app.load_plugin_set_catalogs(psc)
245
+ for ps in build_plugin_command.get_plugin_sets():
246
+ self._app.load_plugin_sets(ps)
247
+ self._app.load_plugin_signing_credentials(build_plugin_command.get_plugin_signing_credentials())
248
+ self._obtain_password(build_plugin_command, non_interactive=build_plugin_command.non_interactive)
215
249
  # Action
216
250
  # ... plugin_id -> (set_id, jar_path, plugin)
217
251
  ret = self._app.build_plugin(build_plugin_command.get_plugin_identifiers())
@@ -224,8 +258,10 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
224
258
  self._do_string_command(string_command)
225
259
 
226
260
  def _deploy_plugin(self, deploy_plugin_command: DeployPluginCommand) -> None:
227
- # Prerequisites
228
- self._app.load_plugin_registries(deploy_plugin_command.plugin_registry_catalog)
261
+ for prc in deploy_plugin_command.get_plugin_registry_catalogs():
262
+ self._app.load_plugin_registry_catalogs(prc)
263
+ for pr in deploy_plugin_command.get_plugin_registries():
264
+ self._app.load_plugin_registries(pr)
229
265
  # Action
230
266
  # ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
231
267
  ret = self._app.deploy_plugin(deploy_plugin_command.get_plugin_jars(),
@@ -245,21 +281,26 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
245
281
  def _license(self, string_command: StringCommand) -> None:
246
282
  self._do_string_command(string_command)
247
283
 
248
- def _obtain_password(self, non_interactive_options: NonInteractiveOptions) -> None:
249
- if non_interactive_options.plugin_signing_password is not None:
250
- _p = non_interactive_options.plugin_signing_password
251
- elif non_interactive_options.non_interactive:
284
+ def _obtain_password(self, plugin_building_options: PluginBuildingOptions, non_interactive: bool=False) -> None:
285
+ if plugin_building_options.plugin_signing_password:
286
+ _p = plugin_building_options.plugin_signing_password
287
+ elif not non_interactive:
252
288
  _p = getpass('Plugin signing password: ')
253
289
  else:
254
290
  self._parser.error('no plugin signing password specified while in non-interactive mode')
255
291
  self._app.set_password(lambda: _p)
256
292
 
257
293
  def _release_plugin(self, release_plugin_command: ReleasePluginCommand) -> None:
258
- # Prerequisites
259
- self._app.load_plugin_sets(release_plugin_command.plugin_set_catalog)
260
- self._app.load_plugin_registries(release_plugin_command.plugin_registry_catalog)
261
- self._app.load_plugin_signing_credentials(release_plugin_command.plugin_signing_credentials)
262
- self._obtain_password(release_plugin_command)
294
+ for psc in release_plugin_command.get_plugin_set_catalogs():
295
+ self._app.load_plugin_set_catalogs(psc)
296
+ for ps in release_plugin_command.get_plugin_sets():
297
+ self._app.load_plugin_sets(ps)
298
+ for prc in release_plugin_command.get_plugin_registry_catalogs():
299
+ self._app.load_plugin_registry_catalogs(prc)
300
+ for pr in release_plugin_command.get_plugin_registries():
301
+ self._app.load_plugin_registries(pr)
302
+ self._app.load_plugin_signing_credentials(release_plugin_command.get_plugin_signing_credentials())
303
+ self._obtain_password(release_plugin_command, non_interactive=release_plugin_command.non_interactive)
263
304
  # Action
264
305
  # ... plugin_id -> list of (registry_id, layer_id, dst_path, plugin)
265
306
  ret = self._app.release_plugin(release_plugin_command.get_plugin_identifiers(),
@@ -37,14 +37,19 @@ Library to represent a LOCKSS plugin.
37
37
  from __future__ import annotations
38
38
 
39
39
  from collections.abc import Callable
40
- from pathlib import Path, PurePath
41
- from typing import Any, List, Optional, Union
40
+ from pathlib import Path
41
+ from typing import Any, Optional
42
42
  import xml.etree.ElementTree as ET
43
43
  from zipfile import ZipFile
44
44
 
45
- import java_manifest as JM
45
+ import java_manifest
46
46
  from lockss.pybasic.fileutil import path
47
47
 
48
+ from .util import PathOrStr
49
+
50
+
51
+ PluginIdentifier = str
52
+
48
53
 
49
54
  class Plugin(object):
50
55
 
@@ -56,7 +61,7 @@ class Plugin(object):
56
61
  if tag != 'map':
57
62
  raise RuntimeError(f'{plugin_path!s}: invalid root element: {tag}')
58
63
 
59
- def get_aux_packages(self) -> List[str]:
64
+ def get_aux_packages(self) -> list[str]:
60
65
  key = 'plugin_aux_packages'
61
66
  lst = [x[1] for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
62
67
  if lst is None or len(lst) < 1:
@@ -65,13 +70,13 @@ class Plugin(object):
65
70
  raise ValueError(f'plugin declares {len(lst)} entries for {key}')
66
71
  return [x.text for x in lst[0].findall('string')]
67
72
 
68
- def get_identifier(self) -> Optional[str]:
73
+ def get_identifier(self) -> Optional[PluginIdentifier]:
69
74
  return self._only_one('plugin_identifier')
70
75
 
71
76
  def get_name(self) -> Optional[str]:
72
77
  return self._only_one('plugin_name')
73
78
 
74
- def get_parent_identifier(self) -> Optional[str]:
79
+ def get_parent_identifier(self) -> Optional[PluginIdentifier]:
75
80
  return self._only_one('plugin_parent')
76
81
 
77
82
  def get_parent_version(self) -> Optional[int]:
@@ -89,7 +94,7 @@ class Plugin(object):
89
94
  return result(lst[0])
90
95
 
91
96
  @staticmethod
92
- def from_jar(jar_path: Union[PurePath, str]) -> Plugin:
97
+ def from_jar(jar_path: PathOrStr) -> Plugin:
93
98
  jar_path = path(jar_path) # in case it's a string
94
99
  plugin_id = Plugin.id_from_jar(jar_path)
95
100
  plugin_fstr = str(Plugin.id_to_file(plugin_id))
@@ -98,19 +103,19 @@ class Plugin(object):
98
103
  return Plugin(plugin_file, plugin_fstr)
99
104
 
100
105
  @staticmethod
101
- def from_path(fpath: Union[PurePath, str]) -> Plugin:
106
+ def from_path(fpath: PathOrStr) -> Plugin:
102
107
  fpath = path(fpath) # in case it's a string
103
108
  with open(fpath, 'r') as input_file:
104
109
  return Plugin(input_file, fpath)
105
110
 
106
111
  @staticmethod
107
- def file_to_id(plugin_fstr: str) -> str:
112
+ def file_to_id(plugin_fstr: str) -> PluginIdentifier:
108
113
  return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
109
114
 
110
115
  @staticmethod
111
- def id_from_jar(jar_path: Union[PurePath, str]) -> str:
116
+ def id_from_jar(jar_path: PathOrStr) -> PluginIdentifier:
112
117
  jar_path = path(jar_path) # in case it's a string
113
- manifest = JM.from_jar(jar_path)
118
+ manifest = java_manifest.from_jar(jar_path)
114
119
  for entry in manifest:
115
120
  if entry.get('Lockss-Plugin') == 'true':
116
121
  name = entry.get('Name')
@@ -121,9 +126,9 @@ class Plugin(object):
121
126
  raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
122
127
 
123
128
  @staticmethod
124
- def id_to_dir(plugin_id: str) -> Path:
129
+ def id_to_dir(plugin_id: PluginIdentifier) -> Path:
125
130
  return Plugin.id_to_file(plugin_id).parent
126
131
 
127
132
  @staticmethod
128
- def id_to_file(plugin_id: str) -> Path:
133
+ def id_to_file(plugin_id: PluginIdentifier) -> Path:
129
134
  return Path(f'{plugin_id.replace(".", "/")}.xml')