lockss-turtles 0.6.0.dev2__py3-none-any.whl → 0.6.0.dev4__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.
@@ -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-dev4'
9
9
 
10
10
  __copyright__ = '''
11
11
  Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
lockss/turtles/app.py CHANGED
@@ -28,79 +28,135 @@
28
28
  # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
29
  # POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
- from collections.abc import Callable
32
- import importlib.resources as IR
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
33
36
  from pathlib import Path
34
- from typing import Dict, List, Optional, Tuple, Union
37
+ from typing import ClassVar, Literal, Optional, Union
35
38
 
39
+ import yaml
36
40
  from lockss.pybasic.fileutil import path
41
+ from pydantic import BaseModel, Field
37
42
  import xdg
38
43
 
39
- from . import resources as __resources__
40
- from .plugin import Plugin
41
- from .plugin_registry import PluginRegistry, PluginRegistryCatalog
42
- from .plugin_set import PluginSet, PluginSetCatalog
43
- from .util import YamlT, load_and_validate
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
44
48
 
45
49
 
46
- class TurtlesApp(object):
47
50
 
48
- CONFIG_DIR_NAME = 'lockss-turtles'
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
49
61
 
50
- XDG_CONFIG_DIR: Path = xdg.xdg_config_home().joinpath(CONFIG_DIR_NAME)
62
+ def get_plugin_signing_keystore(self) -> Path:
63
+ return self.get_root().joinpath(self.plugin_signing_keystore)
51
64
 
52
- USR_CONFIG_DIR: Path = Path('/usr/local/share', CONFIG_DIR_NAME)
53
65
 
54
- ETC_CONFIG_DIR: Path = Path('/etc', CONFIG_DIR_NAME)
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)
55
71
 
56
- CONFIG_DIRS: List[Path] = [XDG_CONFIG_DIR, USR_CONFIG_DIR, ETC_CONFIG_DIR]
72
+ USR_CONFIG_DIR: ClassVar[Path] = Path('/usr/local/share', CONFIG_DIR_NAME)
57
73
 
58
- PLUGIN_REGISTRY_CATALOG: str = 'plugin-registry-catalog.yaml'
74
+ ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
59
75
 
60
- PLUGIN_SET_CATALOG: str = 'plugin-set-catalog.yaml'
76
+ CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, USR_CONFIG_DIR, ETC_CONFIG_DIR)
61
77
 
62
- PLUGIN_SIGNING_CREDENTIALS: str = 'plugin-signing-credentials.yaml'
78
+ PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
63
79
 
64
- PLUGIN_SIGNING_CREDENTIALS_SCHEMA: str = 'plugin-signing-credentials-schema.json'
80
+ PLUGIN_SET_CATALOG: ClassVar[str] = 'plugin-set-catalog.yaml'
81
+
82
+ PLUGIN_SIGNING_CREDENTIALS: ClassVar[str] = 'plugin-signing-credentials.yaml'
65
83
 
66
84
  def __init__(self) -> None:
67
85
  super().__init__()
68
86
  self._password: Optional[Callable[[], str]] = None
69
- self._plugin_registries: Optional[List[PluginRegistry]] = None
70
- self._plugin_sets: Optional[List[PluginSet]] = None
71
- self._plugin_signing_credentials: YamlT = 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
72
92
 
73
- def build_plugin(self, plugin_ids: List[str]) -> Dict[str, Tuple[str, Path, Plugin]]:
93
+ def build_plugin(self, plugin_ids: list[PluginIdentifier]) -> dict[str, tuple[str, Path, Plugin]]:
74
94
  return {plugin_id: self._build_one_plugin(plugin_id) for plugin_id in plugin_ids}
75
95
 
76
- def deploy_plugin(self, src_paths: List[Path], layer_ids: List[str], interactive: bool=False) -> Dict[Tuple[Path, str], List[Tuple[str, str, Optional[Path], Optional[Plugin]]]]:
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]]]]:
77
97
  plugin_ids = [Plugin.id_from_jar(src_path) for src_path in src_paths]
78
98
  return {(src_path, plugin_id): self._deploy_one_plugin(src_path,
79
99
  plugin_id,
80
100
  layer_ids,
81
101
  interactive=interactive) for src_path, plugin_id in zip(src_paths, plugin_ids)}
82
102
 
83
- def load_plugin_registries(self, plugin_registry_catalog_path: Optional[Union[Path, str]]=None) -> None:
84
- if self._plugin_registries is None:
85
- plugin_registry_catalog = PluginRegistryCatalog.from_path(self.select_plugin_registry_catalog(plugin_registry_catalog_path))
86
- self._plugin_registries = list()
87
- for plugin_registry_file in plugin_registry_catalog.get_plugin_registry_files():
88
- self._plugin_registries.extend(PluginRegistry.from_path(plugin_registry_file))
89
-
90
- def load_plugin_sets(self, plugin_set_catalog_path: Optional[Union[Path, str]]=None) -> None:
91
- if self._plugin_sets is None:
92
- plugin_set_catalog = PluginSetCatalog.from_path(self.select_plugin_set_catalog(plugin_set_catalog_path))
93
- self._plugin_sets = list()
94
- for plugin_set_file in plugin_set_catalog.get_plugin_set_files():
95
- self._plugin_sets.extend(PluginSet.from_path(plugin_set_file))
96
-
97
- def load_plugin_signing_credentials(self, plugin_signing_credentials_path: Optional[Union[Path, str]]=None) -> None:
98
- if self._plugin_signing_credentials is None:
99
- plugin_signing_credentials_path = path(plugin_signing_credentials_path) if plugin_signing_credentials_path else self._select_file(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS)
100
- with IR.path(__resources__, TurtlesApp.PLUGIN_SIGNING_CREDENTIALS_SCHEMA) as plugin_signing_credentials_schema_path:
101
- self._plugin_signing_credentials = load_and_validate(plugin_signing_credentials_schema_path, plugin_signing_credentials_path)
102
-
103
- def release_plugin(self, plugin_ids: List[str], layer_ids: List[str], interactive: bool=False) -> Dict[str, List[Tuple[str, str, Path, Plugin]]]:
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]]]:
104
160
  # ... plugin_id -> (set_id, jar_path, plugin)
105
161
  ret1 = self.build_plugin(plugin_ids)
106
162
  jar_paths = [jar_path for set_id, jar_path, plugin in ret1.values()]
@@ -110,19 +166,10 @@ class TurtlesApp(object):
110
166
  interactive=interactive)
111
167
  return {plugin_id: val for (jar_path, plugin_id), val in ret2.items()}
112
168
 
113
- def select_plugin_registry_catalog(self, preselected: Optional[Union[Path, str]]=None) -> Path:
114
- return TurtlesApp._select_file(TurtlesApp.PLUGIN_REGISTRY_CATALOG, preselected)
115
-
116
- def select_plugin_set_catalog(self, preselected: Optional[Union[Path, str]]=None) -> Path:
117
- return TurtlesApp._select_file(TurtlesApp.PLUGIN_SET_CATALOG, preselected)
118
-
119
- def select_plugin_signing_credentials(self, preselected: Optional[Union[Path, str]]=None) -> Path:
120
- return TurtlesApp._select_file(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS, preselected)
121
-
122
169
  def set_password(self, pw: Union[Callable[[], str], str]) -> None:
123
170
  self._password = pw if callable(pw) else lambda: pw
124
171
 
125
- def _build_one_plugin(self, plugin_id: str) -> Tuple[str, Optional[Path], Optional[Plugin]]:
172
+ def _build_one_plugin(self, plugin_id: str) -> tuple[str, Optional[Path], Optional[Plugin]]:
126
173
  for plugin_set in self._plugin_sets:
127
174
  if plugin_set.has_plugin(plugin_id):
128
175
  bp = plugin_set.build_plugin(plugin_id,
@@ -132,7 +179,7 @@ class TurtlesApp(object):
132
179
  return plugin_set.get_id(), bp[0] if bp else None, bp[1] if bp else None
133
180
  raise Exception(f'{plugin_id}: not found in any plugin set')
134
181
 
135
- def _deploy_one_plugin(self, src_jar: Path, plugin_id: str, layer_ids: List[str], interactive: bool=False) -> List[Tuple[str, str, Optional[Path], Optional[Plugin]]]:
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]]]:
136
183
  ret = list()
137
184
  for plugin_registry in self._plugin_registries:
138
185
  if plugin_registry.has_plugin(plugin_id):
@@ -154,39 +201,45 @@ class TurtlesApp(object):
154
201
  return self._password() if self._password else None
155
202
 
156
203
  def _get_plugin_signing_alias(self) -> str:
157
- return self._plugin_signing_credentials['plugin-signing-alias']
204
+ return self._plugin_signing_credentials.get_plugin_signing_alias()
158
205
 
159
- def _get_plugin_signing_keystore(self) -> str:
160
- return self._plugin_signing_credentials['plugin-signing-keystore']
206
+ def _get_plugin_signing_keystore(self) -> Path:
207
+ return self._plugin_signing_credentials.get_plugin_signing_keystore()
161
208
 
162
209
  def _get_plugin_signing_password(self) -> str:
163
210
  return self._get_password()
164
211
 
165
212
  @staticmethod
166
- def default_plugin_registry_catalogs() -> List[Path]:
213
+ def default_plugin_registry_catalog_choices() -> tuple[Path, ...]:
167
214
  return TurtlesApp._default_files(TurtlesApp.PLUGIN_REGISTRY_CATALOG)
168
215
 
169
216
  @staticmethod
170
- def default_plugin_set_catalogs() -> List[Path]:
217
+ def default_plugin_set_catalog_choices() -> tuple[Path, ...]:
171
218
  return TurtlesApp._default_files(TurtlesApp.PLUGIN_SET_CATALOG)
172
219
 
173
220
  @staticmethod
174
- def default_plugin_signing_credentials() -> List[Path]:
221
+ def default_plugin_signing_credentials_choices() -> tuple[Path, ...]:
175
222
  return TurtlesApp._default_files(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS)
176
223
 
177
224
  @staticmethod
178
- def _default_files(file_str) -> List[Path]:
179
- return [dir_path.joinpath(file_str) for dir_path in TurtlesApp.CONFIG_DIRS]
225
+ def select_default_plugin_registry_catalog() -> Optional[Path]:
226
+ return TurtlesApp._select_file(TurtlesApp.default_plugin_registry_catalog_choices())
180
227
 
181
228
  @staticmethod
182
- def _select_file(file_str, preselected: Optional[Union[Path, str]]=None) -> Path:
183
- if preselected:
184
- preselected = path(preselected)
185
- if not preselected.is_file():
186
- raise FileNotFoundError(str(preselected))
187
- return preselected
188
- choices = TurtlesApp._default_files(file_str)
189
- ret = next(filter(lambda f: f.is_file(), choices), None)
190
- if ret is None:
191
- raise FileNotFoundError(' or '.join(map(str, choices)))
192
- return ret
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
lockss/turtles/cli.py CHANGED
@@ -41,73 +41,104 @@ 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
46
  from . import __copyright__, __license__, __version__
47
47
  from .app import TurtlesApp
48
+ from .plugin_registry import PluginRegistryLayerIdentifier
49
+ from .util import file_or
48
50
 
49
51
 
50
52
  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()))}')
53
+ plugin_set: Optional[list[FilePath]] = Field(aliases=['-s'], description=f'(plugin sets) add one or more plugin sets to the loaded plugin sets')
54
+ 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())}')
55
+ 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
56
  plugin_signing_password: Optional[str] = Field(description='(plugin signing credentials) set the plugin signing password, or if none, prompt interactively')
54
57
 
58
+ def get_plugin_sets(self) -> list[Path]:
59
+ return [path(p) for p in self.plugin_set or []]
60
+
61
+ def get_plugin_set_catalogs(self) -> list[Path]:
62
+ if self.plugin_set or self.plugin_set_catalog:
63
+ return [path(p) for p in self.plugin_set_catalog or []]
64
+ if single := TurtlesApp.select_default_plugin_set_catalog():
65
+ return [single]
66
+ raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
67
+
68
+ def get_plugin_signing_credentials(self) -> Path:
69
+ if self.plugin_signing_credentials:
70
+ return path(self.plugin_signing_credentials)
71
+ if ret := TurtlesApp.select_default_plugin_signing_credentials():
72
+ return ret
73
+ raise FileNotFoundError(file_or(TurtlesApp.default_plugin_signing_credentials_choices()))
74
+
55
75
 
56
76
  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')
77
+ plugin_registry: Optional[list[FilePath]] = Field(aliases=['-r'], description=f'(plugin registry) add one or more plugin registries to the loaded plugin registries')
78
+ 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())}')
79
+ 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')
80
+ 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
81
  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
82
  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
83
 
63
84
  @validator('plugin_registry_layers', each_item=True, pre=True)
64
- def _expand_each_plugin_registry_layers_path(cls, v: Path):
85
+ def _expand_each_plugin_registry_layers_path(cls, v: Path) -> Path:
65
86
  return path(v)
66
87
 
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]]
88
+ def get_plugin_registries(self) -> list[Path]:
89
+ return [path(p) for p in self.plugin_registry or []]
90
+
91
+ def get_plugin_registry_catalogs(self) -> list[Path]:
92
+ if self.plugin_registry or self.plugin_registry_catalog:
93
+ return [path(p) for p in self.plugin_registry_catalog or []]
94
+ if single := TurtlesApp.select_default_plugin_registry_catalog():
95
+ return [single]
96
+ raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
97
+
98
+ def get_plugin_registry_layers(self) -> list[PluginRegistryLayerIdentifier]:
99
+ ret = [*(self.plugin_registry_layer or []), *[file_lines(file_path) for file_path in self.plugin_registry_layers or []]]
69
100
  for layer in reversed(['testing', 'production']):
70
101
  if getattr(self, layer, False):
71
102
  ret.insert(0, layer)
72
- if len(ret) == 0:
73
- raise RuntimeError('empty list of plugin registry layers')
74
- return ret
103
+ if ret:
104
+ return ret
105
+ raise ValueError('Empty list of plugin registry layers')
75
106
 
76
107
 
77
108
  class PluginIdentifierOptions(BaseModel):
78
109
  """
79
110
  The --identifier/-i and --identifiers/-I options.
80
111
  """
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')
112
+ 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')
113
+ 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
114
 
84
115
  @validator('plugin_identifiers', each_item=True, pre=True)
85
- def _expand_each_plugin_identifiers_path(cls, v: Path):
116
+ def _expand_each_plugin_identifiers_path(cls, v: Path) -> Path:
86
117
  return path(v)
87
118
 
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
119
+ def get_plugin_identifiers(self) -> list[str]:
120
+ ret = [*(self.plugin_identifier or []), *[file_lines(file_path) for file_path in self.plugin_identifiers or []]]
121
+ if ret:
122
+ return ret
123
+ raise ValueError('Empty list of plugin identifiers')
93
124
 
94
125
 
95
126
  class PluginJarOptions(BaseModel):
96
127
  """
97
128
  The --plugin-jar/-j and --plugin-jars/-J options.
98
129
  """
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')
130
+ 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')
131
+ 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
132
 
102
133
  @validator('plugin_jar', 'plugin_jars', each_item=True, pre=True)
103
- def _expand_each_plugin_jars_path(cls, v: Path):
134
+ def _expand_each_plugin_jars_path(cls, v: Path) -> Path:
104
135
  return path(v)
105
136
 
106
137
  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
138
+ ret = [*(self.plugin_jar or []), *[file_lines(file_path) for file_path in self.plugin_jars or []]]
139
+ if len(ret):
140
+ return ret
141
+ raise ValueError('Empty list of plugin JARs')
111
142
 
112
143
 
113
144
  class NonInteractiveOptions(BaseModel):
@@ -208,10 +239,12 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
208
239
  return self._build_plugin(build_plugin_command)
209
240
 
210
241
  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)
242
+ for psc in build_plugin_command.get_plugin_set_catalogs():
243
+ self._app.load_plugin_set_catalogs(psc)
244
+ for ps in build_plugin_command.get_plugin_sets():
245
+ self._app.load_plugin_sets(ps)
246
+ self._app.load_plugin_signing_credentials(build_plugin_command.get_plugin_signing_credentials())
247
+ self._obtain_password(build_plugin_command, non_interactive=build_plugin_command.non_interactive)
215
248
  # Action
216
249
  # ... plugin_id -> (set_id, jar_path, plugin)
217
250
  ret = self._app.build_plugin(build_plugin_command.get_plugin_identifiers())
@@ -224,8 +257,10 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
224
257
  self._do_string_command(string_command)
225
258
 
226
259
  def _deploy_plugin(self, deploy_plugin_command: DeployPluginCommand) -> None:
227
- # Prerequisites
228
- self._app.load_plugin_registries(deploy_plugin_command.plugin_registry_catalog)
260
+ for prc in deploy_plugin_command.get_plugin_registry_catalogs():
261
+ self._app.load_plugin_registry_catalogs(prc)
262
+ for pr in deploy_plugin_command.get_plugin_registries():
263
+ self._app.load_plugin_registries(pr)
229
264
  # Action
230
265
  # ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
231
266
  ret = self._app.deploy_plugin(deploy_plugin_command.get_plugin_jars(),
@@ -245,21 +280,26 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
245
280
  def _license(self, string_command: StringCommand) -> None:
246
281
  self._do_string_command(string_command)
247
282
 
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:
283
+ def _obtain_password(self, plugin_building_options: PluginBuildingOptions, non_interactive: bool=False) -> None:
284
+ if plugin_building_options.plugin_signing_password:
285
+ _p = plugin_building_options.plugin_signing_password
286
+ elif not non_interactive:
252
287
  _p = getpass('Plugin signing password: ')
253
288
  else:
254
289
  self._parser.error('no plugin signing password specified while in non-interactive mode')
255
290
  self._app.set_password(lambda: _p)
256
291
 
257
292
  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)
293
+ for psc in release_plugin_command.get_plugin_set_catalogs():
294
+ self._app.load_plugin_set_catalogs(psc)
295
+ for ps in release_plugin_command.get_plugin_sets():
296
+ self._app.load_plugin_sets(ps)
297
+ for prc in release_plugin_command.get_plugin_registry_catalogs():
298
+ self._app.load_plugin_registry_catalogs(prc)
299
+ for pr in release_plugin_command.get_plugin_registries():
300
+ self._app.load_plugin_registries(pr)
301
+ self._app.load_plugin_signing_credentials(release_plugin_command.get_plugin_signing_credentials())
302
+ self._obtain_password(release_plugin_command, non_interactive=release_plugin_command.non_interactive)
263
303
  # Action
264
304
  # ... plugin_id -> list of (registry_id, layer_id, dst_path, plugin)
265
305
  ret = self._app.release_plugin(release_plugin_command.get_plugin_identifiers(),
lockss/turtles/plugin.py CHANGED
@@ -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')