lockss-turtles 0.6.0.dev17__tar.gz → 0.6.0.dev19__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.dev17
3
+ Version: 0.6.0.dev19
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-dev1
37
+ .. |RELEASE| replace:: 0.6.0-dev19
38
38
  .. |RELEASE_DATE| replace:: ?
39
39
 
40
40
  .. |HELP| replace:: ``--help/-h``
@@ -54,7 +54,7 @@ Turtles
54
54
  :alt: Turtles logo
55
55
  :align: right
56
56
 
57
- Turtles is a tool to manage LOCKSS plugin sets and LOCKSS plugin registries.
57
+ Turtles is a command line tool and Python library to manage LOCKSS plugin sets and LOCKSS plugin registries.
58
58
 
59
59
  **Latest release:** |RELEASE| (|RELEASE_DATE|)
60
60
 
@@ -2,7 +2,7 @@
2
2
  Turtles
3
3
  =======
4
4
 
5
- .. |RELEASE| replace:: 0.6.0-dev1
5
+ .. |RELEASE| replace:: 0.6.0-dev19
6
6
  .. |RELEASE_DATE| replace:: ?
7
7
 
8
8
  .. |HELP| replace:: ``--help/-h``
@@ -22,7 +22,7 @@ Turtles
22
22
  :alt: Turtles logo
23
23
  :align: right
24
24
 
25
- Turtles is a tool to manage LOCKSS plugin sets and LOCKSS plugin registries.
25
+ Turtles is a command line tool and Python library to manage LOCKSS plugin sets and LOCKSS plugin registries.
26
26
 
27
27
  **Latest release:** |RELEASE| (|RELEASE_DATE|)
28
28
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  [project]
30
30
  name = "lockss-turtles"
31
- version = "0.6.0-dev17" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
31
+ version = "0.6.0-dev19" # 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,7 +5,7 @@ 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-dev17'
8
+ __version__ = '0.6.0-dev19'
9
9
 
10
10
  __copyright__ = '''
11
11
  Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
@@ -28,6 +28,10 @@
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
+ Entry point for the ``lockss.turtles`` module.
33
+ """
34
+
31
35
  from lockss.turtles.cli import main
32
36
 
33
37
  main()
@@ -33,6 +33,7 @@ Tool for managing LOCKSS plugin sets and LOCKSS plugin registries
33
33
  """
34
34
 
35
35
  from getpass import getpass
36
+ from itertools import chain
36
37
  from pathlib import Path
37
38
 
38
39
  from exceptiongroup import ExceptionGroup
@@ -51,7 +52,7 @@ from .util import file_or
51
52
 
52
53
 
53
54
  class PluginBuildingOptions(BaseModel):
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: Optional[list[FilePath]] = Field(aliases=['-s'], description=f'(plugin sets) add one or more plugin set definition files to the loaded plugin sets')
55
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())}')
56
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())}')
57
58
  plugin_signing_password: Optional[str] = Field(description='(plugin signing credentials) set the plugin signing password, or if none, prompt interactively')
@@ -97,9 +98,9 @@ class PluginDeploymentOptions(BaseModel):
97
98
  raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
98
99
 
99
100
  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 []]]
101
+ ret = [*(self.plugin_registry_layer or []), *chain.from_iterable(file_lines(file_path) for file_path in self.plugin_registry_layers or [])]
101
102
  for layer in reversed(['testing', 'production']):
102
- if getattr(self, layer, False):
103
+ if getattr(self, layer, False) and layer not in ret:
103
104
  ret.insert(0, layer)
104
105
  if ret:
105
106
  return ret
@@ -87,7 +87,7 @@ class BasePluginRegistryLayout(BaseModel, ABC):
87
87
 
88
88
  def get_plugin_registry(self) -> PluginRegistry:
89
89
  if self._plugin_registry is None:
90
- raise RuntimeError('Uninitialized plugin registry')
90
+ raise ValueError('Uninitialized plugin registry')
91
91
  return self._plugin_registry
92
92
 
93
93
  def get_type(self) -> PluginRegistryLayoutType:
@@ -194,7 +194,7 @@ class PluginRegistryLayer(BaseModel, ABC):
194
194
 
195
195
  def get_plugin_registry(self) -> PluginRegistry:
196
196
  if self._plugin_registry is None:
197
- raise RuntimeError('Uninitialized plugin registry')
197
+ raise ValueError('Uninitialized plugin registry')
198
198
  return self._plugin_registry
199
199
 
200
200
  def initialize(self, plugin_registry: PluginRegistry) -> PluginRegistryLayer:
@@ -28,6 +28,10 @@
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
+ Representations of plugin sets.
33
+ """
34
+
31
35
  # Remove in Python 3.14
32
36
  # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
33
37
  from __future__ import annotations
@@ -45,63 +49,168 @@ from pydantic import BaseModel, Field
45
49
  from .plugin import Plugin, PluginIdentifier
46
50
  from .util import BaseModelWithRoot
47
51
 
48
-
52
+ #: A type alias for the plugin set catalog kind.
49
53
  PluginSetCatalogKind = Literal['PluginSetCatalog']
50
54
 
51
55
 
52
56
  class PluginSetCatalog(BaseModelWithRoot):
57
+ """
58
+ A Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to represent a
59
+ plugin set catalog.
60
+ """
61
+ #: This object's kind.
53
62
  kind: PluginSetCatalogKind = Field(description="This object's kind")
63
+ #: A non-empty list of plugin set files.
54
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')
55
65
 
56
66
  def get_plugin_set_files(self) -> list[Path]:
67
+ """
68
+ Return this plugin set catalog's list of plugin set definition file
69
+ paths (relative to the root if not absolute).
70
+
71
+ :return: A list of plugin set definition file paths.
72
+ :rtype: list[Path]
73
+ """
57
74
  return [self.get_root().joinpath(p) for p in self.plugin_set_files]
58
75
 
59
76
 
77
+ #: A type alias for the plugin set builder type.
60
78
  PluginSetBuilderType = Literal['ant', 'maven']
61
79
 
62
80
 
63
81
  class BasePluginSetBuilder(BaseModelWithRoot, ABC):
82
+ """
83
+ An abstract Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to
84
+ represent a plugin set builder, with concrete implementations
85
+ ``AntPluginSetBuilder`` and ``MavenPluginSetBuilder``.
86
+ """
87
+
88
+ #: Pydantic definition of the ``type`` field.
64
89
  TYPE_FIELD: ClassVar[dict[str, str]] = dict(description='A plugin builder type', title='Plugin Builder Type')
90
+ #: Pydantic definition of the ``main`` field.
65
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')
92
+ #: Pydantic definition of the ``test`` field.
66
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')
67
94
 
68
95
  @abstractmethod
69
96
  def build_plugin(self, plugin_id: PluginIdentifier, keystore_path: Path, keystore_alias: str, keystore_password=None) -> tuple[Path, Plugin]:
70
- pass
97
+ """
98
+ Builds the given plugin, using the given plugin signing credentials.
99
+
100
+ :param plugin_id: A plugin identifier.
101
+ :type plugin_id: PluginIdentifier
102
+ :param keystore_path: The path to the plugin signing keystore.
103
+ :type keystore_path: Path
104
+ :param keystore_alias: The signing alias to use from the plugin signing
105
+ keystore.
106
+ :type keystore_alias: str
107
+ :param keystore_password: The signing password.
108
+ :type keystore_password: Any
109
+ :return: A tuple of the plugin JAR path and the corresponding ``Plugin``
110
+ object.
111
+ :rtype: tuple[Path, Plugin]
112
+ """
113
+ pass # FIXME: typing of keystore_password
71
114
 
72
115
  def get_main(self) -> Path:
116
+ """
117
+ Returns this plugin set builder's main code path (relative to the root
118
+ if not absolute).
119
+
120
+ :return: This plugin set's main code path.
121
+ :rtype: Path
122
+ :raises ValueError: If this object is not properly initialized.
123
+ """
73
124
  return self.get_root().joinpath(self._get_main())
74
125
 
75
126
  def get_test(self) -> Path:
127
+ """
128
+ Returns this plugin set builder's unit test path (relative to the root
129
+ if not absolute).
130
+
131
+ :return: This plugin set's unit test path.
132
+ :rtype: Path
133
+ :raises ValueError: If this object is not properly initialized.
134
+ """
76
135
  return self.get_root().joinpath(self._get_test())
77
136
 
78
137
  def get_type(self) -> PluginSetBuilderType:
138
+ """
139
+ Returns this plugin set builder's type.
140
+
141
+ :return: This plugin set builder's type.
142
+ :rtype: PluginSetBuilderType
143
+ """
79
144
  return getattr(self, 'type')
80
145
 
81
146
  def has_plugin(self, plugin_id: PluginIdentifier) -> bool:
147
+ """
148
+ Determines if the given plugin identifier represents a plugin that is
149
+ present in the plugin set.
150
+
151
+ :param plugin_id: A plugin identifier.
152
+ :type plugin_id: PluginIdentifier
153
+ :return: Whether the plugin is present in the plugin set.
154
+ :rtype: bool
155
+ """
82
156
  return self._plugin_path(plugin_id).is_file()
83
157
 
84
158
  def make_plugin(self, plugin_id: PluginIdentifier) -> Plugin:
159
+ """
160
+ Makes a ``Plugin`` object from the given plugin identifier.
161
+
162
+ :param plugin_id: A plugin identifier.
163
+ :type plugin_id: PluginIdentifier
164
+ :return: The corresponding ``Plugin`` object.
165
+ :rtype: Plugin
166
+ """
85
167
  return Plugin.from_path(self._plugin_path(plugin_id))
86
168
 
87
169
  def _get_main(self) -> str:
170
+ """
171
+ Returns the concrete implementation's ``main`` field.
172
+
173
+ :return: The ``main`` field.
174
+ :rtype: str
175
+ """
88
176
  return getattr(self, 'main')
89
177
 
90
178
  def _get_test(self) -> str:
179
+ """
180
+ Returns the concrete implementation's ``test`` field.
181
+
182
+ :return: The ``test`` field.
183
+ :rtype: str
184
+ """
91
185
  return getattr(self, 'test')
92
186
 
93
187
  def _plugin_path(self, plugin_id: PluginIdentifier) -> Path:
188
+ """
189
+ Returns the path of the plugin file for the given plugin identifier
190
+ relative to the plugin set's main code path.
191
+
192
+ :param plugin_id: A plugin identifier.
193
+ :type plugin_id: PluginIdentifier
194
+ :return: The plugin file.
195
+ :rtype: Path
196
+ """
94
197
  return self.get_main().joinpath(Plugin.id_to_file(plugin_id))
95
198
 
96
199
 
97
200
  class AntPluginSetBuilder(BasePluginSetBuilder):
201
+ #: Default value for the ``main`` field.
98
202
  DEFAULT_MAIN: ClassVar[str] = 'plugins/src'
203
+ #: Default value for the ``test`` field.
99
204
  DEFAULT_TEST: ClassVar[str] = 'plugins/test/src'
100
205
 
206
+ #: This plugin set builder's type.
101
207
  type: Literal['ant'] = Field(**BasePluginSetBuilder.TYPE_FIELD)
208
+ #: This plugin set builder's main code path.
102
209
  main: Optional[str] = Field(DEFAULT_MAIN, **BasePluginSetBuilder.MAIN_FIELD)
210
+ #: This plugin set builder's unit test path.
103
211
  test: Optional[str] = Field(DEFAULT_TEST, **BasePluginSetBuilder.TEST_FIELD)
104
212
 
213
+ #: An internal flag to remember if a build has occurred.
105
214
  _built: bool
106
215
 
107
216
  def build_plugin(self, plugin_id: PluginIdentifier, keystore_path: Path, keystore_alias: str, keystore_password=None) -> tuple[Path, Plugin]:
@@ -118,6 +227,9 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
118
227
  self._built = False
119
228
 
120
229
  def _big_build(self) -> None:
230
+ """
231
+ Optionally performs the "big build".
232
+ """
121
233
  if not self._built:
122
234
  # Do build
123
235
  subprocess.run('ant load-plugins',
@@ -125,10 +237,26 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
125
237
  self._built = True
126
238
 
127
239
  def _little_build(self, plugin_id: PluginIdentifier, keystore_path: Path, keystore_alias: str, keystore_password: str=None) -> tuple[Path, Plugin]:
240
+ """
241
+ Performs the "little build" of the given plugin.
242
+
243
+ :param plugin_id: A plugin identifier.
244
+ :type plugin_id: PluginIdentifier
245
+ :param keystore_path: The path to the plugin signing keystore.
246
+ :type keystore_path: Path
247
+ :param keystore_alias: The signing alias to use from the plugin signing
248
+ keystore.
249
+ :type keystore_alias: str
250
+ :param keystore_password: The signing password.
251
+ :type keystore_password: str
252
+ :return: A tuple of the plugin JAR path and the corresponding ``Plugin``
253
+ object.
254
+ :rtype: tuple[Path, Plugin]
255
+ """
128
256
  orig_plugin = None
129
257
  cur_id = plugin_id
130
258
  # Get all directories for jarplugin -d
131
- dirs = list()
259
+ dirs = []
132
260
  while cur_id is not None:
133
261
  cur_plugin = self.make_plugin(cur_id)
134
262
  orig_plugin = orig_plugin or cur_plugin
@@ -211,7 +339,7 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
211
339
  def _little_build(self, plugin_id: PluginIdentifier) -> tuple[Path, Plugin]:
212
340
  jar_path = self.get_root().joinpath('target', 'pluginjars', f'{plugin_id}.jar')
213
341
  if not jar_path.is_file():
214
- raise Exception(f'{plugin_id}: built JAR not found: {jar_path!s}')
342
+ raise FileNotFoundError(str(jar_path))
215
343
  return jar_path, Plugin.from_jar(jar_path)
216
344
 
217
345
  def _sanitize(self, called_process_error: subprocess.CalledProcessError) -> subprocess.CalledProcessError:
@@ -52,7 +52,7 @@ class BaseModelWithRoot(BaseModel):
52
52
 
53
53
  def get_root(self) -> Path:
54
54
  if self._root is None:
55
- raise RuntimeError('Uninitialized root')
55
+ raise ValueError('Uninitialized root')
56
56
  return self._root
57
57
 
58
58
  def initialize(self, root: PathOrStr) -> BaseModelWithRoot:
@@ -28,8 +28,13 @@
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
+ from pathlib import Path
32
+
31
33
  from . import PydanticTestCase
32
- from lockss.turtles.plugin_set import AntPluginSetBuilder, MavenPluginSetBuilder, PluginSet, PluginSetBuilder
34
+ from lockss.turtles.plugin_set import AntPluginSetBuilder, MavenPluginSetBuilder, PluginSet, PluginSetBuilder, PluginSetBuilderType
35
+
36
+
37
+ ROOT: Path = Path('.').absolute()
33
38
 
34
39
 
35
40
  class TestPluginSet(PydanticTestCase):
@@ -46,17 +51,27 @@ class TestPluginSet(PydanticTestCase):
46
51
  PluginSet(kind='PluginSet', id='myset', name='My Set', builder=AntPluginSetBuilder(type='ant'))
47
52
  PluginSet(kind='PluginSet', id='myset', name='My Set', builder=MavenPluginSetBuilder(type='maven'))
48
53
 
54
+
49
55
  class TestPluginSetBuilder(PydanticTestCase):
50
56
 
51
- def doTestPluginSetBuilder(self, Pbsc: type(PluginSetBuilder), typ: str, def_main: str, def_test: str) -> None:
57
+ def doTestPluginSetBuilder(self, Pbsc: type(PluginSetBuilder), typ: PluginSetBuilderType, def_main: Path, def_test: Path) -> None:
52
58
  self.assertPydanticMissing(lambda: Pbsc(), 'type')
53
59
  self.assertPydanticLiteralError(lambda: Pbsc(type='BADTYPE'), 'type', typ)
54
60
  self.assertIsNone(Pbsc(type=typ)._root)
55
- self.assertEqual(Pbsc(type=typ)._get_main(), def_main)
56
- self.assertEqual(Pbsc(type=typ, main='mymain')._get_main(), 'mymain')
57
- self.assertEqual(Pbsc(type=typ)._get_test(), def_test)
58
- self.assertEqual(Pbsc(type=typ, test='mytest')._get_test(), 'mytest')
61
+ with self.assertRaises(ValueError):
62
+ Pbsc(type=typ).get_root()
63
+ self.assertEqual(Pbsc(type=typ).initialize(ROOT).get_root(), ROOT)
64
+ with self.assertRaises(ValueError):
65
+ Pbsc(type=typ, main='anything').get_main()
66
+ self.assertEqual(Pbsc(type=typ).initialize(ROOT).get_main(), def_main)
67
+ self.assertEqual(Pbsc(type=typ, main='mymain').initialize(ROOT).get_main(), ROOT.joinpath('mymain'))
68
+ self.assertEqual(Pbsc(type=typ, main='/opt/main').initialize(ROOT).get_main(), Path('/opt/main'))
69
+ with self.assertRaises(ValueError):
70
+ Pbsc(type=typ, test='anything').get_test()
71
+ self.assertEqual(Pbsc(type=typ).initialize(ROOT).get_test(), def_test)
72
+ self.assertEqual(Pbsc(type=typ, test='mytest').initialize(ROOT).get_test(), ROOT.joinpath('mytest'))
73
+ self.assertEqual(Pbsc(type=typ, test='/opt/test').initialize(ROOT).get_test(), Path('/opt/test'))
59
74
 
60
75
  def testPluginSetBuilder(self):
61
- self.doTestPluginSetBuilder(AntPluginSetBuilder, 'ant', 'plugins/src', 'plugins/test/src')
62
- self.doTestPluginSetBuilder(MavenPluginSetBuilder, 'maven', 'src/main/java', 'src/test/java')
76
+ self.doTestPluginSetBuilder(AntPluginSetBuilder, 'ant', ROOT.joinpath('plugins/src'), ROOT.joinpath('plugins/test/src'))
77
+ self.doTestPluginSetBuilder(MavenPluginSetBuilder, 'maven', ROOT.joinpath('src/main/java'), ROOT.joinpath('src/test/java'))