lockss-turtles 0.6.0.dev1__py3-none-any.whl → 0.6.0.dev3__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 -76
- lockss/turtles/cli.py +83 -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.dev1.dist-info → lockss_turtles-0.6.0.dev3.dist-info}/METADATA +5 -3
- lockss_turtles-0.6.0.dev3.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.dev1.dist-info/RECORD +0 -19
- {lockss_turtles-0.6.0.dev1.dist-info → lockss_turtles-0.6.0.dev3.dist-info}/LICENSE +0 -0
- {lockss_turtles-0.6.0.dev1.dist-info → lockss_turtles-0.6.0.dev3.dist-info}/WHEEL +0 -0
- {lockss_turtles-0.6.0.dev1.dist-info → lockss_turtles-0.6.0.dev3.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
|
-
|
|
44
|
+
from .plugin import Plugin, PluginIdentifier
|
|
45
|
+
from .plugin_registry import PluginRegistry, PluginRegistryCatalog, PluginRegistryCatalogKind, PluginRegistryKind, PluginRegistryLayerIdentifier
|
|
46
|
+
from .plugin_set import PluginSet, PluginSetCatalog, PluginSetCatalogKind, PluginSetKind
|
|
47
|
+
from .util import BaseModelWithRoot, PathOrStr
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
PluginSigningCredentialsKind = Literal['PluginSigningCredentials']
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PluginSigningCredentials(BaseModelWithRoot):
|
|
55
|
+
kind: PluginSigningCredentialsKind = Field(description="This object's kind")
|
|
56
|
+
plugin_signing_keystore: str = Field(title='Plugin Signing Keystore', description='A path to the plugin signing keystore')
|
|
57
|
+
plugin_signing_alias: str = Field(title='Plugin Signing Alias', description='The plugin signing alias to use')
|
|
58
|
+
|
|
59
|
+
def get_plugin_signing_alias(self) -> str:
|
|
60
|
+
return self.plugin_signing_alias
|
|
61
|
+
|
|
62
|
+
def get_plugin_signing_keystore(self) -> Path:
|
|
63
|
+
return self.get_root().joinpath(self.plugin_signing_keystore)
|
|
44
64
|
|
|
45
65
|
|
|
46
66
|
class TurtlesApp(object):
|
|
47
67
|
|
|
48
|
-
|
|
68
|
+
CONFIG_DIR_NAME: ClassVar[str] = 'lockss-turtles'
|
|
49
69
|
|
|
50
|
-
|
|
70
|
+
XDG_CONFIG_DIR: ClassVar[Path] = Path(xdg.xdg_config_home(), CONFIG_DIR_NAME)
|
|
51
71
|
|
|
52
|
-
|
|
72
|
+
USR_CONFIG_DIR: ClassVar[Path] = Path('/usr/local/share', CONFIG_DIR_NAME)
|
|
53
73
|
|
|
54
|
-
|
|
74
|
+
ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
|
|
55
75
|
|
|
56
|
-
|
|
76
|
+
CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, USR_CONFIG_DIR, ETC_CONFIG_DIR)
|
|
57
77
|
|
|
58
|
-
|
|
78
|
+
PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
|
|
59
79
|
|
|
60
|
-
|
|
80
|
+
PLUGIN_SET_CATALOG: ClassVar[str] = 'plugin-set-catalog.yaml'
|
|
61
81
|
|
|
62
|
-
|
|
82
|
+
PLUGIN_SIGNING_CREDENTIALS: ClassVar[str] = 'plugin-signing-credentials.yaml'
|
|
63
83
|
|
|
64
84
|
def __init__(self) -> None:
|
|
65
85
|
super().__init__()
|
|
66
86
|
self._password: Optional[Callable[[], str]] = None
|
|
67
|
-
self._plugin_registries:
|
|
68
|
-
self.
|
|
69
|
-
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
|
|
70
92
|
|
|
71
|
-
def build_plugin(self, plugin_ids:
|
|
93
|
+
def build_plugin(self, plugin_ids: list[PluginIdentifier]) -> dict[str, tuple[str, Path, Plugin]]:
|
|
72
94
|
return {plugin_id: self._build_one_plugin(plugin_id) for plugin_id in plugin_ids}
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
def deploy_plugin(self, src_paths: List[Path], layer_ids: List[str], interactive: bool=False) -> Dict[Tuple[Path, str], List[Tuple[str, str, Optional[Path], Optional[Plugin]]]]:
|
|
96
|
+
def deploy_plugin(self, src_paths: list[Path], layer_ids: list[PluginRegistryLayerIdentifier], interactive: bool=False) -> dict[tuple[Path, str], list[tuple[str, str, Optional[Path], Optional[Plugin]]]]:
|
|
76
97
|
plugin_ids = [Plugin.id_from_jar(src_path) for src_path in src_paths]
|
|
77
98
|
return {(src_path, plugin_id): self._deploy_one_plugin(src_path,
|
|
78
99
|
plugin_id,
|
|
79
100
|
layer_ids,
|
|
80
101
|
interactive=interactive) for src_path, plugin_id in zip(src_paths, plugin_ids)}
|
|
81
102
|
|
|
82
|
-
def load_plugin_registries(self,
|
|
83
|
-
|
|
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,20 +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
|
-
|
|
126
|
-
def _build_one_plugin(self, plugin_id: str) -> Tuple[str, Optional[Path], Optional[Plugin]]:
|
|
172
|
+
def _build_one_plugin(self, plugin_id: str) -> tuple[str, Optional[Path], Optional[Plugin]]:
|
|
127
173
|
for plugin_set in self._plugin_sets:
|
|
128
174
|
if plugin_set.has_plugin(plugin_id):
|
|
129
175
|
bp = plugin_set.build_plugin(plugin_id,
|
|
@@ -133,8 +179,7 @@ class TurtlesApp(object):
|
|
|
133
179
|
return plugin_set.get_id(), bp[0] if bp else None, bp[1] if bp else None
|
|
134
180
|
raise Exception(f'{plugin_id}: not found in any plugin set')
|
|
135
181
|
|
|
136
|
-
|
|
137
|
-
def _deploy_one_plugin(self, src_jar: Path, plugin_id: str, layer_ids: List[str], interactive: bool=False) -> List[Tuple[str, str, Optional[Path], Optional[Plugin]]]:
|
|
182
|
+
def _deploy_one_plugin(self, src_jar: Path, plugin_id: PluginIdentifier, layer_ids: list[PluginRegistryLayerIdentifier], interactive: bool=False) -> list[tuple[str, str, Optional[Path], Optional[Plugin]]]:
|
|
138
183
|
ret = list()
|
|
139
184
|
for plugin_registry in self._plugin_registries:
|
|
140
185
|
if plugin_registry.has_plugin(plugin_id):
|
|
@@ -156,39 +201,45 @@ class TurtlesApp(object):
|
|
|
156
201
|
return self._password() if self._password else None
|
|
157
202
|
|
|
158
203
|
def _get_plugin_signing_alias(self) -> str:
|
|
159
|
-
return self._plugin_signing_credentials
|
|
204
|
+
return self._plugin_signing_credentials.get_plugin_signing_alias()
|
|
160
205
|
|
|
161
|
-
def _get_plugin_signing_keystore(self) ->
|
|
162
|
-
return self._plugin_signing_credentials
|
|
206
|
+
def _get_plugin_signing_keystore(self) -> Path:
|
|
207
|
+
return self._plugin_signing_credentials.get_plugin_signing_keystore()
|
|
163
208
|
|
|
164
209
|
def _get_plugin_signing_password(self) -> str:
|
|
165
210
|
return self._get_password()
|
|
166
211
|
|
|
167
212
|
@staticmethod
|
|
168
|
-
def
|
|
213
|
+
def default_plugin_registry_catalog_choices() -> tuple[Path, ...]:
|
|
169
214
|
return TurtlesApp._default_files(TurtlesApp.PLUGIN_REGISTRY_CATALOG)
|
|
170
215
|
|
|
171
216
|
@staticmethod
|
|
172
|
-
def
|
|
217
|
+
def default_plugin_set_catalog_choices() -> tuple[Path, ...]:
|
|
173
218
|
return TurtlesApp._default_files(TurtlesApp.PLUGIN_SET_CATALOG)
|
|
174
219
|
|
|
175
220
|
@staticmethod
|
|
176
|
-
def
|
|
221
|
+
def default_plugin_signing_credentials_choices() -> tuple[Path, ...]:
|
|
177
222
|
return TurtlesApp._default_files(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS)
|
|
178
223
|
|
|
179
224
|
@staticmethod
|
|
180
|
-
def
|
|
181
|
-
return
|
|
225
|
+
def select_default_plugin_registry_catalog() -> Optional[Path]:
|
|
226
|
+
return TurtlesApp._select_file(TurtlesApp.default_plugin_registry_catalog_choices())
|
|
182
227
|
|
|
183
228
|
@staticmethod
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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,105 @@ from lockss.pybasic.outpututil import OutputFormatOptions
|
|
|
41
41
|
from pydantic.v1 import BaseModel, Field, FilePath
|
|
42
42
|
from pydantic.v1.class_validators import validator
|
|
43
43
|
import tabulate
|
|
44
|
-
from typing import
|
|
44
|
+
from typing import Optional
|
|
45
45
|
|
|
46
|
+
from lockss.turtles.plugin_registry import PluginRegistryLayer
|
|
46
47
|
from . import __copyright__, __license__, __version__
|
|
47
48
|
from .app import TurtlesApp
|
|
49
|
+
from .plugin_registry import PluginRegistryLayerIdentifier
|
|
50
|
+
from .util import file_or
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
class PluginBuildingOptions(BaseModel):
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
plugin_set: Optional[list[FilePath]] = Field(aliases=['-s'], description=f'(plugin sets) add one or more plugin sets to the loaded plugin sets')
|
|
55
|
+
plugin_set_catalog: Optional[list[FilePath]] = Field(aliases=['-S'], description=f'(plugin sets) add one or more plugin set catalogs to the loaded plugin set catalogs; if no plugin set catalogs or plugin sets are specified, load {file_or(TurtlesApp.default_plugin_set_catalog_choices())}')
|
|
56
|
+
plugin_signing_credentials: Optional[FilePath] = Field(aliases=['-c'], description=f'(plugin signing credentials) load the plugin signing credentials from the given file, or if none, from {file_or(TurtlesApp.default_plugin_signing_credentials_choices())}')
|
|
53
57
|
plugin_signing_password: Optional[str] = Field(description='(plugin signing credentials) set the plugin signing password, or if none, prompt interactively')
|
|
54
58
|
|
|
59
|
+
def get_plugin_sets(self) -> list[Path]:
|
|
60
|
+
return [path(p) for p in self.plugin_set or []]
|
|
61
|
+
|
|
62
|
+
def get_plugin_set_catalogs(self) -> list[Path]:
|
|
63
|
+
if self.plugin_set or self.plugin_set_catalog:
|
|
64
|
+
return [path(p) for p in self.plugin_set_catalog or []]
|
|
65
|
+
if single := TurtlesApp.select_default_plugin_set_catalog():
|
|
66
|
+
return [single]
|
|
67
|
+
raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
|
|
68
|
+
|
|
69
|
+
def get_plugin_signing_credentials(self) -> Path:
|
|
70
|
+
if self.plugin_signing_credentials:
|
|
71
|
+
return path(self.plugin_signing_credentials)
|
|
72
|
+
if ret := TurtlesApp.select_default_plugin_signing_credentials():
|
|
73
|
+
return ret
|
|
74
|
+
raise FileNotFoundError(file_or(TurtlesApp.default_plugin_signing_credentials_choices()))
|
|
75
|
+
|
|
55
76
|
|
|
56
77
|
class PluginDeploymentOptions(BaseModel):
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
plugin_registry: Optional[list[FilePath]] = Field(aliases=['-r'], description=f'(plugin registry) add one or more plugin registries to the loaded plugin registries')
|
|
79
|
+
plugin_registry_catalog: Optional[list[FilePath]] = Field(aliases=['-R'], description=f'(plugin registry) add one or more plugin registry catalogs to the loaded plugin registry catalogs; if no plugin registry catalogs or plugin registries are specified, load {file_or(TurtlesApp.default_plugin_registry_catalog_choices())}')
|
|
80
|
+
plugin_registry_layer: Optional[list[str]] = Field(aliases=['-l'], description='(plugin registry layers) add one or more plugin registry layers to the set of plugin registry layers to process')
|
|
81
|
+
plugin_registry_layers: Optional[list[FilePath]] = Field(aliases=['-L'], description='(plugin registry layers) add the plugin registry layers listed in one or more files to the set of plugin registry layers to process')
|
|
60
82
|
testing: Optional[bool] = Field(False, aliases=['-t'], description='(plugin registry layers) synonym for --plugin-registry-layer testing (i.e. add "testing" to the list of plugin registry layers to process)')
|
|
61
83
|
production: Optional[bool] = Field(False, aliases=['-p'], description='(plugin registry layers) synonym for --plugin-registry-layer production (i.e. add "production" to the list of plugin registry layers to process)')
|
|
62
84
|
|
|
63
85
|
@validator('plugin_registry_layers', each_item=True, pre=True)
|
|
64
|
-
def _expand_each_plugin_registry_layers_path(cls, v: Path):
|
|
86
|
+
def _expand_each_plugin_registry_layers_path(cls, v: Path) -> Path:
|
|
65
87
|
return path(v)
|
|
66
88
|
|
|
67
|
-
def
|
|
68
|
-
|
|
89
|
+
def get_plugin_registries(self) -> list[Path]:
|
|
90
|
+
return [path(p) for p in self.plugin_registry or []]
|
|
91
|
+
|
|
92
|
+
def get_plugin_registry_catalogs(self) -> list[Path]:
|
|
93
|
+
if self.plugin_registry or self.plugin_registry_catalog:
|
|
94
|
+
return [path(p) for p in self.plugin_registry_catalog or []]
|
|
95
|
+
if single := TurtlesApp.select_default_plugin_registry_catalog():
|
|
96
|
+
return [single]
|
|
97
|
+
raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
|
|
98
|
+
|
|
99
|
+
def get_plugin_registry_layers(self) -> list[PluginRegistryLayerIdentifier]:
|
|
100
|
+
ret = [*(self.plugin_registry_layer or []), *[file_lines(file_path) for file_path in self.plugin_registry_layers or []]]
|
|
69
101
|
for layer in reversed(['testing', 'production']):
|
|
70
102
|
if getattr(self, layer, False):
|
|
71
103
|
ret.insert(0, layer)
|
|
72
|
-
if
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
if ret:
|
|
105
|
+
return ret
|
|
106
|
+
raise ValueError('Empty list of plugin registry layers')
|
|
75
107
|
|
|
76
108
|
|
|
77
109
|
class PluginIdentifierOptions(BaseModel):
|
|
78
110
|
"""
|
|
79
111
|
The --identifier/-i and --identifiers/-I options.
|
|
80
112
|
"""
|
|
81
|
-
plugin_identifier: Optional[
|
|
82
|
-
plugin_identifiers: Optional[
|
|
113
|
+
plugin_identifier: Optional[list[str]] = Field(aliases=['-i'], description='(plugin identifiers) add one or more plugin identifiers to the set of plugin identifiers to process')
|
|
114
|
+
plugin_identifiers: Optional[list[FilePath]] = Field(aliases=['-I'], description='(plugin identifiers) add the plugin identifiers listed in one or more files to the set of plugin identifiers to process')
|
|
83
115
|
|
|
84
116
|
@validator('plugin_identifiers', each_item=True, pre=True)
|
|
85
|
-
def _expand_each_plugin_identifiers_path(cls, v: Path):
|
|
117
|
+
def _expand_each_plugin_identifiers_path(cls, v: Path) -> Path:
|
|
86
118
|
return path(v)
|
|
87
119
|
|
|
88
|
-
def get_plugin_identifiers(self) ->
|
|
89
|
-
ret = [*self.plugin_identifier[
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
def get_plugin_identifiers(self) -> list[str]:
|
|
121
|
+
ret = [*(self.plugin_identifier or []), *[file_lines(file_path) for file_path in self.plugin_identifiers or []]]
|
|
122
|
+
if ret:
|
|
123
|
+
return ret
|
|
124
|
+
raise ValueError('Empty list of plugin identifiers')
|
|
93
125
|
|
|
94
126
|
|
|
95
127
|
class PluginJarOptions(BaseModel):
|
|
96
128
|
"""
|
|
97
129
|
The --plugin-jar/-j and --plugin-jars/-J options.
|
|
98
130
|
"""
|
|
99
|
-
plugin_jar: Optional[
|
|
100
|
-
plugin_jars: Optional[
|
|
131
|
+
plugin_jar: Optional[list[FilePath]] = Field(aliases=['-j'], description='(plugin JARs) add one or more plugin JARs to the set of plugin JARs to process')
|
|
132
|
+
plugin_jars: Optional[list[FilePath]] = Field(aliases=['-J'], description='(plugin JARs) add the plugin JARs listed in one or more files to the set of plugin JARs to process')
|
|
101
133
|
|
|
102
134
|
@validator('plugin_jar', 'plugin_jars', each_item=True, pre=True)
|
|
103
|
-
def _expand_each_plugin_jars_path(cls, v: Path):
|
|
135
|
+
def _expand_each_plugin_jars_path(cls, v: Path) -> Path:
|
|
104
136
|
return path(v)
|
|
105
137
|
|
|
106
138
|
def get_plugin_jars(self):
|
|
107
|
-
ret = [*self.plugin_jar[
|
|
108
|
-
if len(ret)
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
ret = [*(self.plugin_jar or []), *[file_lines(file_path) for file_path in self.plugin_jars or []]]
|
|
140
|
+
if len(ret):
|
|
141
|
+
return ret
|
|
142
|
+
raise ValueError('Empty list of plugin JARs')
|
|
111
143
|
|
|
112
144
|
|
|
113
145
|
class NonInteractiveOptions(BaseModel):
|
|
@@ -208,10 +240,12 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
|
|
|
208
240
|
return self._build_plugin(build_plugin_command)
|
|
209
241
|
|
|
210
242
|
def _build_plugin(self, build_plugin_command: BuildPluginCommand) -> None:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
243
|
+
for psc in build_plugin_command.get_plugin_set_catalogs():
|
|
244
|
+
self._app.load_plugin_set_catalogs(psc)
|
|
245
|
+
for ps in build_plugin_command.get_plugin_sets():
|
|
246
|
+
self._app.load_plugin_sets(ps)
|
|
247
|
+
self._app.load_plugin_signing_credentials(build_plugin_command.get_plugin_signing_credentials())
|
|
248
|
+
self._obtain_password(build_plugin_command, non_interactive=build_plugin_command.non_interactive)
|
|
215
249
|
# Action
|
|
216
250
|
# ... plugin_id -> (set_id, jar_path, plugin)
|
|
217
251
|
ret = self._app.build_plugin(build_plugin_command.get_plugin_identifiers())
|
|
@@ -224,8 +258,10 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
|
|
|
224
258
|
self._do_string_command(string_command)
|
|
225
259
|
|
|
226
260
|
def _deploy_plugin(self, deploy_plugin_command: DeployPluginCommand) -> None:
|
|
227
|
-
|
|
228
|
-
|
|
261
|
+
for prc in deploy_plugin_command.get_plugin_registry_catalogs():
|
|
262
|
+
self._app.load_plugin_registry_catalogs(prc)
|
|
263
|
+
for pr in deploy_plugin_command.get_plugin_registries():
|
|
264
|
+
self._app.load_plugin_registries(pr)
|
|
229
265
|
# Action
|
|
230
266
|
# ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
|
|
231
267
|
ret = self._app.deploy_plugin(deploy_plugin_command.get_plugin_jars(),
|
|
@@ -245,21 +281,26 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
|
|
|
245
281
|
def _license(self, string_command: StringCommand) -> None:
|
|
246
282
|
self._do_string_command(string_command)
|
|
247
283
|
|
|
248
|
-
def _obtain_password(self,
|
|
249
|
-
if
|
|
250
|
-
_p =
|
|
251
|
-
elif
|
|
284
|
+
def _obtain_password(self, plugin_building_options: PluginBuildingOptions, non_interactive: bool=False) -> None:
|
|
285
|
+
if plugin_building_options.plugin_signing_password:
|
|
286
|
+
_p = plugin_building_options.plugin_signing_password
|
|
287
|
+
elif not non_interactive:
|
|
252
288
|
_p = getpass('Plugin signing password: ')
|
|
253
289
|
else:
|
|
254
290
|
self._parser.error('no plugin signing password specified while in non-interactive mode')
|
|
255
291
|
self._app.set_password(lambda: _p)
|
|
256
292
|
|
|
257
293
|
def _release_plugin(self, release_plugin_command: ReleasePluginCommand) -> None:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
294
|
+
for psc in release_plugin_command.get_plugin_set_catalogs():
|
|
295
|
+
self._app.load_plugin_set_catalogs(psc)
|
|
296
|
+
for ps in release_plugin_command.get_plugin_sets():
|
|
297
|
+
self._app.load_plugin_sets(ps)
|
|
298
|
+
for prc in release_plugin_command.get_plugin_registry_catalogs():
|
|
299
|
+
self._app.load_plugin_registry_catalogs(prc)
|
|
300
|
+
for pr in release_plugin_command.get_plugin_registries():
|
|
301
|
+
self._app.load_plugin_registries(pr)
|
|
302
|
+
self._app.load_plugin_signing_credentials(release_plugin_command.get_plugin_signing_credentials())
|
|
303
|
+
self._obtain_password(release_plugin_command, non_interactive=release_plugin_command.non_interactive)
|
|
263
304
|
# Action
|
|
264
305
|
# ... plugin_id -> list of (registry_id, layer_id, dst_path, plugin)
|
|
265
306
|
ret = self._app.release_plugin(release_plugin_command.get_plugin_identifiers(),
|
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')
|