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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- # Copyright (c) 2000-2023, Board of Trustees of Leland Stanford Jr. University
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,225 +28,653 @@
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
- import importlib.resources
32
- from pathlib import Path
33
- import subprocess
34
-
35
- from lockss.turtles.plugin import Plugin
36
- import lockss.turtles.resources
37
- from lockss.turtles.util import _load_and_validate, _path
38
-
39
-
40
- class PluginRegistryCatalog(object):
41
-
42
- PLUGIN_REGISTRY_CATALOG_SCHEMA = 'plugin-registry-catalog-schema.json'
31
+ """
32
+ Module to represent plugin registries and plugin registry catalogs.
33
+ """
43
34
 
44
- @staticmethod
45
- def from_path(plugin_registry_catalog_path):
46
- plugin_registry_catalog_path = _path(plugin_registry_catalog_path)
47
- with importlib.resources.path(lockss.turtles.resources, PluginRegistryCatalog.PLUGIN_REGISTRY_CATALOG_SCHEMA) as plugin_registry_catalog_schema_path:
48
- parsed = _load_and_validate(plugin_registry_catalog_schema_path, plugin_registry_catalog_path)
49
- return PluginRegistryCatalog(parsed)
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
36
+ from __future__ import annotations
50
37
 
51
- def __init__(self, parsed):
52
- super().__init__()
53
- self._parsed = parsed
54
-
55
- def get_plugin_registry_files(self):
56
- return self._parsed['plugin-registry-files']
57
-
58
-
59
- class PluginRegistry(object):
60
-
61
- PLUGIN_REGISTRY_SCHEMA = 'plugin-registry-schema.json'
62
-
63
- @staticmethod
64
- def from_path(plugin_registry_file_path):
65
- plugin_registry_file_path = _path(plugin_registry_file_path)
66
- with importlib.resources.path(lockss.turtles.resources, PluginRegistry.PLUGIN_REGISTRY_SCHEMA) as plugin_registry_schema_path:
67
- lst = _load_and_validate(plugin_registry_schema_path, plugin_registry_file_path, multiple=True)
68
- return [PluginRegistry._from_obj(parsed, plugin_registry_file_path) for parsed in lst]
38
+ from abc import ABC, abstractmethod
39
+ from pathlib import Path
40
+ import subprocess
41
+ from typing import Annotated, Any, ClassVar, Literal, Optional, Union
42
+
43
+ from lockss.pybasic.errorutil import InternalError
44
+ from lockss.pybasic.fileutil import path
45
+ from pydantic import BaseModel, Field
46
+
47
+ from .plugin import Plugin, PluginIdentifier
48
+ from .util import BaseModelWithRoot
49
+
50
+
51
+ #: A type alias for the plugin registry catalog kind.
52
+ PluginRegistryCatalogKind = Literal['PluginRegistryCatalog']
53
+
54
+
55
+ class PluginRegistryCatalog(BaseModelWithRoot):
56
+ """
57
+ A Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to represent a
58
+ plugin registry catalog.
59
+ """
60
+
61
+ #: This object's kind.
62
+ kind: PluginRegistryCatalogKind = Field(title='Kind',
63
+ description="This object's kind")
64
+
65
+ #: A non-empty list of plugin registry files.
66
+ plugin_registry_files: list[str] = Field(alias='plugin-registry-files',
67
+ min_length=1,
68
+ title='Plugin Registry Files',
69
+ description="A non-empty list of plugin registry files")
70
+
71
+ def get_plugin_registry_files(self) -> list[Path]:
72
+ """
73
+ Returns the list of plugin registry files in this catalog, relative to
74
+ the plugin registry catalog file if applicable.
75
+
76
+ :return: A non-null list of plugin registry file paths.
77
+ :rtype: list[Path]
78
+ """
79
+ return [self.get_root().joinpath(pstr) for pstr in self.plugin_registry_files]
80
+
81
+
82
+ #: A type alias for the two plugin registry layout types.
83
+ PluginRegistryLayoutType = Literal['directory', 'rcs']
84
+
85
+
86
+ #: A type alias for the three plugin registry layout file naming conventions.
87
+ PluginRegistryLayoutFileNamingConvention = Literal['abbreviated', 'identifier', 'underscore']
88
+
89
+
90
+ class BasePluginRegistryLayout(BaseModel, ABC):
91
+ """
92
+ An abstract Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to
93
+ represent a plugin registry layout, with concrete implementations
94
+ ``DirectoryPluginRegistryLayout`` and ``RcsPluginRegistryLayout``.
95
+ """
96
+
97
+ #: Pydantic definition of the ``type`` field.
98
+ TYPE_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Registry Layout Type',
99
+ description='A plugin registry layout type')
100
+
101
+ #: Default file naming convention.
102
+ FILE_NAMING_CONVENTION_DEFAULT: ClassVar[PluginRegistryLayoutFileNamingConvention] = 'identifier'
103
+
104
+ #: Pydantic definition of the ``file_naming_convention`` field.
105
+ FILE_NAMING_CONVENTION_FIELD: ClassVar[dict[str, str]] = dict(alias='file-naming-convention',
106
+ title='Plugin Registry Layout File Naming Convention',
107
+ description='A file naming convention for the plugin registry layout')
108
+
109
+ #: Internal backreference to the enclosing plugin registry; see ``initialize``.
110
+ _plugin_registry: Optional[PluginRegistry]
111
+
112
+ def deploy_plugin(self,
113
+ plugin_id: PluginIdentifier,
114
+ layer: PluginRegistryLayer,
115
+ src_path: Path,
116
+ interactive: bool=False) -> Optional[tuple[Path, Plugin]]:
117
+ """
118
+ Deploys the given plugin to the target plugin registry layer according to
119
+ this plugin registry layout's file naming convention.
120
+
121
+ See ``_copy_jar``.
122
+
123
+ :param plugin_id: A plugin identifier.
124
+ :type plugin_id: PluginIdentifier
125
+ :param layer: A plugin registry layer.
126
+ :type layer: PluginRegistryLayer
127
+ :param src_path: The path of the plugin JAR.
128
+ :type src_path: Path
129
+ :param interactive: If False (the default), no interactive confirmation
130
+ will occur. If True, and the given plugin is being
131
+ deployed to the target layer for the very first
132
+ time, the user will be prompted interactively to
133
+ confirm.
134
+ :type interactive: bool
135
+ :return: A tuple of the path of the deployed JAR and a Plugin object
136
+ instantiated from the source JAR, or None if the user was
137
+ prompted for confirmation and responded negatively.
138
+ :rtype: Optional[tuple[Path, Plugin]]
139
+ """
140
+ src_path = path(src_path) # in case it's a string
141
+ dst_path = self._get_dstpath(plugin_id, layer)
142
+ if not self._proceed_copy(src_path, dst_path, layer, interactive=interactive):
143
+ return None
144
+ self._copy_jar(src_path, dst_path)
145
+ return dst_path, Plugin.from_jar(src_path)
146
+
147
+ # Believed to be abandoned:
148
+
149
+ # def get_file_for(self,
150
+ # plugin_id,
151
+ # layer: PluginRegistryLayer) -> Optional[Path]:
152
+ # """
153
+ #
154
+ # :param plugin_id:
155
+ # :type plugin_id:
156
+ # :param layer:
157
+ # :type layer:
158
+ # :return:
159
+ # :rtype:
160
+ # """
161
+ # jar_path = self._get_dstpath(plugin_id, layer)
162
+ # return jar_path if jar_path.is_file() else None
163
+
164
+ def get_file_naming_convention(self) -> PluginRegistryLayoutFileNamingConvention:
165
+ """
166
+ Returns the concrete implementation's ``file_naming`convention`` field.
167
+
168
+ :return: This plugin registry layout's file naming convention.
169
+ :rtype: PluginRegistryLayoutFileNamingConvention
170
+ """
171
+ return getattr(self, 'file_naming_convention')
172
+
173
+ def get_plugin_registry(self) -> PluginRegistry:
174
+ """
175
+ Returns the enclosing plugin registry.
176
+
177
+ See ``initialize``.
178
+
179
+ :return: The enclosing plugin registry.
180
+ :rtype: PluginRegistry
181
+ :raises ValueError: If ``initialize`` was not called on the object.
182
+ """
183
+ if self._plugin_registry is None:
184
+ raise ValueError('Uninitialized plugin registry')
185
+ return self._plugin_registry
69
186
 
70
- @staticmethod
71
- def _from_obj(parsed, plugin_registry_file_path):
72
- typ = parsed['layout']['type']
73
- if typ == DirectoryPluginRegistry.LAYOUT:
74
- return DirectoryPluginRegistry(parsed)
75
- elif typ == RcsPluginRegistry.LAYOUT:
76
- return RcsPluginRegistry(parsed)
187
+ def get_type(self) -> PluginRegistryLayoutType:
188
+ """
189
+ Returns the concrete implementation's ``type`` field.
190
+
191
+ :return: This plugin registry layout's type.
192
+ :rtype: PluginRegistryLayoutType
193
+ """
194
+ return getattr(self, 'type')
195
+
196
+ def initialize(self,
197
+ plugin_registry: PluginRegistry) -> BasePluginRegistryLayout:
198
+ """
199
+ Initializes the plugin registry backreference. Mandatory call after
200
+ object creation.
201
+
202
+ :param plugin_registry: The enclosing plugin registry.
203
+ :type plugin_registry: PluginRegistry
204
+ :return: This object (for chaining).
205
+ :rtype: BasePluginRegistryLayout
206
+ """
207
+ self._plugin_registry = plugin_registry
208
+ return self
209
+
210
+ def model_post_init(self,
211
+ context: Any) -> None:
212
+ """
213
+ Pydantic post-initialization method, to create the ``_plugin_registry``
214
+ backreference.
215
+
216
+ See ``initialize``.
217
+
218
+ :param context: The Pydantic context.
219
+ :type context: Any
220
+ """
221
+ super().model_post_init(context)
222
+ self._plugin_registry = None
223
+
224
+ @abstractmethod
225
+ def _copy_jar(self,
226
+ src_path: Path,
227
+ dst_path: Path) -> None:
228
+ """
229
+ Implementation-specific copy of the plugin JAR from a source path to its
230
+ intended deployed path.
231
+
232
+ :param src_path: The path of the plugin JAR to be deployed.
233
+ :type src_path: Path
234
+ :param dst_path: The intended path of the deployed JAR.
235
+ :type dst_path: Path
236
+ """
237
+ pass
238
+
239
+ def _get_dstfile(self,
240
+ plugin_id: PluginIdentifier) -> str:
241
+ """
242
+ Computes the destination file name (not path) based on this layout's
243
+ file naming convention.
244
+
245
+ Implemented here because common to both concrete implementations.
246
+
247
+ :param plugin_id: A plugin identifier.
248
+ :type plugin_id: PluginIdentifier
249
+ :return: A file name string consistent with the file naming convention.
250
+ For a plugin identifier ``org.myproject.plugin.MyPlugin``, the
251
+ result is ``MyPlugin.jar`` for ``abbreviated``,
252
+ ``org.myproject.plugin.MyPlugin.jar`` for ``identifier`` and
253
+ ``org_myproject_plugin_MyPlugin.jar`` for ``underscore``.
254
+ :rtype: str
255
+ """
256
+ if (conv := self.get_file_naming_convention()) == 'abbreviated':
257
+ return f'{plugin_id.split(".")[-1]}.jar'
258
+ elif conv == 'identifier':
259
+ return f'{plugin_id}.jar'
260
+ elif conv == 'underscore':
261
+ return f'{plugin_id.replace(".", "_")}.jar'
77
262
  else:
78
- raise RuntimeError(f'{plugin_registry_file_path!s}: unknown layout type: {typ}')
263
+ raise InternalError()
264
+
265
+ def _get_dstpath(self,
266
+ plugin_id: PluginIdentifier,
267
+ layer: PluginRegistryLayer) -> Path:
268
+ """
269
+ Computes the destination path for the given plugin being deployed to the
270
+ target layer, using the layout's file naming convention.
271
+
272
+ :param plugin_id: A plugin identifier.
273
+ :type plugin_id: PluginIdentifier
274
+ :param layer: A target plugin registry layer.
275
+ :type layer: PluginRegistryLayer
276
+ :return: The would-be destination of the deployed JAR.
277
+ :rtype: Path
278
+ """
279
+ return layer.get_path().joinpath(self._get_dstfile(plugin_id))
280
+
281
+ def _proceed_copy(self,
282
+ src_path: Path,
283
+ dst_path: Path,
284
+ layer: PluginRegistryLayer,
285
+ interactive: bool=False) -> bool:
286
+ """
287
+ Determines whether the copy of the JAR should proceed.
288
+
289
+ :param src_path: The path of the JAR being deployed.
290
+ :type src_path: Path
291
+ :param dst_path: The path of the intended deployed JAR.
292
+ :type dst_path: Path
293
+ :param layer: The target plugin registry layer.
294
+ :type layer: PluginRegistryLayer
295
+ :param interactive: Whether interactive prompts are allowed (False by
296
+ default)
297
+ :type interactive: bool
298
+ :return: True, unless the destination file does not exist yet, the
299
+ interactive flag is True, and the user does not respond
300
+ positively to the confirmation prompt.
301
+ :rtype: bool
302
+ """
303
+ if not dst_path.exists():
304
+ if interactive:
305
+ i = input(f'{dst_path} does not exist in {self.get_plugin_registry().get_id()}:{layer.get_id()} ({layer.get_name()}); create it (y/n)? [n] ').lower() or 'n'
306
+ if i != 'y':
307
+ return False
308
+ return True
79
309
 
80
- def __init__(self, parsed):
81
- super().__init__()
82
- self._parsed = parsed
83
310
 
84
- def get_id(self):
85
- return self._parsed['id']
311
+ class DirectoryPluginRegistryLayout(BasePluginRegistryLayout):
312
+ """
313
+ A plugin registry layout that keeps plugin JARs in a single directory.
314
+ """
86
315
 
87
- def get_layer(self, layer_id):
88
- for layer in self.get_layers():
89
- if layer.get_id() == layer_id:
90
- return layer
91
- return None
316
+ #: This plugin registry layout's type.
317
+ type: Literal['directory'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
92
318
 
93
- def get_layer_ids(self):
94
- return [layer.get_id() for layer in self.get_layers()]
319
+ #: This plugin registry layout's file naming convention.
320
+ file_naming_convention: PluginRegistryLayoutFileNamingConvention = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT,
321
+ **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
95
322
 
96
- def get_layers(self):
97
- return [self._make_layer(layer_elem) for layer_elem in self._parsed['layers']]
323
+ def _copy_jar(self,
324
+ src_path: Path,
325
+ dst_path: Path) -> None:
326
+ """
327
+ Copies the plugin JAR from a source path to its intended deployed path.
98
328
 
99
- def get_layout_type(self):
100
- return self._parsed['layout']['type']
329
+ Additionally, if SELinux is enabled, sets the type of the security
330
+ context of the deployed path to ``httpd_sys_content_t``.
101
331
 
102
- def get_name(self):
103
- return self._parsed['name']
332
+ :param src_path: The path of the plugin JAR to be deployed.
333
+ :type src_path: Path
334
+ :param dst_path: The intended path of the deployed JAR.
335
+ :type dst_path: Path
336
+ :raises subprocess.CalledProcessError: If an invoked subprocess fails.
337
+ """
338
+ dst_dir, dst_file = dst_path.parent, dst_path.name
339
+ subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=dst_dir)
340
+ if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
341
+ cmd = ['chcon', '-t', 'httpd_sys_content_t', dst_file]
342
+ subprocess.run(cmd, check=True, cwd=dst_dir)
104
343
 
105
- def get_plugin_identifiers(self):
106
- return self._parsed['plugin-identifiers']
107
344
 
108
- def has_plugin(self, plugin_id):
109
- return plugin_id in self.get_plugin_identifiers()
345
+ class RcsPluginRegistryLayout(DirectoryPluginRegistryLayout):
346
+ """
347
+ A plugin registry layout that is like ``DirectoryPluginRegistryLayout`` but
348
+ also uses `GNU RCS <https://www.gnu.org/software/rcs/>`_ to keep a record of
349
+ successive plugin versions in an ``RCS`` subdirectory.
350
+ """
110
351
 
111
- def _make_layer(self, parsed):
112
- raise NotImplementedError('_make_layer')
352
+ #: This plugin registry layout's type. Shadows that of ``DirectoryPluginRegistryLayout`` due to inheritance.
353
+ type: Literal['rcs'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
113
354
 
355
+ # Believed to be unnecessary:
114
356
 
115
- class PluginRegistryLayer(object):
357
+ #file_naming_convention: Optional[PluginRegistryLayoutFileNamingConvention] = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT, **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
116
358
 
117
- PRODUCTION = 'production'
359
+ def _copy_jar(self,
360
+ src_path: Path,
361
+ dst_path: Path) -> None:
362
+ """
363
+ Copies the plugin JAR from a source path to its intended deployed path.
118
364
 
119
- TESTING = 'testing'
365
+ Does ``co -l`` if applicable, does the same copy as the parent
366
+ ``DirectoryPluginRegistryLayout._copy_jar``, then does ``ci -u``.
120
367
 
121
- def __init__(self, plugin_registry, parsed):
122
- super().__init__()
123
- self._parsed = parsed
124
- self._plugin_registry = plugin_registry
368
+ :param src_path: The path of the plugin JAR to be deployed.
369
+ :type src_path: Path
370
+ :param dst_path: The intended path of the deployed JAR.
371
+ :type dst_path: Path
372
+ :raises subprocess.CalledProcessError: If an invoked subprocess fails.
373
+ """
374
+ dst_dir, dst_file = dst_path.parent, dst_path.name
375
+ plugin = Plugin.from_jar(src_path)
376
+ rcs_path = dst_dir.joinpath('RCS', f'{dst_file},v')
377
+ # Maybe do co -l before the parent's copy
378
+ if dst_path.exists() and rcs_path.is_file():
379
+ cmd = ['co', '-l', dst_file]
380
+ subprocess.run(cmd, check=True, cwd=dst_dir)
381
+ # Do the parent's copy
382
+ super()._copy_jar(src_path, dst_path)
383
+ # Do ci -u after the parent's copy
384
+ cmd = ['ci', '-u', f'-mVersion {plugin.get_version()}']
385
+ if not rcs_path.is_file():
386
+ cmd.append(f'-t-{plugin.get_name()}')
387
+ cmd.append(dst_file)
388
+ subprocess.run(cmd, check=True, cwd=dst_dir)
389
+
390
+
391
+ #: A type alias for plugin registry layouts, which is the union of
392
+ #: ``DirectoryPluginRegistryLayout`` and ``RcsPluginRegistryLayout`` using
393
+ #: ``type`` as the discriminator field.
394
+ PluginRegistryLayout = Annotated[Union[DirectoryPluginRegistryLayout, RcsPluginRegistryLayout], Field(discriminator='type')]
395
+
396
+
397
+ #: A type alias for plugin registry layer identifiers.
398
+ PluginRegistryLayerIdentifier = str
399
+
400
+
401
+ class PluginRegistryLayer(BaseModel):
402
+ """
403
+ A Pydantic model to represent a plugin registry layer.
404
+ """
405
+
406
+ #: This plugin registry layer's identifier.
407
+ id: PluginRegistryLayerIdentifier = Field(title='Plugin Registry Layer Identifier',
408
+ description='An identifier for the plugin registry layer')
409
+
410
+ #: This plugin registry layer's name.
411
+ name: str = Field(title='Plugin Registry Layer Name',
412
+ description='A name for the plugin registry layer')
413
+
414
+ #: This plugin registry layer's path.
415
+ path: str = Field(title='Plugin Registry Layer Path',
416
+ description='A root path for the plugin registry layer')
417
+
418
+ #: Internal backreference to the enclosing plugin registry; see ``initialize``.
419
+ _plugin_registry: Optional[PluginRegistry]
420
+
421
+ def deploy_plugin(self,
422
+ plugin_id: PluginIdentifier,
423
+ src_path: Path,
424
+ interactive: bool=False) -> Optional[tuple[Path, Plugin]]:
425
+ """
426
+ Deploys the given plugin to this plugin registry layer according to
427
+ this plugin registry layout's file naming convention.
428
+
429
+ :param plugin_id: A plugin identifier.
430
+ :type plugin_id: PluginIdentifier
431
+ :param src_path: The path of the plugin JAR.
432
+ :type src_path: Path
433
+ :param interactive: If False (the default), no interactive confirmation
434
+ will occur. If True, and the given plugin is being
435
+ deployed to this layer for the very first time, the
436
+ user will be prompted interactively to confirm.
437
+ :type interactive: bool
438
+ :return: A tuple of the path of the deployed JAR and a Plugin object
439
+ instantiated from the source JAR, or None if the user was
440
+ prompted for confirmation and responded negatively.
441
+ :rtype: Optional[tuple[Path, Plugin]]
442
+ """
443
+ return self.get_plugin_registry().get_layout().deploy_plugin(plugin_id, self, src_path, interactive)
444
+
445
+ def get_id(self) -> PluginRegistryLayerIdentifier:
446
+ """
447
+ Returns this plugin registry layer's identifier.
448
+
449
+ :return: This plugin registry layer's identifier.
450
+ :rtype: PluginRegistryLayerIdentifier
451
+ """
452
+ return self.id
453
+
454
+ def get_jars(self) -> list[Path]:
455
+ """
456
+ Returns the list of this plugin registry layer's JAR file paths.
457
+
458
+ :return: A sorted list of JAR file paths.
459
+ :rtype: list[Path]
460
+ """
461
+ # FIXME Strictly speaking this should be in the layout
462
+ return sorted(self.get_path().glob('*.jar'))
125
463
 
126
- # Returns (dst_path, plugin)
127
- def deploy_plugin(self, plugin_id, jar_path, interactive=False):
128
- raise NotImplementedError('deploy_plugin')
464
+ def get_name(self) -> str:
465
+ """
466
+ Returns this plugin registry layer's name.
129
467
 
130
- def get_file_for(self, plugin_id):
131
- raise NotImplementedError('get_file_for')
468
+ :return: This plugin registry layer's name.
469
+ :rtype: str
470
+ """
471
+ return self.name
132
472
 
133
- def get_id(self):
134
- return self._parsed['id']
473
+ def get_path(self) -> Path:
474
+ """
475
+ Returns this plugin registry layer's path.
135
476
 
136
- def get_jars(self):
137
- raise NotImplementedError('get_jars')
477
+ :return: This plugin registry layer's path.
478
+ :rtype: Path
479
+ """
480
+ return self.get_plugin_registry().get_root().joinpath(self.path)
138
481
 
139
- def get_name(self):
140
- return self._parsed['name']
482
+ def get_plugin_registry(self) -> PluginRegistry:
483
+ """
484
+ Returns the enclosing plugin registry.
141
485
 
142
- def get_path(self):
143
- return _path(self._parsed['path'])
486
+ See ``initialize``.
144
487
 
145
- def get_plugin_registry(self):
488
+ :return: The enclosing plugin registry.
489
+ :rtype: PluginRegistry
490
+ :raises ValueError: If ``initialize`` was not called on the object.
491
+ """
492
+ if self._plugin_registry is None:
493
+ raise ValueError('Uninitialized plugin registry')
146
494
  return self._plugin_registry
147
495
 
496
+ def initialize(self,
497
+ plugin_registry: PluginRegistry) -> PluginRegistryLayer:
498
+ """
499
+ Initializes the plugin registry backreference. Mandatory call after
500
+ object creation.
501
+
502
+ :param plugin_registry: The enclosing plugin registry.
503
+ :type plugin_registry: PluginRegistry
504
+ :return: This object (for chaining).
505
+ :rtype: BasePluginRegistryLayout
506
+ """
507
+ self._plugin_registry = plugin_registry
508
+ return self
509
+
510
+ def model_post_init(self,
511
+ context: Any) -> None:
512
+ """
513
+ Pydantic post-initialization method, to create the ``_plugin_registry``
514
+ backreference.
515
+
516
+ See ``initialize``.
517
+
518
+ :param context: The Pydantic context.
519
+ :type context: Any
520
+ """
521
+ super().model_post_init(context)
522
+ self._plugin_registry = None
523
+
524
+
525
+ #: A type alias for the plugin registry kind.
526
+ PluginRegistryKind = Literal['PluginRegistry']
527
+
528
+
529
+ #: A type alias for plugin registry identifiers.
530
+ PluginRegistryIdentifier = str
531
+
532
+
533
+ class PluginRegistry(BaseModelWithRoot):
534
+ """
535
+ A Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to represent a
536
+ plugin registry.
537
+ """
538
+
539
+ #: This object's kind.
540
+ kind: PluginRegistryKind = Field(title='Kind',
541
+ description="This object's kind")
542
+
543
+ #: This plugin registry's identifier.
544
+ id: PluginRegistryIdentifier = Field(title='Plugin Registry Identifier',
545
+ description='An identifier for the plugin set')
546
+
547
+ #: This plugin registry's name.
548
+ name: str = Field(title='Plugin Registry Name',
549
+ description='A name for the plugin set')
550
+
551
+ #: This plugin registry's layout.
552
+ layout: PluginRegistryLayout = Field(title='Plugin Registry Layout',
553
+ description='A layout for the plugin registry')
554
+
555
+ #: This plugin registry's layers.
556
+ layers: list[PluginRegistryLayer] = Field(min_length=1,
557
+ title='Plugin Registry Layers',
558
+ description="A non-empty list of plugin registry layers")
559
+
560
+ #: The plugin identifiers in this registry.
561
+ plugin_identifiers: list[PluginIdentifier] = Field(alias='plugin-identifiers',
562
+ min_length=1,
563
+ title='Plugin Identifiers',
564
+ description="A non-empty list of plugin identifiers")
565
+
566
+ #: The suppressed plugin identifiers, formerly in this plugin registry.
567
+ suppressed_plugin_identifiers: list[PluginIdentifier] = Field([],
568
+ alias='suppressed-plugin-identifiers',
569
+ title='Suppressed Plugin Identifiers',
570
+ description="A list of suppressed plugin identifiers")
571
+
572
+ def get_id(self) -> PluginRegistryIdentifier:
573
+ """
574
+ Returns this plugin registry's identifier.
575
+
576
+ :return: This plugin registry's identifier.
577
+ :rtype: PluginRegistryIdentifier
578
+ """
579
+ return self.id
580
+
581
+ def get_layer(self,
582
+ layer_id: PluginRegistryLayerIdentifier) -> Optional[PluginRegistryLayer]:
583
+ """
584
+ Returns the plugin registry layer with the given identifier.
585
+
586
+ :param layer_id: A plugin registry layer identifier.
587
+ :type layer_id: PluginRegistryLayerIdentifier
588
+ :return: The plugin registry layer from this registry with the given
589
+ identifier, or None if there is no such layer.
590
+ :rtype: Optional[PluginRegistryLayer]
591
+ """
592
+ for layer in self.get_layers():
593
+ if layer.get_id() == layer_id:
594
+ return layer
595
+ return None
148
596
 
149
- class DirectoryPluginRegistry(PluginRegistry):
150
-
151
- LAYOUT = 'directory'
152
-
153
- FILE_NAMING_CONVENTION_ABBREVIATED = 'abbreviated'
154
-
155
- FILE_NAMING_CONVENTION_IDENTIFIER = 'identifier'
156
-
157
- FILE_NAMING_CONVENTION_UNDERSCORE = 'underscore'
158
-
159
- DEFAULT_FILE_NAMING_CONVENTION = FILE_NAMING_CONVENTION_IDENTIFIER
160
-
161
- def __init__(self, parsed):
162
- super().__init__(parsed)
163
-
164
- def _make_layer(self, parsed):
165
- return DirectoryPluginRegistryLayer(self, parsed)
166
-
167
-
168
- class DirectoryPluginRegistryLayer(PluginRegistryLayer):
169
-
170
- def __init__(self, plugin_registry, parsed):
171
- super().__init__(plugin_registry, parsed)
172
-
173
- def deploy_plugin(self, plugin_id, src_path, interactive=False):
174
- src_path = _path(src_path) # in case it's a string
175
- dst_path = self._get_dstpath(plugin_id)
176
- if not self._proceed_copy(src_path, dst_path, interactive=interactive):
177
- return None
178
- self._copy_jar(src_path, dst_path, interactive=interactive)
179
- return (dst_path, Plugin.from_jar(src_path))
180
-
181
- def get_file_for(self, plugin_id):
182
- jar_path = self._get_dstpath(plugin_id)
183
- return jar_path if jar_path.is_file() else None
184
-
185
- def get_file_naming_convention(self):
186
- return self.get_plugin_registry()._parsed['layout'].get('file-naming-convention', DirectoryPluginRegistry.DEFAULT_FILE_NAMING_CONVENTION)
187
-
188
- def get_jars(self):
189
- return sorted(self.get_path().glob('*.jar'))
190
-
191
- def _copy_jar(self, src_path, dst_path, interactive=False):
192
- basename = dst_path.name
193
- subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=self.get_path())
194
- if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
195
- cmd = ['chcon', '-t', 'httpd_sys_content_t', basename]
196
- subprocess.run(cmd, check=True, cwd=self.get_path())
197
-
198
- def _get_dstpath(self, plugin_id):
199
- return Path(self.get_path(), self._get_dstfile(plugin_id))
200
-
201
- def _get_dstfile(self, plugin_id):
202
- conv = self.get_file_naming_convention()
203
- if conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_IDENTIFIER:
204
- return f'{plugin_id}.jar'
205
- elif conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_UNDERSCORE:
206
- return f'{plugin_id.replace(".", "_")}.jar'
207
- elif conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_ABBREVIATED:
208
- return f'{plugin_id.split(".")[-1]}.jar'
209
- else:
210
- raise RuntimeError(f'{self.get_plugin_registry().get_id()}: unknown file naming convention: {conv}')
211
-
212
- def _proceed_copy(self, src_path, dst_path, interactive=False):
213
- if not dst_path.exists():
214
- if interactive:
215
- i = input(f'{dst_path} does not exist in {self.get_plugin_registry().get_id()}:{self.get_id()} ({self.get_name()}); create it (y/n)? [n] ').lower() or 'n'
216
- if i != 'y':
217
- return False
218
- return True
219
-
220
-
221
- class RcsPluginRegistry(DirectoryPluginRegistry):
222
-
223
- LAYOUT = 'rcs'
224
-
225
- def __init__(self, parsed):
226
- super().__init__(parsed)
597
+ def get_layer_ids(self) -> list[PluginRegistryLayerIdentifier]:
598
+ """
599
+ Returns a list of all the plugin registry layer identifiers in this
600
+ registry.
227
601
 
228
- def _make_layer(self, parsed):
229
- return RcsPluginRegistryLayer(self, parsed)
602
+ :return: A list of plugin registry layer identifiers.
603
+ :rtype: list[PluginRegistryLayerIdentifier]
604
+ """
605
+ return [layer.get_id() for layer in self.get_layers()]
230
606
 
607
+ def get_layers(self) -> list[PluginRegistryLayer]:
608
+ """
609
+ Returns a list of all the plugin registry layers in this registry.
610
+
611
+ :return: A list of plugin registry layers.
612
+ :rtype: list[PluginRegistryLayer]
613
+ """
614
+ return self.layers
615
+
616
+ def get_layout(self) -> BasePluginRegistryLayout:
617
+ """
618
+ Returns this plugin registry's layout.
619
+
620
+ :return: A list of plugin registry layers.
621
+ :rtype: list[PluginRegistryLayer]
622
+ """
623
+ return self.layout
624
+
625
+ def get_name(self) -> str:
626
+ """
627
+ Returns this plugin registry's name.
628
+
629
+ :return: This plugin registry's name.
630
+ :rtype: str
631
+ """
632
+ return self.name
633
+
634
+ def get_plugin_identifiers(self) -> list[PluginIdentifier]:
635
+ """
636
+ Returns the list of plugin identifiers in this registry.
637
+
638
+ :return: The list of plugin identifiers in this registry.
639
+ :rtype: list[PluginIdentifier]
640
+ """
641
+ return self.plugin_identifiers
642
+
643
+ def get_suppressed_plugin_identifiers(self) -> list[PluginIdentifier]:
644
+ """
645
+ Returns the list of suppressed plugin identifiers in this registry.
646
+
647
+ :return: The list of suppressed plugin identifiers in this registry.
648
+ :rtype: list[PluginIdentifier]
649
+ """
650
+ return self.suppressed_plugin_identifiers
651
+
652
+ def has_plugin(self,
653
+ plugin_id: PluginIdentifier) -> bool:
654
+ """
655
+ Determines if a given plugin identifier is in this registry.
656
+
657
+ :param plugin_id: A plugin identifier.
658
+ :type plugin_id: PluginIdentifier
659
+ :return: True if and only if the given plugin identifier is in this
660
+ registry.
661
+ :rtype: bool
662
+ """
663
+ return plugin_id in self.get_plugin_identifiers()
231
664
 
232
- class RcsPluginRegistryLayer(DirectoryPluginRegistryLayer):
665
+ def model_post_init(self,
666
+ context: Any) -> None:
667
+ """
668
+ Pydantic post-initialization method to initialize the layout and all the
669
+ layers with this registry as the enclosing registry.
233
670
 
234
- def __init__(self, plugin_registry, parsed):
235
- super().__init__(plugin_registry, parsed)
671
+ See ``BasePluginRegistrLayout.initialize`` and
672
+ ``PluginRegistryLayout.initialize``.
236
673
 
237
- def _copy_jar(self, src_path, dst_path, interactive=False):
238
- basename = dst_path.name
239
- plugin = Plugin.from_jar(src_path)
240
- rcs_path = self.get_path().joinpath('RCS', f'{basename},v')
241
- # Maybe do co -l before the parent's copy
242
- if dst_path.exists() and rcs_path.is_file():
243
- cmd = ['co', '-l', basename]
244
- subprocess.run(cmd, check=True, cwd=self.get_path())
245
- # Do the parent's copy
246
- super()._copy_jar(src_path, dst_path)
247
- # Do ci -u after the parent's copy
248
- cmd = ['ci', '-u', f'-mVersion {plugin.get_version()}']
249
- if not rcs_path.is_file():
250
- cmd.append(f'-t-{plugin.get_name()}')
251
- cmd.append(basename)
252
- subprocess.run(cmd, check=True, cwd=self.get_path())
674
+ :param context: The Pydantic context.
675
+ :type context: Any
676
+ """
677
+ super().model_post_init(context)
678
+ self.get_layout().initialize(self)
679
+ for layer in self.get_layers():
680
+ layer.initialize(self)