lockss-turtles 0.5.0.dev4__py3-none-any.whl → 0.6.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.
lockss/turtles/app.py CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- # Copyright (c) 2000-2023, Board of Trustees of Leland Stanford Jr. University
3
+ # Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
4
4
  #
5
5
  # Redistribution and use in source and binary forms, with or without
6
6
  # modification, are permitted provided that the following conditions are met:
@@ -28,103 +28,357 @@
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
- import importlib.resources
32
- from pathlib import Path
31
+ """
32
+ Module to represent Turtles operations.
33
+ """
33
34
 
34
- import xdg
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
36
+ from __future__ import annotations
35
37
 
36
- from lockss.turtles.plugin import Plugin
37
- from lockss.turtles.plugin_registry import PluginRegistry, PluginRegistryCatalog
38
- from lockss.turtles.plugin_set import PluginSet, PluginSetCatalog
39
- import lockss.turtles.resources
40
- from lockss.turtles.util import _load_and_validate, _path
38
+ # Remove in Python 3.11; see https://docs.python.org/3.11/library/exceptions.html#exception-groups
39
+ from exceptiongroup import ExceptionGroup
41
40
 
41
+ from collections.abc import Callable, Iterable
42
+ from pathlib import Path
43
+ from typing import ClassVar, Optional, Union
42
44
 
43
- class TurtlesApp(object):
45
+ from lockss.pybasic.fileutil import path
46
+ from pydantic import ValidationError
47
+ import xdg
48
+ import yaml
44
49
 
45
- XDG_CONFIG_DIR = xdg.xdg_config_home().joinpath(__package__)
50
+ from .plugin import Plugin, PluginIdentifier
51
+ from .plugin_registry import PluginRegistry, PluginRegistryCatalog, PluginRegistryCatalogKind, PluginRegistryIdentifier, PluginRegistryKind, PluginRegistryLayerIdentifier
52
+ from .plugin_set import PluginSet, PluginSetCatalog, PluginSetCatalogKind, PluginSetKind
53
+ from .plugin_signing_credentials import PluginSigningCredentials, PluginSigningCredentialsKind
54
+ from .util import PathOrStr
46
55
 
47
- USR_CONFIG_DIR = Path('/usr/local/share', __package__)
48
56
 
49
- ETC_CONFIG_DIR = Path('/etc', __package__)
57
+ #: Type alias for the result of a single plugin building operation.
58
+ #: First item (index 0): identifier of the plugin set that had the given plugin.
59
+ #: Second item (index 1): plugin JAR file path (or None if not built).
60
+ #: Third item (index 2): plugin object (or None if not built).
61
+ BuildPluginResult = tuple[str, Optional[Path], Optional[Plugin]]
50
62
 
51
- CONFIG_DIRS = [XDG_CONFIG_DIR, USR_CONFIG_DIR, ETC_CONFIG_DIR]
52
63
 
53
- PLUGIN_REGISTRY_CATALOG = 'plugin-registry-catalog.yaml'
64
+ #: Type alias for the result of a single plugin deployment operation to a given
65
+ #: plugin registry layer.
66
+ #: First item (index 0):
67
+ #: Second item (index 1):
68
+ #: Third item (index 2): deployed JAR file path (or None if not deployed).
69
+ #: Fourth item (index 3): plugin object (or None if not deployed).
70
+ DeployPluginResult = tuple[PluginRegistryIdentifier, PluginRegistryLayerIdentifier, Optional[Path], Optional[Plugin]]
54
71
 
55
- PLUGIN_SET_CATALOG = 'plugin-set-catalog.yaml'
56
72
 
57
- PLUGIN_SIGNING_CREDENTIALS = 'plugin-signing-credentials.yaml'
73
+ class Turtles(object):
74
+ """
75
+ A Turtles command object, which can be used to execute Turtles operations.
76
+ """
58
77
 
59
- PLUGIN_SIGNING_CREDENTIALS_SCHEMA = 'plugin-signing-credentials-schema.json'
78
+ #: The name of a Turtles configuration directory.
79
+ CONFIG_DIR_NAME: ClassVar[str] = 'lockss-turtles'
60
80
 
61
- @staticmethod
62
- def _default_files(file_str):
63
- return [dir_path.joinpath(file_str) for dir_path in TurtlesApp.CONFIG_DIRS]
81
+ #: The Turtles configuration directory under ``$XDG_CONFIG_HOME`` (by
82
+ # default ``$HOME/.config``, which is typically ``/home/$USER/.config``).
83
+ XDG_CONFIG_DIR: ClassVar[Path] = Path(xdg.xdg_config_home(), CONFIG_DIR_NAME)
64
84
 
65
- @staticmethod
66
- def _select_file(file_str, preselected=None):
67
- if preselected:
68
- preselected = _path(preselected)
69
- if not preselected.is_file():
70
- raise FileNotFoundError(str(preselected))
71
- return preselected
72
- choices = TurtlesApp._default_files(file_str)
73
- ret = next(filter(Path.is_file, choices), None)
74
- if ret is None:
75
- raise FileNotFoundError(' or '.join(map(str, choices)))
76
- return ret
85
+ #: The Turtles configuration directory under ``/etc``.
86
+ ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
77
87
 
78
- def __init__(self):
79
- super().__init__()
80
- self._password = None
81
- self._plugin_registries = None
82
- self._plugin_sets = None
83
- self._plugin_signing_credentials = None
88
+ #: The Turtles configuration directory under ``/usr/local/share``.
89
+ USR_CONFIG_DIR: ClassVar[Path] = Path('/usr/local/share', CONFIG_DIR_NAME)
84
90
 
85
- # Returns plugin_id -> (set_id, jar_path, plugin)
86
- def build_plugin(self, plugin_ids):
87
- return {plugin_id: self._build_one_plugin(plugin_id) for plugin_id in plugin_ids}
91
+ #: The Turtles configuration directories in order of preference:
92
+ #: ``XDG_CONFIG_DIR``, ``ETC_CONFIG_DIR``, ``USR_CONFIG_DIR``
93
+ CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, ETC_CONFIG_DIR, USR_CONFIG_DIR)
88
94
 
89
- def default_plugin_registry_catalogs(self):
90
- return TurtlesApp._default_files(TurtlesApp.PLUGIN_REGISTRY_CATALOG)
95
+ #: The default plugin registry catalog file name.
96
+ PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
91
97
 
92
- def default_plugin_set_catalogs(self):
93
- return TurtlesApp._default_files(TurtlesApp.PLUGIN_SET_CATALOG)
98
+ #: The default plugin set catalog file name.
99
+ PLUGIN_SET_CATALOG: ClassVar[str] = 'plugin-set-catalog.yaml'
94
100
 
95
- def default_plugin_signing_credentials(self):
96
- return TurtlesApp._default_files(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS)
101
+ #: The default plugin signing credentials file name.
102
+ PLUGIN_SIGNING_CREDENTIALS: ClassVar[str] = 'plugin-signing-credentials.yaml'
103
+
104
+ def __init__(self) -> None:
105
+ """
106
+ Constructor.
107
+ """
108
+ super().__init__()
109
+ self._plugin_signing_password_callable: Optional[Callable[[], str]] = None
110
+ self._plugin_registries: list[PluginRegistry] = list()
111
+ self._plugin_registry_catalogs: list[PluginRegistryCatalog] = list()
112
+ self._plugin_set_catalogs: list[PluginSetCatalog] = list()
113
+ self._plugin_sets: list[PluginSet] = list()
114
+ self._plugin_signing_credentials: Optional[PluginSigningCredentials] = None
115
+
116
+ def build_plugin(self,
117
+ plugin_id_or_plugin_ids: Union[PluginIdentifier, list[PluginIdentifier]]) -> dict[str, BuildPluginResult]:
118
+ """
119
+ Builds zero or more plugins.
120
+
121
+ :param plugin_id_or_plugin_ids: Either one plugin identifier, or a list
122
+ of plugin identifiers.
123
+ :type plugin_id_or_plugin_ids: Union[PluginIdentifier, list[PluginIdentifier]]
124
+ :return: A mapping from plugin identifier to build plugin result; if no
125
+ plugin identifiers were given, the result is an empty mapping.
126
+ :rtype: dict[str, BuildPluginResult]
127
+ :raises Exception: If a given plugin identifier is not found in any
128
+ loaded plugin set.
129
+ """
130
+ plugin_ids: list[PluginIdentifier] = plugin_id_or_plugin_ids if isinstance(plugin_id_or_plugin_ids, list) else [plugin_id_or_plugin_ids]
131
+ return {plugin_id: self._build_one_plugin(plugin_id) for plugin_id in plugin_ids}
97
132
 
98
- # Returns (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
99
- def deploy_plugin(self, src_paths, layer_ids, interactive=False):
100
- plugin_ids = [Plugin.id_from_jar(src_path) for src_path in src_paths]
133
+ def deploy_plugin(self,
134
+ src_path_or_src_paths: Union[Path, list[Path]],
135
+ layer_id_or_layer_ids: Union[PluginRegistryLayerIdentifier, list[PluginRegistryLayerIdentifier]],
136
+ interactive: bool=False) -> dict[tuple[Path, PluginIdentifier], list[DeployPluginResult]]:
137
+ """
138
+ Deploys zero or more plugins.
139
+
140
+ :param src_path_or_src_paths: Either one signed JAR file paths or a list
141
+ of signed JAR file paths.
142
+ :type src_path_or_src_paths: Union[Path, list[Path]]
143
+ :param layer_id_or_layer_ids: Either one plugin registry layer
144
+ identifier or a list of plugin registry
145
+ layer identifiers.
146
+ :type layer_id_or_layer_ids: Union[PluginRegistryLayerIdentifier, list[PluginRegistryLayerIdentifier]]
147
+ :param interactive: Whether interactive prompts are allowed (default
148
+ False).
149
+ :type interactive: bool
150
+ :return: A mapping from tuples of signed JAR file path and corresponding
151
+ plugin identifier to a list of build deployment results (one
152
+ per plugin registry layer); if no signed JAR file paths were
153
+ given, the result is an empty mapping.
154
+ :rtype: dict[tuple[Path, PluginIdentifier], list[DeployPluginResult]]
155
+ :raises Exception: If a given plugin is not declared in any loaded
156
+ plugin registry.
157
+ """
158
+ src_paths: list[Path] = src_path_or_src_paths if isinstance(src_path_or_src_paths, list) else [src_path_or_src_paths]
159
+ layer_ids: list[PluginRegistryLayerIdentifier] = layer_id_or_layer_ids if isinstance(layer_id_or_layer_ids, list) else [layer_id_or_layer_ids]
160
+ plugin_ids = [Plugin.id_from_jar(src_path) for src_path in src_paths] # FIXME: should go down to _deploy_one_plugin?
101
161
  return {(src_path, plugin_id): self._deploy_one_plugin(src_path,
102
162
  plugin_id,
103
163
  layer_ids,
104
164
  interactive=interactive) for src_path, plugin_id in zip(src_paths, plugin_ids)}
105
165
 
106
- def load_plugin_registries(self, plugin_registry_catalog_path=None):
107
- if self._plugin_registries is None:
108
- plugin_registry_catalog = PluginRegistryCatalog.from_path(self.select_plugin_registry_catalog(plugin_registry_catalog_path))
109
- self._plugin_registries = list()
110
- for plugin_registry_file in plugin_registry_catalog.get_plugin_registry_files():
111
- self._plugin_registries.extend(PluginRegistry.from_path(plugin_registry_file))
112
-
113
- def load_plugin_sets(self, plugin_set_catalog_path=None):
114
- if self._plugin_sets is None:
115
- plugin_set_catalog = PluginSetCatalog.from_path(self.select_plugin_set_catalog(plugin_set_catalog_path))
116
- self._plugin_sets = list()
117
- for plugin_set_file in plugin_set_catalog.get_plugin_set_files():
118
- self._plugin_sets.extend(PluginSet.from_path(plugin_set_file))
119
-
120
- def load_plugin_signing_credentials(self, plugin_signing_credentials_path=None):
121
- if self._plugin_signing_credentials is None:
122
- plugin_signing_credentials_path = _path(plugin_signing_credentials_path) if plugin_signing_credentials_path else self._select_file(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS)
123
- with importlib.resources.path(lockss.turtles.resources, TurtlesApp.PLUGIN_SIGNING_CREDENTIALS_SCHEMA) as plugin_signing_credentials_schema_path:
124
- self._plugin_signing_credentials = _load_and_validate(plugin_signing_credentials_schema_path, plugin_signing_credentials_path)
125
-
126
- # Returns plugin_id -> list of (registry_id, layer_id, dst_path, plugin)
127
- def release_plugin(self, plugin_ids, layer_ids, interactive=False):
166
+ def load_plugin_registries(self,
167
+ plugin_registry_path_or_str: PathOrStr) -> Turtles:
168
+ """
169
+ Processes the given YAML file, loading all plugin registry definitions
170
+ it contains, ignoring other YAML objects.
171
+
172
+ :param plugin_registry_path_or_str: A file path (or string).
173
+ :type plugin_registry_path_or_str: PathOrStr
174
+ :return: This Turtles object (for chaining).
175
+ :rtype: Turtles
176
+ :raises ExceptionGroup: If one or more errors occur while loading plugin
177
+ registry definitions.
178
+ :raises ValueError: If the given file has already been processed or if
179
+ it contains no plugin registry definitions.
180
+ """
181
+ plugin_registry_path = path(plugin_registry_path_or_str)
182
+ if plugin_registry_path in map(lambda pr: pr.get_root(), self._plugin_registries):
183
+ raise ValueError(f'Plugin registries already loaded from: {plugin_registry_path!s}')
184
+ errs, at_least_one = [], False
185
+ with plugin_registry_path.open('r') as fpr:
186
+ for yaml_obj in yaml.safe_load_all(fpr):
187
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginRegistryKind.__args__:
188
+ try:
189
+ plugin_registry = PluginRegistry(**yaml_obj).initialize(plugin_registry_path.parent)
190
+ self._plugin_registries.append(plugin_registry)
191
+ at_least_one = True
192
+ except ValidationError as ve:
193
+ errs.append(ve)
194
+ if errs:
195
+ raise ExceptionGroup(f'Errors while loading plugin registries from: {plugin_registry_path!s}', errs)
196
+ if not at_least_one:
197
+ raise ValueError(f'No plugin registries found in: {plugin_registry_path!s}')
198
+ return self
199
+
200
+ def load_plugin_registry_catalogs(self,
201
+ plugin_registry_catalog_path_or_str: PathOrStr) -> Turtles:
202
+ """
203
+ Processes the given YAML file, loading all plugin registry catalog
204
+ definitions it contains and in turn all plugin registry definitions they
205
+ reference, ignoring other YAML objects.
206
+
207
+ :param plugin_registry_catalog_path_or_str: A file path (or string).
208
+ :type plugin_registry_catalog_path_or_str: PathOrStr
209
+ :return: This Turtles object (for chaining).
210
+ :rtype: Turtles
211
+ :raises ExceptionGroup: If one or more errors occur while loading plugin
212
+ registry catalog definitions or the plugin
213
+ registry definitions they reference.
214
+ :raises ValueError: If the given file has already been processed or if
215
+ it contains no plugin registry catalog definitions.
216
+ """
217
+ plugin_registry_catalog_path = path(plugin_registry_catalog_path_or_str)
218
+ if plugin_registry_catalog_path in map(lambda prc: prc.get_root(), self._plugin_registry_catalogs):
219
+ raise ValueError(f'Plugin registry catalogs already loaded from: {plugin_registry_catalog_path!s}')
220
+ errs, at_least_one = [], False
221
+ with plugin_registry_catalog_path.open('r') as fprc:
222
+ for yaml_obj in yaml.safe_load_all(fprc):
223
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginRegistryCatalogKind.__args__:
224
+ try:
225
+ plugin_registry_catalog = PluginRegistryCatalog(**yaml_obj).initialize(plugin_registry_catalog_path.parent)
226
+ self._plugin_registry_catalogs.append(plugin_registry_catalog)
227
+ at_least_one = True
228
+ for plugin_registry_file in plugin_registry_catalog.get_plugin_registry_files():
229
+ try:
230
+ self.load_plugin_registries(plugin_registry_catalog_path.joinpath(plugin_registry_file))
231
+ except ValueError as ve:
232
+ errs.append(ve)
233
+ except ExceptionGroup as eg:
234
+ errs.extend(eg.exceptions)
235
+ except ValidationError as ve:
236
+ errs.append(ve)
237
+ if errs:
238
+ raise ExceptionGroup(f'Errors while loading plugin registry catalogs from: {plugin_registry_catalog_path!s}', errs)
239
+ if not at_least_one:
240
+ raise ValueError(f'No plugin registry catalogs found in: {plugin_registry_catalog_path!s}')
241
+ return self
242
+
243
+ def load_plugin_set_catalogs(self,
244
+ plugin_set_catalog_path_or_str: PathOrStr) -> Turtles:
245
+ """
246
+ Processes the given YAML file, loading all plugin set catalog
247
+ definitions it contains and in turn all plugin set definitions they
248
+ reference, ignoring other YAML objects.
249
+
250
+ :param plugin_set_catalog_path_or_str: A file path (or string).
251
+ :type plugin_set_catalog_path_or_str: PathOrStr
252
+ :return: This Turtles object (for chaining).
253
+ :rtype: Turtles
254
+ :raises ExceptionGroup: If one or more errors occur while loading plugin
255
+ set catalog definitions or the plugin set
256
+ definitions they reference.
257
+ :raises ValueError: If the given file has already been processed or if
258
+ it contains no plugin set catalog definitions.
259
+ """
260
+ plugin_set_catalog_path = path(plugin_set_catalog_path_or_str)
261
+ if plugin_set_catalog_path in map(lambda psc: psc.get_root(), self._plugin_set_catalogs):
262
+ raise ValueError(f'Plugin set catalogs already loaded from: {plugin_set_catalog_path!s}')
263
+ errs, at_least_one = [], False
264
+ with plugin_set_catalog_path.open('r') as fpsc:
265
+ for yaml_obj in yaml.safe_load_all(fpsc):
266
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSetCatalogKind.__args__:
267
+ try:
268
+ plugin_set_catalog = PluginSetCatalog(**yaml_obj).initialize(plugin_set_catalog_path.parent)
269
+ self._plugin_set_catalogs.append(plugin_set_catalog)
270
+ at_least_one = True
271
+ for plugin_set_file in plugin_set_catalog.get_plugin_set_files():
272
+ try:
273
+ self.load_plugin_sets(plugin_set_catalog_path.joinpath(plugin_set_file))
274
+ except ValueError as ve:
275
+ errs.append(ve)
276
+ except ExceptionGroup as eg:
277
+ errs.extend(eg.exceptions)
278
+ except ValidationError as ve:
279
+ errs.append(ve)
280
+ if errs:
281
+ raise ExceptionGroup(f'Errors while loading plugin set catalogs from: {plugin_set_catalog_path!s}', errs)
282
+ if not at_least_one:
283
+ raise ValueError(f'No plugin set catalogs found in: {plugin_set_catalog_path!s}')
284
+ return self
285
+
286
+ def load_plugin_sets(self,
287
+ plugin_set_path_or_str: PathOrStr) -> Turtles:
288
+ """
289
+ Processes the given YAML file, loading all plugin set definitions it
290
+ contains, ignoring other YAML objects.
291
+
292
+ :param plugin_set_path_or_str: A file path (or string).
293
+ :type plugin_set_path_or_str: PathOrStr
294
+ :return: This Turtles object (for chaining).
295
+ :rtype: Turtles
296
+ :raises ExceptionGroup: If one or more errors occur while loading plugin
297
+ set definitions.
298
+ :raises ValueError: If the given file has already been processed or if
299
+ it contains no plugin set definitions.
300
+ """
301
+ plugin_set_path = path(plugin_set_path_or_str)
302
+ if plugin_set_path in map(lambda ps: ps.get_root(), self._plugin_sets):
303
+ raise ValueError(f'Plugin sets already loaded from: {plugin_set_path!s}')
304
+ errs, at_least_one = [], False
305
+ with plugin_set_path.open('r') as fps:
306
+ for yaml_obj in yaml.safe_load_all(fps):
307
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSetKind.__args__:
308
+ try:
309
+ plugin_set = PluginSet(**yaml_obj).initialize(plugin_set_path.parent)
310
+ self._plugin_sets.append(plugin_set)
311
+ at_least_one = True
312
+ except ValidationError as ve:
313
+ errs.append(ve)
314
+ if errs:
315
+ raise ExceptionGroup(f'Errors while loading plugin sets from: {plugin_set_path!s}', errs)
316
+ if not at_least_one:
317
+ raise ValueError(f'No plugin sets found in: {plugin_set_path!s}')
318
+ return self
319
+
320
+ def load_plugin_signing_credentials(self,
321
+ plugin_signing_credentials_path_or_str: PathOrStr) -> Turtles:
322
+ """
323
+ Processes the given YAML file, loading all plugin set definitions it
324
+ contains in search of exactly one, ignoring YAML objects of other kinds.
325
+
326
+ :param plugin_signing_credentials_path_or_str: A file path (or string).
327
+ :type plugin_signing_credentials_path_or_str: PathOrStr
328
+ :return: This Turtles object (for chaining).
329
+ :rtype: Turtles
330
+ :raises ExceptionGroup: If one or more errors occur while loading plugin
331
+ signing credentials definitions.
332
+ :raises ValueError: If the given file has already been processed, if it
333
+ contains no plugin signing credentials definitions,
334
+ or if it contains more than one plugin signing
335
+ credentials definitions.
336
+ """
337
+ plugin_signing_credentials_path = path(plugin_signing_credentials_path_or_str)
338
+ if self._plugin_signing_credentials:
339
+ raise ValueError(f'Plugin signing credentials already loaded from: {self._plugin_signing_credentials.get_root()!s}')
340
+ found = 0
341
+ with plugin_signing_credentials_path.open('r') as fpsc:
342
+ for yaml_obj in yaml.safe_load_all(fpsc):
343
+ if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSigningCredentialsKind.__args__:
344
+ found = found + 1
345
+ if not self._plugin_signing_credentials:
346
+ try:
347
+ plugin_signing_credentials = PluginSigningCredentials(**yaml_obj).initialize(plugin_signing_credentials_path.parent)
348
+ self._plugin_signing_credentials = plugin_signing_credentials
349
+ except ValidationError as ve:
350
+ raise ExceptionGroup(f'Errors while loading plugin signing credentials from: {plugin_signing_credentials_path!s}', [ve])
351
+ if found == 0:
352
+ raise ValueError(f'No plugin signing credentials found in: {plugin_signing_credentials_path!s}')
353
+ if found > 1:
354
+ raise ValueError(f'Multiple plugin signing credentials found in: {plugin_signing_credentials_path!s}')
355
+ return self
356
+
357
+ def release_plugin(self,
358
+ plugin_id_or_plugin_ids: Union[PluginIdentifier, list[PluginIdentifier]],
359
+ layer_id_or_layer_ids: Union[PluginRegistryLayerIdentifier, list[PluginRegistryLayerIdentifier]],
360
+ interactive: bool=False) -> dict[PluginIdentifier, list[DeployPluginResult]]:
361
+ """
362
+ Releases (builds then deploys) zero or more plugins.
363
+
364
+ :param plugin_id_or_plugin_ids: Either one plugin identifier, or a list
365
+ of plugin identifiers.
366
+ :type plugin_id_or_plugin_ids: Union[PluginIdentifier, list[PluginIdentifier]]
367
+ :param layer_id_or_layer_ids: Either one plugin registry layer
368
+ identifier or a list of plugin registry
369
+ layer identifiers.
370
+ :type layer_id_or_layer_ids: Union[PluginRegistryLayerIdentifier, list[PluginRegistryLayerIdentifier]]
371
+ :param interactive: Whether interactive prompts are allowed (default
372
+ False).
373
+ :type interactive: bool
374
+ :return: A mapping from plugin identifier to plugin deployment result;
375
+ if no plugins were given, the result is an empty mapping.
376
+ :rtype: dict[PluginIdentifier, list[DeployPluginResult]]
377
+ :raises Exception: If a given plugin is not found in any plugin set or
378
+ is not declared in any loaded plugin registry.
379
+ """
380
+ plugin_ids: list[PluginIdentifier] = plugin_id_or_plugin_ids if isinstance(plugin_id_or_plugin_ids, list) else [plugin_id_or_plugin_ids]
381
+ layer_ids: list[PluginRegistryLayerIdentifier] = layer_id_or_layer_ids if isinstance(layer_id_or_layer_ids, list) else [layer_id_or_layer_ids]
128
382
  # ... plugin_id -> (set_id, jar_path, plugin)
129
383
  ret1 = self.build_plugin(plugin_ids)
130
384
  jar_paths = [jar_path for set_id, jar_path, plugin in ret1.values()]
@@ -134,54 +388,211 @@ class TurtlesApp(object):
134
388
  interactive=interactive)
135
389
  return {plugin_id: val for (jar_path, plugin_id), val in ret2.items()}
136
390
 
137
- def select_plugin_registry_catalog(self, preselected=None):
138
- return TurtlesApp._select_file(TurtlesApp.PLUGIN_REGISTRY_CATALOG, preselected)
139
-
140
- def select_plugin_set_catalog(self, preselected=None):
141
- return TurtlesApp._select_file(TurtlesApp.PLUGIN_SET_CATALOG, preselected)
142
-
143
- def select_plugin_signing_credentials(self, preselected=None):
144
- return TurtlesApp._select_file(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS, preselected)
145
-
146
- def set_password(self, pw):
147
- self._password = pw if callable(pw) else lambda x: pw
148
-
149
- # Returns (set_id, jar_path, plugin)
150
- def _build_one_plugin(self, plugin_id):
391
+ def set_plugin_signing_password(self,
392
+ callable_or_password: Union[str, Callable[[], str]]) -> None:
393
+ """
394
+ Sets the plugin signing password callable.
395
+
396
+ :param callable_or_password: A callable returning a string (or simply a
397
+ string).
398
+ :type callable_or_password: Union[str, Callable[[], str]]
399
+ """
400
+ self._plugin_signing_password_callable = callable_or_password if callable(callable_or_password) else lambda: callable_or_password
401
+
402
+ def _build_one_plugin(self,
403
+ plugin_id: PluginIdentifier) -> BuildPluginResult:
404
+ """
405
+ Builds one plugin.
406
+
407
+ :param plugin_id: A plugin identifier.
408
+ :type plugin_id: PluginIdentifier
409
+ :return: A plugin build result object; if the plugin set returned None,
410
+ the second and third items (index 1 and 2) are None.
411
+ :rtype: BuildPluginResult
412
+ :raises Exception: If the given plugin identifier is not found in any
413
+ loaded plugin set.
414
+ """
151
415
  for plugin_set in self._plugin_sets:
152
416
  if plugin_set.has_plugin(plugin_id):
153
- return (plugin_set.get_id(),
154
- *plugin_set.build_plugin(plugin_id,
155
- self._get_plugin_signing_keystore(),
156
- self._get_plugin_signing_alias(),
157
- self._get_plugin_signing_password()))
158
- raise Exception(f'{plugin_id}: not found in any plugin set')
159
-
160
- # Returns list of (registry_id, layer_id, dst_path, plugin)
161
- def _deploy_one_plugin(self, src_jar, plugin_id, layer_ids, interactive=False):
417
+ bp = plugin_set.build_plugin(plugin_id,
418
+ self._get_plugin_signing_keystore(),
419
+ self._get_plugin_signing_alias(),
420
+ self._get_plugin_signing_password())
421
+ return plugin_set.get_id(), bp[0] if bp else None, bp[1] if bp else None
422
+ raise Exception(f'plugin identifier not found in any loaded plugin set: {plugin_id}')
423
+
424
+ def _deploy_one_plugin(self,
425
+ src_jar: Path,
426
+ plugin_id: PluginIdentifier,
427
+ layer_ids: list[PluginRegistryLayerIdentifier],
428
+ interactive: bool=False) -> list[DeployPluginResult]:
429
+ """
430
+ Deploys a single plugin to the provided plugin registry layers of all
431
+ loaded plugin registries that declare the given plugin.
432
+
433
+ :param src_jar: File path of the signed JAR.
434
+ :type src_jar: Path
435
+ :param plugin_id: The corresponding plugin identifier.
436
+ :type plugin_id: PluginIdentifier
437
+ :param layer_ids: A list of plugin layer identifiers.
438
+ :type layer_ids: list[PluginRegistryLayerIdentifier]
439
+ :param interactive: Whether interactive prompts are allowed (default
440
+ False).
441
+ :type interactive: bool
442
+ :return: A non-empty list of plugin deployment results; if for any, the
443
+ plugin registry returned None, the third and fourth items
444
+ (index 2 and 3) are None.
445
+ :rtype: list[DeployPluginResult]
446
+ :raises Exception: If the given plugin identifier is not declared in any
447
+ loaded plugin registry.
448
+ """
162
449
  ret = list()
163
450
  for plugin_registry in self._plugin_registries:
164
451
  if plugin_registry.has_plugin(plugin_id):
165
452
  for layer_id in layer_ids:
166
- layer = plugin_registry.get_layer(layer_id)
167
- if layer is not None:
453
+ if layer := plugin_registry.get_layer(layer_id):
454
+ dp = layer.deploy_plugin(plugin_id,
455
+ src_jar,
456
+ interactive=interactive)
168
457
  ret.append((plugin_registry.get_id(),
169
458
  layer.get_id(),
170
- *layer.deploy_plugin(plugin_id,
171
- src_jar,
172
- interactive=interactive)))
459
+ dp[0] if dp else None,
460
+ dp[1] if dp else None))
173
461
  if len(ret) == 0:
174
462
  raise Exception(f'{src_jar}: {plugin_id} not declared in any plugin registry')
175
463
  return ret
176
464
 
177
- def _get_password(self):
178
- return self._password() if self._password else None
465
+ def _get_plugin_signing_alias(self) -> str:
466
+ """
467
+ Returns the plugin signing alias from the loaded plugin signing
468
+ credentials.
469
+
470
+ :return: The plugin signing alias.
471
+ :rtype: str
472
+ """
473
+ return self._plugin_signing_credentials.get_plugin_signing_alias()
474
+
475
+ def _get_plugin_signing_keystore(self) -> Path:
476
+ """
477
+ Returns the plugin signing keystore file path from the loaded plugin
478
+ signing credentials.
479
+
480
+ :return: The plugin signing keystore file path.
481
+ :rtype: Path
482
+ """
483
+ return self._plugin_signing_credentials.get_plugin_signing_keystore()
484
+
485
+ def _get_plugin_signing_password(self) -> Optional[Callable[[], str]]:
486
+ """
487
+ Returns the plugin signing password.
488
+
489
+ :return: The plugin signing password callable.
490
+ :rtype: Optional[Callable[[], str]]
491
+ """
492
+ return self._plugin_signing_password_callable
493
+
494
+ @staticmethod
495
+ def default_plugin_registry_catalog_choices() -> tuple[Path, ...]:
496
+ """
497
+ Returns the tuple of default plugin registry catalog file choices.
498
+
499
+ See ``CONFIG_DIRS`` and ``PLUGIN_REGISTRY_CATALOG``.
500
+
501
+ :return: A tuple of default plugin registry catalog file choices.
502
+ :rtype: tuple[Path, ...]
503
+ """
504
+ return Turtles._default_files(Turtles.PLUGIN_REGISTRY_CATALOG)
505
+
506
+ @staticmethod
507
+ def default_plugin_set_catalog_choices() -> tuple[Path, ...]:
508
+ """
509
+ Returns the tuple of default plugin set catalog file choices.
510
+
511
+ See ``CONFIG_DIRS`` and ``PLUGIN_SET_CATALOG``.
512
+
513
+ :return: A tuple of default plugin set catalog file choices.
514
+ :rtype: tuple[Path, ...]
515
+ """
516
+ return Turtles._default_files(Turtles.PLUGIN_SET_CATALOG)
517
+
518
+ @staticmethod
519
+ def default_plugin_signing_credentials_choices() -> tuple[Path, ...]:
520
+ """
521
+ Returns the tuple of default plugin signing credentials file choices.
522
+
523
+ See ``CONFIG_DIRS`` and ``PLUGIN_SIGNING_CREDENTIALS``.
524
+
525
+ :return: A tuple of default plugin signing credentials file choices.
526
+ :rtype: tuple[Path, ...]
527
+ """
528
+ return Turtles._default_files(Turtles.PLUGIN_SIGNING_CREDENTIALS)
529
+
530
+ @staticmethod
531
+ def select_default_plugin_registry_catalog() -> Optional[Path]:
532
+ """
533
+ Of the default plugin registry catalog file choices, select the first
534
+ one that exists.
179
535
 
180
- def _get_plugin_signing_alias(self):
181
- return self._plugin_signing_credentials['plugin-signing-alias']
536
+ See ``default_plugin_registry_catalog_choices`` and ``_select_file``.
182
537
 
183
- def _get_plugin_signing_keystore(self):
184
- return self._plugin_signing_credentials['plugin-signing-keystore']
538
+ :return: The first of the default plugin registry catalog file choices
539
+ that exists, or None if none do.
540
+ :rtype: Optional[Path]
541
+ """
542
+ return Turtles._select_file(Turtles.default_plugin_registry_catalog_choices())
185
543
 
186
- def _get_plugin_signing_password(self):
187
- return self._get_password()
544
+ @staticmethod
545
+ def select_default_plugin_set_catalog() -> Optional[Path]:
546
+ """
547
+ Of the default plugin set catalog file choices, select the first one
548
+ that exists.
549
+
550
+ See ``default_plugin_registry_set_choices`` and ``_select_file``.
551
+
552
+ :return: The first of the default plugin set catalog file choices that
553
+ exists, or None if none do.
554
+ :rtype: Optional[Path]
555
+ """
556
+ return Turtles._select_file(Turtles.default_plugin_set_catalog_choices())
557
+
558
+ @staticmethod
559
+ def select_default_plugin_signing_credentials() -> Optional[Path]:
560
+ """
561
+ Of the default plugin signing credentials file choices, select the first
562
+ one that exists.
563
+
564
+ See ``default_plugin_registry_set_choices`` and ``_select_file``.
565
+
566
+ :return: The first of the default plugin signing credentials file
567
+ choices that exists, or None if none do.
568
+ :rtype: Optional[Path]
569
+ """
570
+ return Turtles._select_file(Turtles.default_plugin_signing_credentials_choices())
571
+
572
+ @staticmethod
573
+ def _default_files(file_str) -> tuple[Path, ...]:
574
+ """
575
+ Given a file base name, returns a tuple of this file in the various
576
+ Turtles configuration directories (``CONFIG_DIRS``).
577
+
578
+ :param file_str: A file base name.
579
+ :type file_str: str
580
+ :return: The file in the various Turtles configuration directories.
581
+ :rtype: tuple[Path, ...]
582
+ """
583
+ return tuple(dir_path.joinpath(file_str) for dir_path in Turtles.CONFIG_DIRS)
584
+
585
+ @staticmethod
586
+ def _select_file(choices: Iterable[Path]) -> Optional[Path]:
587
+ """
588
+ Of the given files, returns the first one that exists.
589
+
590
+ :param choices: An iterable of file paths.
591
+ :type choices: Iterable[Path]
592
+ :return: The first choice that exists, or None if none do.
593
+ :rtype: Optional[Path]
594
+ """
595
+ for p in choices:
596
+ if p.is_file():
597
+ return p
598
+ return None