lockss-turtles 0.6.0.dev19__tar.gz → 0.6.0.dev21__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lockss-turtles
3
- Version: 0.6.0.dev19
3
+ Version: 0.6.0.dev21
4
4
  Summary: Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries
5
5
  License: BSD-3-Clause
6
6
  Author: Thib Guicherd-Callin
@@ -34,7 +34,7 @@ Description-Content-Type: text/x-rst
34
34
  Turtles
35
35
  =======
36
36
 
37
- .. |RELEASE| replace:: 0.6.0-dev19
37
+ .. |RELEASE| replace:: 0.6.0-dev20
38
38
  .. |RELEASE_DATE| replace:: ?
39
39
 
40
40
  .. |HELP| replace:: ``--help/-h``
@@ -2,7 +2,7 @@
2
2
  Turtles
3
3
  =======
4
4
 
5
- .. |RELEASE| replace:: 0.6.0-dev19
5
+ .. |RELEASE| replace:: 0.6.0-dev20
6
6
  .. |RELEASE_DATE| replace:: ?
7
7
 
8
8
  .. |HELP| replace:: ``--help/-h``
@@ -28,7 +28,7 @@
28
28
 
29
29
  [project]
30
30
  name = "lockss-turtles"
31
- version = "0.6.0-dev19" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
31
+ version = "0.6.0-dev21" # 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"
@@ -5,12 +5,15 @@ Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin
5
5
  registries.
6
6
  """
7
7
 
8
- __version__ = '0.6.0-dev19'
8
+ #: This package's version.
9
+ __version__ = '0.6.0-dev21'
9
10
 
11
+ #: This package's copyright.
10
12
  __copyright__ = '''
11
13
  Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
12
14
  '''.strip()
13
15
 
16
+ #: This package's license.
14
17
  __license__ = __copyright__ + '\n\n' + '''
15
18
  Redistribution and use in source and binary forms, with or without
16
19
  modification, are permitted provided that the following conditions are met:
@@ -74,7 +74,7 @@ class TurtlesApp(object):
74
74
 
75
75
  ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
76
76
 
77
- CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, USR_CONFIG_DIR, ETC_CONFIG_DIR)
77
+ CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, ETC_CONFIG_DIR, USR_CONFIG_DIR)
78
78
 
79
79
  PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
80
80
 
@@ -52,10 +52,10 @@ from .util import file_or
52
52
 
53
53
 
54
54
  class PluginBuildingOptions(BaseModel):
55
- plugin_set: Optional[list[FilePath]] = Field(aliases=['-s'], description=f'(plugin sets) add one or more plugin set definition files to the loaded plugin sets')
56
- 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())}')
57
- 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())}')
58
- plugin_signing_password: Optional[str] = Field(description='(plugin signing credentials) set the plugin signing password, or if none, prompt interactively')
55
+ plugin_set: Optional[list[FilePath]] = Field(aliases=['-s'], title='Plugin Sets', description=f'(plugin sets) add one or more plugin set definition files to the loaded plugin sets')
56
+ plugin_set_catalog: Optional[list[FilePath]] = Field(aliases=['-S'], title='Plugin Set Catalogs', 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())}')
57
+ plugin_signing_credentials: Optional[FilePath] = Field(aliases=['-c'], title='Plugin Signing Credentials', 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())}')
58
+ plugin_signing_password: Optional[str] = Field(title='Plugin Signing Password', description='(plugin signing credentials) set the plugin signing password, or if none, prompt interactively')
59
59
 
60
60
  def get_plugin_sets(self) -> list[Path]:
61
61
  return [path(p) for p in self.plugin_set or []]
@@ -119,7 +119,7 @@ class PluginIdentifierOptions(BaseModel):
119
119
  return path(v)
120
120
 
121
121
  def get_plugin_identifiers(self) -> list[str]:
122
- ret = [*(self.plugin_identifier or []), *[file_lines(file_path) for file_path in self.plugin_identifiers or []]]
122
+ ret = [*(self.plugin_identifier or []), *chain.from_iterable(file_lines(file_path) for file_path in self.plugin_identifiers or [])]
123
123
  if ret:
124
124
  return ret
125
125
  raise ValueError('Empty list of plugin identifiers')
@@ -137,7 +137,7 @@ class PluginJarOptions(BaseModel):
137
137
  return path(v)
138
138
 
139
139
  def get_plugin_jars(self):
140
- ret = [*(self.plugin_jar or []), *[file_lines(file_path) for file_path in self.plugin_jars or []]]
140
+ ret = [*(self.plugin_jar or []), *chain.from_iterable(file_lines(file_path) for file_path in self.plugin_jars or [])]
141
141
  if len(ret):
142
142
  return ret
143
143
  raise ValueError('Empty list of plugin JARs')
@@ -161,7 +161,7 @@ class ReleasePluginCommand(OutputFormatOptions, NonInteractiveOptions, PluginDep
161
161
 
162
162
  class TurtlesCommand(BaseModel):
163
163
  bp: Optional[BuildPluginCommand] = Field(description='synonym for: build-plugin')
164
- build_plugin: Optional[BuildPluginCommand] = Field(description='build (package and sign) plugins', alias='build-plugin')
164
+ build_plugin: Optional[BuildPluginCommand] = Field(description='build plugins', alias='build-plugin')
165
165
  copyright: Optional[StringCommand.type(__copyright__)] = Field(description=COPYRIGHT_DESCRIPTION)
166
166
  deploy_plugin: Optional[DeployPluginCommand] = Field(description='deploy plugins', alias='deploy-plugin')
167
167
  dp: Optional[DeployPluginCommand] = Field(description='synonym for: deploy-plugin')
@@ -0,0 +1,278 @@
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
+ Library to represent a LOCKSS plugin.
33
+ """
34
+
35
+ # Remove in Python 3.14
36
+ # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
37
+ from __future__ import annotations
38
+
39
+ from collections.abc import Callable
40
+ from pathlib import Path
41
+ from typing import Any, AnyStr, IO, Optional
42
+ import xml.etree.ElementTree as ET
43
+ from zipfile import ZipFile
44
+
45
+ import java_manifest
46
+ from lockss.pybasic.fileutil import path
47
+
48
+ from .util import PathOrStr
49
+
50
+
51
+ #: A type alias for plugin identifiers.
52
+ PluginIdentifier = str
53
+
54
+
55
+ class Plugin(object):
56
+ """
57
+ An object to represent a LOCKSS plugin.
58
+ """
59
+
60
+ def __init__(self, plugin_file: IO[AnyStr], plugin_path: PathOrStr) -> None:
61
+ """
62
+ Constructor.
63
+
64
+ Other exceptions than ``RuntimeError`` may be raised if the plugin
65
+ definition file cannot be parsed by ``xml.etree.ElementTree``.
66
+
67
+ :param plugin_file: An open file-like object that can read the plugin
68
+ definition file.
69
+ :type plugin_file: IO[AnyStr]
70
+ :param plugin_path: A string (or Path) representing the hierarchical
71
+ path of the plugin definition file (as a real file,
72
+ or as a file entry in a JAR file).
73
+ :type plugin_path: PathOrStr
74
+ :raises RuntimeError: If the plugin definition file parses as XML but
75
+ the top-level element is not <map>.
76
+ """
77
+ super().__init__()
78
+ self._path = plugin_path
79
+ self._parsed = ET.parse(plugin_file).getroot()
80
+ tag = self._parsed.tag
81
+ if tag != 'map':
82
+ raise RuntimeError(f'{plugin_path!s}: invalid root element: {tag}')
83
+
84
+ def get_aux_packages(self) -> list[str]:
85
+ """
86
+ Returns the (possibly empty) list of auxiliary code packages declared by
87
+ the plugin (``plugin_aux_packages``).
88
+
89
+ :return: A non-null list of strings representing auxiliary code
90
+ packages.
91
+ :rtype: list[str]
92
+ """
93
+ key = 'plugin_aux_packages'
94
+ lst = [x[1] for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
95
+ if lst is None or len(lst) < 1:
96
+ return []
97
+ if len(lst) > 1:
98
+ raise ValueError(f'plugin declares {len(lst)} entries for {key}')
99
+ return [x.text for x in lst[0].findall('string')]
100
+
101
+ def get_identifier(self) -> Optional[PluginIdentifier]:
102
+ """
103
+ Get this plugin's identifier (``plugin_identifier``).
104
+
105
+ :return: A plugin identifier, or None if missing.
106
+ :rtype: Optional[PluginIdentifier]
107
+ :raises ValueError: If the plugin definition contains more than one.
108
+ """
109
+ return self._only_one('plugin_identifier')
110
+
111
+ def get_name(self) -> Optional[str]:
112
+ """
113
+ Get this plugin's name (``plugin_name``).
114
+
115
+ :return: A plugin name, or None if missing.
116
+ :rtype: Optional[str]
117
+ :raises ValueError: If the plugin definition contains more than one.
118
+ """
119
+ return self._only_one('plugin_name')
120
+
121
+ def get_parent_identifier(self) -> Optional[PluginIdentifier]:
122
+ """
123
+ Get this plugin's parent identifier (``plugin_parent``).
124
+
125
+ :return: A parent plugin identifier, or None if this plugin has no
126
+ parent.
127
+ :rtype: Optional[PluginIdentifier]
128
+ :raises ValueError: If the plugin definition contains more than one.
129
+ """
130
+ return self._only_one('plugin_parent')
131
+
132
+ def get_parent_version(self) -> Optional[int]:
133
+ """
134
+ Get this plugin's parent version (``plugin_parent_version``).
135
+
136
+ :return: A parent plugin version, or None if this plugin has no
137
+ parent.
138
+ :rtype: Optional[int]
139
+ :raises ValueError: If the plugin definition contains more than one.
140
+ """
141
+ return self._only_one('plugin_parent_version', int)
142
+
143
+ def get_version(self) -> Optional[int]:
144
+ """
145
+ Get this plugin's version (``plugin_version``).
146
+
147
+ :return: A plugin version, or None if missing.
148
+ :rtype: Optional[int]
149
+ :raises ValueError: If the plugin definition contains more than one.
150
+ """
151
+ return self._only_one('plugin_version', int)
152
+
153
+ def _only_one(self, key: str, result: Callable[[str], Any]=str) -> Optional[Any]:
154
+ """
155
+ Retrieves the value of a given key in the plugin definition, optionally
156
+ coerced into a representation (by default simply a string).
157
+
158
+ :param key: A plugin key.
159
+ :param key: str
160
+ :param result: A functor that takes a string and returns the desired
161
+ representation; by default this is the string constructor
162
+ ``str``, meaning by default the string values are
163
+ returned unchanged.
164
+ :param result: Callable[[str], Any]
165
+ :return: The value for the given key, coerced through the given functor,
166
+ or None if the plugin definition does not contain any entry
167
+ with the given key.
168
+ :rtype: Optional[Any]
169
+ :raises ValueError: If the plugin definition contains more than one
170
+ entry with the given key.
171
+ """
172
+ lst = [x[1].text for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
173
+ if lst is None or len(lst) < 1:
174
+ return None
175
+ if len(lst) > 1:
176
+ raise ValueError(f'plugin declares {len(lst)} entries for {key}')
177
+ return result(lst[0])
178
+
179
+ @staticmethod
180
+ def from_jar(jar_path_or_str: PathOrStr) -> Plugin:
181
+ """
182
+ Instantiates a Plugin object from the given plugin JAR file.
183
+
184
+ :param jar_path_or_str: The path to a plugin JAR.
185
+ :type jar_path_or_str: PathOrStr
186
+ :return: A Plugin object.
187
+ :rtype: Plugin
188
+ """
189
+ jar_path = path(jar_path_or_str)
190
+ plugin_id = Plugin.id_from_jar(jar_path)
191
+ plugin_fstr = str(Plugin.id_to_file(plugin_id))
192
+ with ZipFile(jar_path, 'r') as zip_file:
193
+ with zip_file.open(plugin_fstr, 'r') as plugin_file:
194
+ return Plugin(plugin_file, plugin_fstr)
195
+
196
+ @staticmethod
197
+ def from_path(path_or_str: PathOrStr) -> Plugin:
198
+ """
199
+ Instantiates a Plugin object from the given plugin file.
200
+
201
+ :param path_or_str: The path to a plugin file.
202
+ :type path_or_str: PathOrStr
203
+ :return: A Plugin object.
204
+ :rtype: Plugin
205
+ """
206
+ fpath = path(path_or_str)
207
+ with fpath.open('r') as input_file:
208
+ return Plugin(input_file, fpath)
209
+
210
+ @staticmethod
211
+ def file_to_id(plugin_fstr: str) -> PluginIdentifier:
212
+ """
213
+ Converts a plugin file path (ending in ``.xml``) to the implied plugin
214
+ identifier (e.g. ``org/myproject/plugin/MyPlugin.xml`` implies
215
+ ``org.myproject.plugin.MyPlugin``).
216
+
217
+ See also ``id_to_file``.
218
+
219
+ :param plugin_fstr: A string file path.
220
+ :type plugin_fstr: str
221
+ :return: A plugin identifier.
222
+ :rtype: PluginIdentifier
223
+ """
224
+ return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
225
+
226
+ @staticmethod
227
+ def id_from_jar(jar_path_or_str: PathOrStr) -> PluginIdentifier:
228
+ """
229
+ Extracts the plugin identifier from a plugin JAR's manifest file.
230
+
231
+ :param jar_path_or_str: The path to a plugin JAR.
232
+ :type jar_path_or_str: PathOrStr
233
+ :return: The plugin identifier extracted from the given plugin JAR's
234
+ manifest file.
235
+ :rtype: PluginIdentifier
236
+ :raises Exception: If the JAR's manifest file has no entry with
237
+ ``Lockss-Plugin`` equal to ``true`` and ``Name``
238
+ equal to the packaged plugin's identifier.
239
+ """
240
+ jar_path = path(jar_path_or_str)
241
+ manifest = java_manifest.from_jar(jar_path)
242
+ for entry in manifest:
243
+ if entry.get('Lockss-Plugin') == 'true':
244
+ name = entry.get('Name')
245
+ if name is None:
246
+ raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
247
+ return Plugin.file_to_id(name)
248
+ else:
249
+ raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
250
+
251
+ @staticmethod
252
+ def id_to_dir(plugin_id: PluginIdentifier) -> Path:
253
+ """
254
+ Returns the path of the directory containing the given plugin identifier
255
+ (for example ``org/myproject/plugin`` for
256
+ ``org.myproject.plugin.MyPlugin``).
257
+
258
+ :param plugin_id: A plugin identifier.
259
+ :type plugin_id: PluginIdentifier
260
+ :return: The directory path containing the given plugin identifier.
261
+ :rtype: Path
262
+ """
263
+ return Plugin.id_to_file(plugin_id).parent
264
+
265
+ @staticmethod
266
+ def id_to_file(plugin_id: PluginIdentifier) -> Path:
267
+ """
268
+ Returns the path of the definition file corresponding to the given
269
+ plugin identifier (for example ``org/myproject/plugin/MyPlugin.xml`` for
270
+ ``org.myproject.plugin.MyPlugin``).
271
+
272
+ :param plugin_id: A plugin identifier.
273
+ :type plugin_id: PluginIdentifier
274
+ :return: The path of the definition file corresponding to the given
275
+ plugin identifier.
276
+ :rtype: Path
277
+ """
278
+ return Path(f'{plugin_id.replace(".", "/")}.xml')
@@ -45,13 +45,12 @@ from .plugin import Plugin, PluginIdentifier
45
45
  from .util import BaseModelWithRoot
46
46
 
47
47
 
48
-
49
48
  PluginRegistryCatalogKind = Literal['PluginRegistryCatalog']
50
49
 
51
50
 
52
51
  class PluginRegistryCatalog(BaseModelWithRoot):
53
- kind: PluginRegistryCatalogKind = Field(description="This object's kind")
54
- plugin_registry_files: list[str] = Field(min_length=1, description="A non-empty list of plugin registry files", title='Plugin Registry Files', alias='plugin-registry-files')
52
+ kind: PluginRegistryCatalogKind = Field(title='Kind', description="This object's kind")
53
+ plugin_registry_files: list[str] = Field(alias='plugin-registry-files', min_length=1, title='Plugin Registry Files', description="A non-empty list of plugin registry files")
55
54
 
56
55
  def get_plugin_registry_files(self) -> list[Path]:
57
56
  return [self.get_root().joinpath(pstr) for pstr in self.plugin_registry_files]
@@ -66,7 +65,7 @@ PluginRegistryLayoutFileNamingConvention = Literal['abbreviated', 'identifier',
66
65
  class BasePluginRegistryLayout(BaseModel, ABC):
67
66
  TYPE_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Registry Layout Type', description='A plugin registry layout type')
68
67
  FILE_NAMING_CONVENTION_DEFAULT: ClassVar[PluginRegistryLayoutFileNamingConvention] = 'identifier'
69
- FILE_NAMING_CONVENTION_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Registry Layout File Naming Convention', description='A file naming convention for the plugin registry layout', alias='file-naming-convention')
68
+ FILE_NAMING_CONVENTION_FIELD: ClassVar[dict[str, str]] = dict(alias='file-naming-convention', title='Plugin Registry Layout File Naming Convention', description='A file naming convention for the plugin registry layout')
70
69
 
71
70
  _plugin_registry: Optional[PluginRegistry]
72
71
 
@@ -213,13 +212,13 @@ PluginRegistryIdentifier = str
213
212
 
214
213
 
215
214
  class PluginRegistry(BaseModelWithRoot):
216
- kind: PluginRegistryKind = Field(description="This object's kind")
215
+ kind: PluginRegistryKind = Field(title='Kind', description="This object's kind")
217
216
  id: PluginRegistryIdentifier = Field(title='Plugin Registry Identifier', description='An identifier for the plugin set')
218
217
  name: str = Field(title='Plugin Registry Name', description='A name for the plugin set')
219
218
  layout: PluginRegistryLayout = Field(title='Plugin Registry Layout', description='A layout for the plugin registry')
220
219
  layers: list[PluginRegistryLayer] = Field(min_length=1, title='Plugin Registry Layers', description="A non-empty list of plugin registry layers")
221
- plugin_identifiers: list[PluginIdentifier] = Field(min_length=1, title='Plugin Identifiers', description="A non-empty list of plugin identifiers", alias='plugin-identifiers')
222
- suppressed_plugin_identifiers: list[PluginIdentifier] = Field([], title='Suppressed Plugin Identifiers', description="A list of suppressed plugin identifiers", alias='suppressed-plugin-identifiers')
220
+ plugin_identifiers: list[PluginIdentifier] = Field(alias='plugin-identifiers', min_length=1, title='Plugin Identifiers', description="A non-empty list of plugin identifiers")
221
+ suppressed_plugin_identifiers: list[PluginIdentifier] = Field([], alias='suppressed-plugin-identifiers', title='Suppressed Plugin Identifiers', description="A list of suppressed plugin identifiers")
223
222
 
224
223
  def get_id(self) -> PluginRegistryIdentifier:
225
224
  return self.id
@@ -59,9 +59,9 @@ class PluginSetCatalog(BaseModelWithRoot):
59
59
  plugin set catalog.
60
60
  """
61
61
  #: This object's kind.
62
- kind: PluginSetCatalogKind = Field(description="This object's kind")
62
+ kind: PluginSetCatalogKind = Field(title='Kind', description="This object's kind")
63
63
  #: A non-empty list of plugin set files.
64
- plugin_set_files: list[str] = Field(min_length=1, description="A non-empty list of plugin set files", title='Plugin Set Files', alias='plugin-set-files')
64
+ plugin_set_files: list[str] = Field(alias='plugin-set-files', min_length=1, title='Plugin Set Files', description="A non-empty list of plugin set files")
65
65
 
66
66
  def get_plugin_set_files(self) -> list[Path]:
67
67
  """
@@ -86,11 +86,11 @@ class BasePluginSetBuilder(BaseModelWithRoot, ABC):
86
86
  """
87
87
 
88
88
  #: Pydantic definition of the ``type`` field.
89
- TYPE_FIELD: ClassVar[dict[str, str]] = dict(description='A plugin builder type', title='Plugin Builder Type')
89
+ TYPE_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Builder Type', description='A plugin builder type')
90
90
  #: Pydantic definition of the ``main`` field.
91
- MAIN_FIELD: ClassVar[dict[str, str]] = dict(description="The path to the plugins' source code, relative to the root of the project", title='Main Code Path')
91
+ MAIN_FIELD: ClassVar[dict[str, str]] = dict(title='Main Code Path', description="The path to the plugins' source code, relative to the root of the project")
92
92
  #: Pydantic definition of the ``test`` field.
93
- TEST_FIELD: ClassVar[dict[str, str]] = dict(description="The path to the plugins' unit tests, relative to the root of the project", title='Test Code Path')
93
+ TEST_FIELD: ClassVar[dict[str, str]] = dict(title='Test Code Path', description="The path to the plugins' unit tests, relative to the root of the project")
94
94
 
95
95
  @abstractmethod
96
96
  def build_plugin(self, plugin_id: PluginIdentifier, keystore_path: Path, keystore_alias: str, keystore_password=None) -> tuple[Path, Plugin]:
@@ -361,10 +361,10 @@ PluginSetIdentifier = str
361
361
 
362
362
 
363
363
  class PluginSet(BaseModel):
364
- kind: PluginSetKind = Field(description="This object's kind")
365
- id: PluginSetIdentifier = Field(description='An identifier for the plugin set')
366
- name: str = Field(description='A name for the plugin set')
367
- builder: PluginSetBuilder = Field(description='A builder for the plugin set', title='Plugin Set Builder')
364
+ kind: PluginSetKind = Field(title='Kind', description="This object's kind")
365
+ id: PluginSetIdentifier = Field(title='Plugin Set Identifier', description='An identifier for the plugin set')
366
+ name: str = Field(title='Plugin Set Name', description='A name for the plugin set')
367
+ builder: PluginSetBuilder = Field(title='Plugin Set Builder', description='A builder for the plugin set')
368
368
 
369
369
  def build_plugin(self, plugin_id: PluginIdentifier, keystore_path: Path, keystore_alias: str, keystore_password=None) -> tuple[Path, Plugin]:
370
370
  return self.builder.build_plugin(plugin_id, keystore_path, keystore_alias, keystore_password)
@@ -1,134 +0,0 @@
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
- Library to represent a LOCKSS plugin.
33
- """
34
-
35
- # Remove in Python 3.14
36
- # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
37
- from __future__ import annotations
38
-
39
- from collections.abc import Callable
40
- from pathlib import Path
41
- from typing import Any, Optional
42
- import xml.etree.ElementTree as ET
43
- from zipfile import ZipFile
44
-
45
- import java_manifest
46
- from lockss.pybasic.fileutil import path
47
-
48
- from .util import PathOrStr
49
-
50
-
51
- PluginIdentifier = str
52
-
53
-
54
- class Plugin(object):
55
-
56
- def __init__(self, plugin_file, plugin_path) -> None:
57
- super().__init__()
58
- self._path = plugin_path
59
- self._parsed = ET.parse(plugin_file).getroot()
60
- tag = self._parsed.tag
61
- if tag != 'map':
62
- raise RuntimeError(f'{plugin_path!s}: invalid root element: {tag}')
63
-
64
- def get_aux_packages(self) -> list[str]:
65
- key = 'plugin_aux_packages'
66
- lst = [x[1] for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
67
- if lst is None or len(lst) < 1:
68
- return []
69
- if len(lst) > 1:
70
- raise ValueError(f'plugin declares {len(lst)} entries for {key}')
71
- return [x.text for x in lst[0].findall('string')]
72
-
73
- def get_identifier(self) -> Optional[PluginIdentifier]:
74
- return self._only_one('plugin_identifier')
75
-
76
- def get_name(self) -> Optional[str]:
77
- return self._only_one('plugin_name')
78
-
79
- def get_parent_identifier(self) -> Optional[PluginIdentifier]:
80
- return self._only_one('plugin_parent')
81
-
82
- def get_parent_version(self) -> Optional[int]:
83
- return self._only_one('plugin_parent_version', int)
84
-
85
- def get_version(self) -> Optional[int]:
86
- return self._only_one('plugin_version', int)
87
-
88
- def _only_one(self, key: str, result: Callable=str) -> Optional[Any]:
89
- lst = [x[1].text for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
90
- if lst is None or len(lst) < 1:
91
- return None
92
- if len(lst) > 1:
93
- raise ValueError(f'plugin declares {len(lst)} entries for {key}')
94
- return result(lst[0])
95
-
96
- @staticmethod
97
- def from_jar(jar_path: PathOrStr) -> Plugin:
98
- jar_path = path(jar_path) # in case it's a string
99
- plugin_id = Plugin.id_from_jar(jar_path)
100
- plugin_fstr = str(Plugin.id_to_file(plugin_id))
101
- with ZipFile(jar_path, 'r') as zip_file:
102
- with zip_file.open(plugin_fstr, 'r') as plugin_file:
103
- return Plugin(plugin_file, plugin_fstr)
104
-
105
- @staticmethod
106
- def from_path(fpath: PathOrStr) -> Plugin:
107
- fpath = path(fpath) # in case it's a string
108
- with open(fpath, 'r') as input_file:
109
- return Plugin(input_file, fpath)
110
-
111
- @staticmethod
112
- def file_to_id(plugin_fstr: str) -> PluginIdentifier:
113
- return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
114
-
115
- @staticmethod
116
- def id_from_jar(jar_path: PathOrStr) -> PluginIdentifier:
117
- jar_path = path(jar_path) # in case it's a string
118
- manifest = java_manifest.from_jar(jar_path)
119
- for entry in manifest:
120
- if entry.get('Lockss-Plugin') == 'true':
121
- name = entry.get('Name')
122
- if name is None:
123
- raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
124
- return Plugin.file_to_id(name)
125
- else:
126
- raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
127
-
128
- @staticmethod
129
- def id_to_dir(plugin_id: PluginIdentifier) -> Path:
130
- return Plugin.id_to_file(plugin_id).parent
131
-
132
- @staticmethod
133
- def id_to_file(plugin_id: PluginIdentifier) -> Path:
134
- return Path(f'{plugin_id.replace(".", "/")}.xml')