lockss-turtles 0.6.0.dev23__tar.gz → 0.6.0.dev25__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 (21) hide show
  1. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/CHANGELOG.rst +2 -0
  2. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/PKG-INFO +5 -4
  3. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/README.rst +1 -1
  4. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/pyproject.toml +2 -2
  5. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/__init__.py +1 -1
  6. lockss_turtles-0.6.0.dev25/src/lockss/turtles/app.py +598 -0
  7. lockss_turtles-0.6.0.dev25/src/lockss/turtles/cli.py +653 -0
  8. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/plugin.py +2 -3
  9. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/plugin_registry.py +5 -6
  10. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/plugin_set.py +24 -24
  11. lockss_turtles-0.6.0.dev25/src/lockss/turtles/plugin_signing_credentials.py +80 -0
  12. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/util.py +2 -3
  13. lockss_turtles-0.6.0.dev25/tests/unittest/lockss/turtles/__init__.py +106 -0
  14. lockss_turtles-0.6.0.dev25/tests/unittest/lockss/turtles/test_plugin_registry.py +411 -0
  15. lockss_turtles-0.6.0.dev25/tests/unittest/lockss/turtles/test_plugin_set.py +272 -0
  16. lockss_turtles-0.6.0.dev23/src/lockss/turtles/app.py +0 -305
  17. lockss_turtles-0.6.0.dev23/src/lockss/turtles/cli.py +0 -382
  18. lockss_turtles-0.6.0.dev23/tests/unittest/lockss/turtles/__init__.py +0 -65
  19. lockss_turtles-0.6.0.dev23/tests/unittest/lockss/turtles/test_plugin_set.py +0 -77
  20. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/LICENSE +0 -0
  21. {lockss_turtles-0.6.0.dev23 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/__main__.py +0 -0
@@ -77,6 +77,8 @@ Released: ?
77
77
 
78
78
  * Options that read YAML files no longer expect each file to contain a single YAML configuration object, which must be of the kind they target; they now read all YAML configuration objects in each file, loading all the ones with the right kind and ignoring all the others with the other kinds. This applies to existing options ``--plugin-registry-catalog``/``-R`` and ``--plugin-set-catalog``/``-S``, and new options ``--plugin-registry``/``-r`` and ``--plugin-set``/``-s``.
79
79
 
80
+ * In YAML files, values that are paths can now all be either relative with respect to the enclosing file or absolute.
81
+
80
82
  * Now using Pydantic for configuration objects instead of maintaining JSON Schema instances.
81
83
 
82
84
  * Now using type hinting throughout.
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: lockss-turtles
3
- Version: 0.6.0.dev23
3
+ Version: 0.6.0.dev25
4
4
  Summary: Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries
5
5
  License: BSD-3-Clause
6
+ License-File: LICENSE
6
7
  Author: Thib Guicherd-Callin
7
8
  Author-email: thib@cs.stanford.edu
8
9
  Maintainer: Thib Guicherd-Callin
@@ -20,7 +21,7 @@ Classifier: Topic :: System :: Archiving
20
21
  Classifier: Topic :: Utilities
21
22
  Requires-Dist: exceptiongroup (>=1.3.0,<1.4.0)
22
23
  Requires-Dist: java-manifest (>=1.1.0,<1.2.0)
23
- Requires-Dist: lockss-pybasic (>=0.1.0,<0.2.0)
24
+ Requires-Dist: lockss-pybasic (>=0.1.1,<0.2.0)
24
25
  Requires-Dist: pydantic (>=2.11.0,<2.12.0)
25
26
  Requires-Dist: pyyaml (>=6.0.0,<6.1.0)
26
27
  Requires-Dist: xdg (>=6.0.0,<6.1.0)
@@ -34,7 +35,7 @@ Description-Content-Type: text/x-rst
34
35
  Turtles
35
36
  =======
36
37
 
37
- .. |RELEASE| replace:: 0.6.0-dev23 NOT YET RELEASED
38
+ .. |RELEASE| replace:: 0.6.0-dev25 NOT YET RELEASED
38
39
  .. |RELEASE_DATE| replace:: NOT YET RELEASED
39
40
  .. |TURTLES| replace:: **Turtles**
40
41
 
@@ -2,7 +2,7 @@
2
2
  Turtles
3
3
  =======
4
4
 
5
- .. |RELEASE| replace:: 0.6.0-dev23 NOT YET RELEASED
5
+ .. |RELEASE| replace:: 0.6.0-dev25 NOT YET RELEASED
6
6
  .. |RELEASE_DATE| replace:: NOT YET RELEASED
7
7
  .. |TURTLES| replace:: **Turtles**
8
8
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  [project]
30
30
  name = "lockss-turtles"
31
- version = "0.6.0-dev23" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
31
+ version = "0.6.0-dev25" # 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"
@@ -42,7 +42,7 @@ maintainers = [
42
42
  dependencies = [
43
43
  "exceptiongroup (>=1.3.0,<1.4.0)",
44
44
  "java-manifest (>=1.1.0,<1.2.0)",
45
- "lockss-pybasic (>=0.1.0,<0.2.0)",
45
+ "lockss-pybasic (>=0.1.1,<0.2.0)",
46
46
  "pyyaml (>=6.0.0,<6.1.0)",
47
47
  "xdg (>=6.0.0,<6.1.0)",
48
48
  "pydantic (>=2.11.0,<2.12.0)",
@@ -6,7 +6,7 @@ registries.
6
6
  """
7
7
 
8
8
  #: This package's version.
9
- __version__ = '0.6.0-dev23'
9
+ __version__ = '0.6.0-dev25'
10
10
 
11
11
  #: This package's copyright.
12
12
  __copyright__ = '''
@@ -0,0 +1,598 @@
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
+ """
32
+ Module to represent Turtles operations.
33
+ """
34
+
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
36
+ from __future__ import annotations
37
+
38
+ # Remove in Python 3.11; see https://docs.python.org/3.11/library/exceptions.html#exception-groups
39
+ from exceptiongroup import ExceptionGroup
40
+
41
+ from collections.abc import Callable, Iterable
42
+ from pathlib import Path
43
+ from typing import ClassVar, Optional, Union
44
+
45
+ from lockss.pybasic.fileutil import path
46
+ from pydantic import ValidationError
47
+ import xdg
48
+ import yaml
49
+
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
55
+
56
+
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]]
62
+
63
+
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]]
71
+
72
+
73
+ class Turtles(object):
74
+ """
75
+ A Turtles command object, which can be used to execute Turtles operations.
76
+ """
77
+
78
+ #: The name of a Turtles configuration directory.
79
+ CONFIG_DIR_NAME: ClassVar[str] = 'lockss-turtles'
80
+
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)
84
+
85
+ #: The Turtles configuration directory under ``/etc``.
86
+ ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
87
+
88
+ #: The Turtles configuration directory under ``/usr/local/share``.
89
+ USR_CONFIG_DIR: ClassVar[Path] = Path('/usr/local/share', CONFIG_DIR_NAME)
90
+
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)
94
+
95
+ #: The default plugin registry catalog file name.
96
+ PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
97
+
98
+ #: The default plugin set catalog file name.
99
+ PLUGIN_SET_CATALOG: ClassVar[str] = 'plugin-set-catalog.yaml'
100
+
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}
132
+
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?
161
+ return {(src_path, plugin_id): self._deploy_one_plugin(src_path,
162
+ plugin_id,
163
+ layer_ids,
164
+ interactive=interactive) for src_path, plugin_id in zip(src_paths, plugin_ids)}
165
+
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]
382
+ # ... plugin_id -> (set_id, jar_path, plugin)
383
+ ret1 = self.build_plugin(plugin_ids)
384
+ jar_paths = [jar_path for set_id, jar_path, plugin in ret1.values()]
385
+ # ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
386
+ ret2 = self.deploy_plugin(jar_paths,
387
+ layer_ids,
388
+ interactive=interactive)
389
+ return {plugin_id: val for (jar_path, plugin_id), val in ret2.items()}
390
+
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
+ """
415
+ for plugin_set in self._plugin_sets:
416
+ if plugin_set.has_plugin(plugin_id):
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
+ """
449
+ ret = list()
450
+ for plugin_registry in self._plugin_registries:
451
+ if plugin_registry.has_plugin(plugin_id):
452
+ for layer_id in layer_ids:
453
+ if layer := plugin_registry.get_layer(layer_id):
454
+ dp = layer.deploy_plugin(plugin_id,
455
+ src_jar,
456
+ interactive=interactive)
457
+ ret.append((plugin_registry.get_id(),
458
+ layer.get_id(),
459
+ dp[0] if dp else None,
460
+ dp[1] if dp else None))
461
+ if len(ret) == 0:
462
+ raise Exception(f'{src_jar}: {plugin_id} not declared in any plugin registry')
463
+ return ret
464
+
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.
535
+
536
+ See ``default_plugin_registry_catalog_choices`` and ``_select_file``.
537
+
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())
543
+
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