lockss-turtles 0.5.0.dev4__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lockss/turtles/__init__.py +10 -30
- lockss/turtles/__main__.py +7 -3
- lockss/turtles/app.py +520 -109
- lockss/turtles/cli.py +540 -333
- lockss/turtles/plugin.py +207 -50
- lockss/turtles/plugin_registry.py +617 -189
- lockss/turtles/plugin_set.py +534 -187
- lockss/turtles/plugin_signing_credentials.py +84 -0
- lockss/turtles/util.py +70 -21
- {lockss_turtles-0.5.0.dev4.dist-info → lockss_turtles-0.6.0.dist-info}/LICENSE +1 -1
- lockss_turtles-0.6.0.dist-info/METADATA +64 -0
- lockss_turtles-0.6.0.dist-info/RECORD +18 -0
- {lockss_turtles-0.5.0.dev4.dist-info → lockss_turtles-0.6.0.dist-info}/WHEEL +1 -1
- unittest/lockss/turtles/__init__.py +106 -0
- unittest/lockss/turtles/test_plugin_registry.py +417 -0
- unittest/lockss/turtles/test_plugin_set.py +274 -0
- unittest/lockss/turtles/test_plugin_signing_credentials.py +102 -0
- CHANGELOG.rst +0 -113
- 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.5.0.dev4.dist-info/METADATA +0 -1041
- lockss_turtles-0.5.0.dev4.dist-info/RECORD +0 -20
- {lockss_turtles-0.5.0.dev4.dist-info → lockss_turtles-0.6.0.dist-info}/entry_points.txt +0 -0
lockss/turtles/plugin.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2000-
|
|
3
|
+
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
4
|
#
|
|
5
5
|
# Redistribution and use in source and binary forms, with or without
|
|
6
6
|
# modification, are permitted provided that the following conditions are met:
|
|
@@ -28,66 +28,67 @@
|
|
|
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
|
-
|
|
33
|
-
|
|
31
|
+
"""
|
|
32
|
+
Module to represent a LOCKSS plugin.
|
|
33
|
+
"""
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# Remove in Python 3.14; see https://stackoverflow.com/a/33533514
|
|
36
|
+
from __future__ import annotations
|
|
36
37
|
|
|
37
|
-
from
|
|
38
|
+
from collections.abc import Callable
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, AnyStr, IO, Optional
|
|
41
|
+
import xml.etree.ElementTree as ET
|
|
42
|
+
from zipfile import ZipFile
|
|
38
43
|
|
|
44
|
+
import java_manifest
|
|
45
|
+
from lockss.pybasic.fileutil import path
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
from .util import PathOrStr
|
|
41
48
|
|
|
42
|
-
@staticmethod
|
|
43
|
-
def from_jar(jar_path):
|
|
44
|
-
jar_path = _path(jar_path) # in case it's a string
|
|
45
|
-
plugin_id = Plugin.id_from_jar(jar_path)
|
|
46
|
-
plugin_fstr = str(Plugin.id_to_file(plugin_id))
|
|
47
|
-
with zipfile.ZipFile(jar_path, 'r') as zip_file:
|
|
48
|
-
with zip_file.open(plugin_fstr, 'r') as plugin_file:
|
|
49
|
-
return Plugin(plugin_file, plugin_fstr)
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
path = _path(path) # in case it's a string
|
|
54
|
-
with open(path, 'r') as input_file:
|
|
55
|
-
return Plugin(input_file, path)
|
|
50
|
+
#: A type alias for plugin identifiers.
|
|
51
|
+
PluginIdentifier = str
|
|
56
52
|
|
|
57
|
-
@staticmethod
|
|
58
|
-
def file_to_id(plugin_fstr):
|
|
59
|
-
return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for entry in manifest:
|
|
66
|
-
if entry.get('Lockss-Plugin') == 'true':
|
|
67
|
-
name = entry.get('Name')
|
|
68
|
-
if name is None:
|
|
69
|
-
raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
|
|
70
|
-
return Plugin.file_to_id(name)
|
|
71
|
-
else:
|
|
72
|
-
raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
|
|
54
|
+
class Plugin(object):
|
|
55
|
+
"""
|
|
56
|
+
An object to represent a LOCKSS plugin.
|
|
57
|
+
"""
|
|
73
58
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
def __init__(self, plugin_file: IO[AnyStr], plugin_path: PathOrStr) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Constructor.
|
|
77
62
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return Path(f'{plugin_id.replace(".", "/")}.xml')
|
|
63
|
+
Other exceptions than ``RuntimeError`` may be raised if the plugin
|
|
64
|
+
definition file cannot be parsed by ``xml.etree.ElementTree``.
|
|
81
65
|
|
|
82
|
-
|
|
66
|
+
:param plugin_file: An open file-like object that can read the plugin
|
|
67
|
+
definition file.
|
|
68
|
+
:type plugin_file: IO[AnyStr]
|
|
69
|
+
:param plugin_path: A string (or Path) representing the hierarchical
|
|
70
|
+
path of the plugin definition file (as a real file,
|
|
71
|
+
or as a file entry in a JAR file).
|
|
72
|
+
:type plugin_path: PathOrStr
|
|
73
|
+
:raises RuntimeError: If the plugin definition file parses as XML but
|
|
74
|
+
the top-level element is not <map>.
|
|
75
|
+
"""
|
|
83
76
|
super().__init__()
|
|
84
77
|
self._path = plugin_path
|
|
85
|
-
self._parsed =
|
|
78
|
+
self._parsed = ET.parse(plugin_file).getroot()
|
|
86
79
|
tag = self._parsed.tag
|
|
87
80
|
if tag != 'map':
|
|
88
81
|
raise RuntimeError(f'{plugin_path!s}: invalid root element: {tag}')
|
|
89
82
|
|
|
90
|
-
def get_aux_packages(self):
|
|
83
|
+
def get_aux_packages(self) -> list[str]:
|
|
84
|
+
"""
|
|
85
|
+
Returns the (possibly empty) list of auxiliary code packages declared by
|
|
86
|
+
the plugin (``plugin_aux_packages``).
|
|
87
|
+
|
|
88
|
+
:return: A non-null list of strings representing auxiliary code
|
|
89
|
+
packages.
|
|
90
|
+
:rtype: list[str]
|
|
91
|
+
"""
|
|
91
92
|
key = 'plugin_aux_packages'
|
|
92
93
|
lst = [x[1] for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
|
|
93
94
|
if lst is None or len(lst) < 1:
|
|
@@ -96,25 +97,181 @@ class Plugin(object):
|
|
|
96
97
|
raise ValueError(f'plugin declares {len(lst)} entries for {key}')
|
|
97
98
|
return [x.text for x in lst[0].findall('string')]
|
|
98
99
|
|
|
99
|
-
def get_identifier(self):
|
|
100
|
+
def get_identifier(self) -> Optional[PluginIdentifier]:
|
|
101
|
+
"""
|
|
102
|
+
Get this plugin's identifier (``plugin_identifier``).
|
|
103
|
+
|
|
104
|
+
:return: A plugin identifier, or None if missing.
|
|
105
|
+
:rtype: Optional[PluginIdentifier]
|
|
106
|
+
:raises ValueError: If the plugin definition contains more than one.
|
|
107
|
+
"""
|
|
100
108
|
return self._only_one('plugin_identifier')
|
|
101
109
|
|
|
102
|
-
def get_name(self):
|
|
110
|
+
def get_name(self) -> Optional[str]:
|
|
111
|
+
"""
|
|
112
|
+
Get this plugin's name (``plugin_name``).
|
|
113
|
+
|
|
114
|
+
:return: A plugin name, or None if missing.
|
|
115
|
+
:rtype: Optional[str]
|
|
116
|
+
:raises ValueError: If the plugin definition contains more than one.
|
|
117
|
+
"""
|
|
103
118
|
return self._only_one('plugin_name')
|
|
104
119
|
|
|
105
|
-
def get_parent_identifier(self):
|
|
120
|
+
def get_parent_identifier(self) -> Optional[PluginIdentifier]:
|
|
121
|
+
"""
|
|
122
|
+
Get this plugin's parent identifier (``plugin_parent``).
|
|
123
|
+
|
|
124
|
+
:return: A parent plugin identifier, or None if this plugin has no
|
|
125
|
+
parent.
|
|
126
|
+
:rtype: Optional[PluginIdentifier]
|
|
127
|
+
:raises ValueError: If the plugin definition contains more than one.
|
|
128
|
+
"""
|
|
106
129
|
return self._only_one('plugin_parent')
|
|
107
130
|
|
|
108
|
-
def get_parent_version(self):
|
|
131
|
+
def get_parent_version(self) -> Optional[int]:
|
|
132
|
+
"""
|
|
133
|
+
Get this plugin's parent version (``plugin_parent_version``).
|
|
134
|
+
|
|
135
|
+
:return: A parent plugin version, or None if this plugin has no
|
|
136
|
+
parent.
|
|
137
|
+
:rtype: Optional[int]
|
|
138
|
+
:raises ValueError: If the plugin definition contains more than one.
|
|
139
|
+
"""
|
|
109
140
|
return self._only_one('plugin_parent_version', int)
|
|
110
141
|
|
|
111
|
-
def get_version(self):
|
|
142
|
+
def get_version(self) -> Optional[int]:
|
|
143
|
+
"""
|
|
144
|
+
Get this plugin's version (``plugin_version``).
|
|
145
|
+
|
|
146
|
+
:return: A plugin version, or None if missing.
|
|
147
|
+
:rtype: Optional[int]
|
|
148
|
+
:raises ValueError: If the plugin definition contains more than one.
|
|
149
|
+
"""
|
|
112
150
|
return self._only_one('plugin_version', int)
|
|
113
151
|
|
|
114
|
-
def _only_one(self, key, result=str):
|
|
152
|
+
def _only_one(self, key: str, result: Callable[[str], Any]=str) -> Optional[Any]:
|
|
153
|
+
"""
|
|
154
|
+
Retrieves the value of a given key in the plugin definition, optionally
|
|
155
|
+
coerced into a representation (by default simply a string).
|
|
156
|
+
|
|
157
|
+
:param key: A plugin key.
|
|
158
|
+
:param key: str
|
|
159
|
+
:param result: A functor that takes a string and returns the desired
|
|
160
|
+
representation; by default this is the string constructor
|
|
161
|
+
``str``, meaning by default the string values are
|
|
162
|
+
returned unchanged.
|
|
163
|
+
:param result: Callable[[str], Any]
|
|
164
|
+
:return: The value for the given key, coerced through the given functor,
|
|
165
|
+
or None if the plugin definition does not contain any entry
|
|
166
|
+
with the given key.
|
|
167
|
+
:rtype: Optional[Any]
|
|
168
|
+
:raises ValueError: If the plugin definition contains more than one
|
|
169
|
+
entry with the given key.
|
|
170
|
+
"""
|
|
115
171
|
lst = [x[1].text for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
|
|
116
172
|
if lst is None or len(lst) < 1:
|
|
117
173
|
return None
|
|
118
174
|
if len(lst) > 1:
|
|
119
175
|
raise ValueError(f'plugin declares {len(lst)} entries for {key}')
|
|
120
176
|
return result(lst[0])
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def from_jar(jar_path_or_str: PathOrStr) -> Plugin:
|
|
180
|
+
"""
|
|
181
|
+
Instantiates a Plugin object from the given plugin JAR file.
|
|
182
|
+
|
|
183
|
+
:param jar_path_or_str: The path to a plugin JAR.
|
|
184
|
+
:type jar_path_or_str: PathOrStr
|
|
185
|
+
:return: A Plugin object.
|
|
186
|
+
:rtype: Plugin
|
|
187
|
+
"""
|
|
188
|
+
jar_path = path(jar_path_or_str)
|
|
189
|
+
plugin_id = Plugin.id_from_jar(jar_path)
|
|
190
|
+
plugin_fstr = str(Plugin.id_to_file(plugin_id))
|
|
191
|
+
with ZipFile(jar_path, 'r') as zip_file:
|
|
192
|
+
with zip_file.open(plugin_fstr, 'r') as plugin_file:
|
|
193
|
+
return Plugin(plugin_file, plugin_fstr)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def from_path(path_or_str: PathOrStr) -> Plugin:
|
|
197
|
+
"""
|
|
198
|
+
Instantiates a Plugin object from the given plugin file.
|
|
199
|
+
|
|
200
|
+
:param path_or_str: The path to a plugin file.
|
|
201
|
+
:type path_or_str: PathOrStr
|
|
202
|
+
:return: A Plugin object.
|
|
203
|
+
:rtype: Plugin
|
|
204
|
+
"""
|
|
205
|
+
fpath = path(path_or_str)
|
|
206
|
+
with fpath.open('r') as input_file:
|
|
207
|
+
return Plugin(input_file, fpath)
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def file_to_id(plugin_fstr: str) -> PluginIdentifier:
|
|
211
|
+
"""
|
|
212
|
+
Converts a plugin file path (ending in ``.xml``) to the implied plugin
|
|
213
|
+
identifier (e.g. ``org/myproject/plugin/MyPlugin.xml`` implies
|
|
214
|
+
``org.myproject.plugin.MyPlugin``).
|
|
215
|
+
|
|
216
|
+
See also ``id_to_file``.
|
|
217
|
+
|
|
218
|
+
:param plugin_fstr: A string file path.
|
|
219
|
+
:type plugin_fstr: str
|
|
220
|
+
:return: A plugin identifier.
|
|
221
|
+
:rtype: PluginIdentifier
|
|
222
|
+
"""
|
|
223
|
+
return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def id_from_jar(jar_path_or_str: PathOrStr) -> PluginIdentifier:
|
|
227
|
+
"""
|
|
228
|
+
Extracts the plugin identifier from a plugin JAR's manifest file.
|
|
229
|
+
|
|
230
|
+
:param jar_path_or_str: The path to a plugin JAR.
|
|
231
|
+
:type jar_path_or_str: PathOrStr
|
|
232
|
+
:return: The plugin identifier extracted from the given plugin JAR's
|
|
233
|
+
manifest file.
|
|
234
|
+
:rtype: PluginIdentifier
|
|
235
|
+
:raises Exception: If the JAR's manifest file has no entry with
|
|
236
|
+
``Lockss-Plugin`` equal to ``true`` and ``Name``
|
|
237
|
+
equal to the packaged plugin's identifier.
|
|
238
|
+
"""
|
|
239
|
+
jar_path = path(jar_path_or_str)
|
|
240
|
+
manifest = java_manifest.from_jar(jar_path)
|
|
241
|
+
for entry in manifest:
|
|
242
|
+
if entry.get('Lockss-Plugin') == 'true':
|
|
243
|
+
name = entry.get('Name')
|
|
244
|
+
if name is None:
|
|
245
|
+
raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
|
|
246
|
+
return Plugin.file_to_id(name)
|
|
247
|
+
else:
|
|
248
|
+
raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def id_to_dir(plugin_id: PluginIdentifier) -> Path:
|
|
252
|
+
"""
|
|
253
|
+
Returns the path of the directory containing the given plugin identifier
|
|
254
|
+
(for example ``org/myproject/plugin`` for
|
|
255
|
+
``org.myproject.plugin.MyPlugin``).
|
|
256
|
+
|
|
257
|
+
:param plugin_id: A plugin identifier.
|
|
258
|
+
:type plugin_id: PluginIdentifier
|
|
259
|
+
:return: The directory path containing the given plugin identifier.
|
|
260
|
+
:rtype: Path
|
|
261
|
+
"""
|
|
262
|
+
return Plugin.id_to_file(plugin_id).parent
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def id_to_file(plugin_id: PluginIdentifier) -> Path:
|
|
266
|
+
"""
|
|
267
|
+
Returns the path of the definition file corresponding to the given
|
|
268
|
+
plugin identifier (for example ``org/myproject/plugin/MyPlugin.xml`` for
|
|
269
|
+
``org.myproject.plugin.MyPlugin``).
|
|
270
|
+
|
|
271
|
+
:param plugin_id: A plugin identifier.
|
|
272
|
+
:type plugin_id: PluginIdentifier
|
|
273
|
+
:return: The path of the definition file corresponding to the given
|
|
274
|
+
plugin identifier.
|
|
275
|
+
:rtype: Path
|
|
276
|
+
"""
|
|
277
|
+
return Path(f'{plugin_id.replace(".", "/")}.xml')
|