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.
- lockss/turtles/__init__.py +1 -1
- lockss/turtles/app.py +127 -74
- lockss/turtles/cli.py +82 -42
- lockss/turtles/plugin.py +18 -13
- lockss/turtles/plugin_registry.py +140 -165
- lockss/turtles/plugin_set.py +102 -137
- lockss/turtles/util.py +28 -17
- {lockss_turtles-0.6.0.dev2.dist-info → lockss_turtles-0.6.0.dev4.dist-info}/METADATA +2 -3
- lockss_turtles-0.6.0.dev4.dist-info/RECORD +15 -0
- unittest/lockss/turtles/__init__.py +65 -0
- unittest/lockss/turtles/test_plugin_set.py +62 -0
- lockss/turtles/resources/__init__.py +0 -29
- lockss/turtles/resources/plugin-registry-catalog-schema.json +0 -27
- lockss/turtles/resources/plugin-registry-schema.json +0 -115
- lockss/turtles/resources/plugin-set-catalog-schema.json +0 -27
- lockss/turtles/resources/plugin-set-schema.json +0 -92
- lockss/turtles/resources/plugin-signing-credentials-schema.json +0 -27
- lockss_turtles-0.6.0.dev2.dist-info/RECORD +0 -19
- {lockss_turtles-0.6.0.dev2.dist-info → lockss_turtles-0.6.0.dev4.dist-info}/LICENSE +0 -0
- {lockss_turtles-0.6.0.dev2.dist-info → lockss_turtles-0.6.0.dev4.dist-info}/WHEEL +0 -0
- {lockss_turtles-0.6.0.dev2.dist-info → lockss_turtles-0.6.0.dev4.dist-info}/entry_points.txt +0 -0
lockss/turtles/__init__.py
CHANGED
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
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
40
|
-
from .
|
|
41
|
-
from .
|
|
42
|
-
from .
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
USR_CONFIG_DIR: ClassVar[Path] = Path('/usr/local/share', CONFIG_DIR_NAME)
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
|
|
59
75
|
|
|
60
|
-
|
|
76
|
+
CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, USR_CONFIG_DIR, ETC_CONFIG_DIR)
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
|
|
63
79
|
|
|
64
|
-
|
|
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:
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
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:
|
|
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:
|
|
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,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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) ->
|
|
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:
|
|
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
|
|
204
|
+
return self._plugin_signing_credentials.get_plugin_signing_alias()
|
|
158
205
|
|
|
159
|
-
def _get_plugin_signing_keystore(self) ->
|
|
160
|
-
return self._plugin_signing_credentials
|
|
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
|
|
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
|
|
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
|
|
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
|
|
179
|
-
return
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
68
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
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[
|
|
82
|
-
plugin_identifiers: Optional[
|
|
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) ->
|
|
89
|
-
ret = [*self.plugin_identifier[
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
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[
|
|
100
|
-
plugin_jars: Optional[
|
|
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[
|
|
108
|
-
if len(ret)
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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,
|
|
249
|
-
if
|
|
250
|
-
_p =
|
|
251
|
-
elif
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
41
|
-
from typing import Any,
|
|
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
|
|
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) ->
|
|
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[
|
|
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[
|
|
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:
|
|
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:
|
|
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) ->
|
|
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:
|
|
116
|
+
def id_from_jar(jar_path: PathOrStr) -> PluginIdentifier:
|
|
112
117
|
jar_path = path(jar_path) # in case it's a string
|
|
113
|
-
manifest =
|
|
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:
|
|
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:
|
|
133
|
+
def id_to_file(plugin_id: PluginIdentifier) -> Path:
|
|
129
134
|
return Path(f'{plugin_id.replace(".", "/")}.xml')
|