lockss-turtles 0.6.0.dev1__py3-none-any.whl → 0.6.0.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,231 +33,206 @@
33
33
  from __future__ import annotations
34
34
 
35
35
  from abc import ABC, abstractmethod
36
- import importlib.resources as IR
37
36
  from pathlib import Path
38
37
  import subprocess
39
- from typing import List, Optional, Tuple, Union
38
+ from typing import Annotated, Any, ClassVar, Literal, Optional, Union
40
39
 
40
+ from lockss.pybasic.errorutil import InternalError
41
41
  from lockss.pybasic.fileutil import path
42
+ from pydantic import BaseModel, Field
42
43
 
43
- from . import resources as __resources__
44
- from .plugin import Plugin
45
- from .util import YamlT, load_and_validate
44
+ from .plugin import Plugin, PluginIdentifier
45
+ from .util import BaseModelWithRoot
46
46
 
47
47
 
48
- class PluginRegistryCatalog(object):
49
48
 
50
- PLUGIN_REGISTRY_CATALOG_SCHEMA = 'plugin-registry-catalog-schema.json'
49
+ PluginRegistryCatalogKind = Literal['PluginRegistryCatalog']
51
50
 
52
- def __init__(self, parsed: YamlT) -> None:
53
- super().__init__()
54
- self._parsed: YamlT = parsed
55
51
 
56
- def get_plugin_registry_files(self) -> List[str]:
57
- return self._parsed['plugin-registry-files']
52
+ 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')
58
55
 
59
- @staticmethod
60
- def from_path(plugin_registry_catalog_path: Union[Path, str]) -> PluginRegistryCatalog:
61
- plugin_registry_catalog_path = path(plugin_registry_catalog_path)
62
- with IR.path(__resources__, PluginRegistryCatalog.PLUGIN_REGISTRY_CATALOG_SCHEMA) as plugin_registry_catalog_schema_path:
63
- parsed = load_and_validate(plugin_registry_catalog_schema_path, plugin_registry_catalog_path)
64
- return PluginRegistryCatalog(parsed)
56
+ def get_plugin_registry_files(self) -> list[Path]:
57
+ return [self._get_root().joinpath(pstr) for pstr in self.plugin_registry_files]
65
58
 
66
59
 
67
- class PluginRegistry(ABC):
60
+ PluginRegistryLayoutType = Literal['directory', 'rcs']
68
61
 
69
- PLUGIN_REGISTRY_SCHEMA = 'plugin-registry-schema.json'
70
62
 
71
- def __init__(self, parsed: YamlT):
72
- super().__init__()
73
- self._parsed: YamlT = parsed
63
+ PluginRegistryLayoutFileNamingConvention = Literal['abbreviated', 'identifier', 'underscore']
74
64
 
75
- def get_id(self) -> str:
76
- return self._parsed['id']
77
65
 
78
- def get_layer(self, layer_id) -> Optional[PluginRegistryLayer]:
79
- for layer in self.get_layers():
80
- if layer.get_id() == layer_id:
81
- return layer
82
- return None
83
-
84
- def get_layer_ids(self) -> List[str]:
85
- return [layer.get_id() for layer in self.get_layers()]
86
-
87
- def get_layers(self) -> List[PluginRegistryLayer]:
88
- return [self._make_layer(layer_elem) for layer_elem in self._parsed['layers']]
66
+ class BasePluginRegistryLayout(BaseModel, ABC):
67
+ TYPE_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Registry Layout Type', description='A plugin registry layout type')
68
+ 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')
89
70
 
90
- def get_layout_type(self) -> str:
91
- return self._parsed['layout']['type']
71
+ _plugin_registry: Optional[PluginRegistry]
92
72
 
93
- def get_name(self) -> str:
94
- return self._parsed['name']
95
-
96
- def get_plugin_identifiers(self) -> List[str]:
97
- return self._parsed['plugin-identifiers']
98
-
99
- def has_plugin(self, plugin_id) -> bool:
100
- return plugin_id in self.get_plugin_identifiers()
101
-
102
- @abstractmethod
103
- def _make_layer(self, parsed: YamlT) -> PluginRegistryLayer:
104
- pass
73
+ def deploy_plugin(self, plugin_id: PluginIdentifier, layer: PluginRegistryLayer, src_path: Path, interactive: bool=False) -> Optional[tuple[Path, Plugin]]:
74
+ src_path = path(src_path) # in case it's a string
75
+ dst_path = self._get_dstpath(plugin_id, layer)
76
+ if not self._proceed_copy(src_path, dst_path, layer, interactive=interactive):
77
+ return None
78
+ self._copy_jar(src_path, dst_path, interactive=interactive)
79
+ return dst_path, Plugin.from_jar(src_path)
105
80
 
106
- @staticmethod
107
- def from_path(plugin_registry_file_path: Union[Path, str]) -> List[PluginRegistry]:
108
- plugin_registry_file_path = path(plugin_registry_file_path)
109
- with IR.path(__resources__, PluginRegistry.PLUGIN_REGISTRY_SCHEMA) as plugin_registry_schema_path:
110
- lst = load_and_validate(plugin_registry_schema_path, plugin_registry_file_path, multiple=True)
111
- return [PluginRegistry._from_obj(parsed, plugin_registry_file_path) for parsed in lst]
112
-
113
- @staticmethod
114
- def _from_obj(parsed: YamlT, plugin_registry_file_path: Path) -> PluginRegistry:
115
- typ = parsed['layout']['type']
116
- if typ == DirectoryPluginRegistry.LAYOUT:
117
- return DirectoryPluginRegistry(parsed)
118
- elif typ == RcsPluginRegistry.LAYOUT:
119
- return RcsPluginRegistry(parsed)
120
- else:
121
- raise RuntimeError(f'{plugin_registry_file_path!s}: unknown layout type: {typ}')
81
+ def get_file_for(self, plugin_id, layer: PluginRegistryLayer) -> Optional[Path]:
82
+ jar_path = self._get_dstpath(plugin_id, layer)
83
+ return jar_path if jar_path.is_file() else None
122
84
 
85
+ def get_file_naming_convention(self) -> PluginRegistryLayoutFileNamingConvention:
86
+ return getattr(self, 'file_naming_convention')
123
87
 
124
- class PluginRegistryLayer(ABC):
88
+ def get_plugin_registry(self) -> PluginRegistry:
89
+ if self._plugin_registry is None:
90
+ raise RuntimeError('Uninitialized plugin registry')
91
+ return self._plugin_registry
125
92
 
126
- PRODUCTION = 'production'
93
+ def get_type(self) -> PluginRegistryLayoutType:
94
+ return getattr(self, 'type')
127
95
 
128
- TESTING = 'testing'
96
+ def initialize(self, plugin_registry: PluginRegistry) -> BasePluginRegistryLayout:
97
+ self._plugin_registry = plugin_registry
98
+ return self
129
99
 
130
- def __init__(self, plugin_registry: PluginRegistry, parsed: YamlT):
131
- super().__init__()
132
- self._parsed: YamlT = parsed
133
- self._plugin_registry: PluginRegistry = plugin_registry
100
+ def model_post_init(self, context: Any) -> None:
101
+ super().model_post_init(context)
102
+ self._plugin_registry = None
134
103
 
135
104
  @abstractmethod
136
- def deploy_plugin(self, plugin_id: str, jar_path: Path, interactive: bool=False) -> Optional[Tuple[Path, Plugin]]:
105
+ def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
137
106
  pass
138
107
 
139
- @abstractmethod
140
- def get_file_for(self, plugin_id: str) -> Optional[Path]:
141
- pass
108
+ def _get_dstfile(self, plugin_id: PluginIdentifier) -> str:
109
+ if (conv := self.get_file_naming_convention()) == 'abbreviated':
110
+ return f'{plugin_id.split(".")[-1]}.jar'
111
+ elif conv == 'identifier':
112
+ return f'{plugin_id}.jar'
113
+ elif conv == 'underscore':
114
+ return f'{plugin_id.replace(".", "_")}.jar'
115
+ else:
116
+ raise InternalError()
142
117
 
143
- def get_id(self) -> str:
144
- return self._parsed['id']
118
+ def _get_dstpath(self, plugin_id: PluginIdentifier, layer: PluginRegistryLayer) -> Path:
119
+ return layer.get_path().joinpath(self._get_dstfile(plugin_id))
145
120
 
146
- @abstractmethod
147
- def get_jars(self) -> List[Path]:
148
- pass
121
+ def _proceed_copy(self, src_path: Path, dst_path: Path, layer: PluginRegistryLayer, interactive: bool=False) -> bool:
122
+ if not dst_path.exists():
123
+ if interactive:
124
+ 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'
125
+ if i != 'y':
126
+ return False
127
+ return True
149
128
 
150
- def get_name(self) -> str:
151
- return self._parsed['name']
152
129
 
153
- def get_path(self) -> Path:
154
- return path(self._parsed['path'])
130
+ class DirectoryPluginRegistryLayout(BasePluginRegistryLayout):
131
+ type: Literal['directory'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
132
+ file_naming_convention: Optional[PluginRegistryLayoutFileNamingConvention] = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT, **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
155
133
 
156
- def get_plugin_registry(self) -> PluginRegistry:
157
- return self._plugin_registry
134
+ def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
135
+ basename = dst_path.name
136
+ subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=self.get_path())
137
+ if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
138
+ cmd = ['chcon', '-t', 'httpd_sys_content_t', basename]
139
+ subprocess.run(cmd, check=True, cwd=self.get_path())
158
140
 
159
141
 
160
- class DirectoryPluginRegistry(PluginRegistry):
142
+ class RcsPluginRegistryLayout(DirectoryPluginRegistryLayout):
143
+ type: Literal['rcs'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
144
+ file_naming_convention: Optional[PluginRegistryLayoutFileNamingConvention] = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT, **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
145
+
146
+ def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
147
+ basename = dst_path.name
148
+ plugin = Plugin.from_jar(src_path)
149
+ rcs_path = self.get_path().joinpath('RCS', f'{basename},v')
150
+ # Maybe do co -l before the parent's copy
151
+ if dst_path.exists() and rcs_path.is_file():
152
+ cmd = ['co', '-l', basename]
153
+ subprocess.run(cmd, check=True, cwd=self.get_path())
154
+ # Do the parent's copy
155
+ super()._copy_jar(src_path, dst_path)
156
+ # Do ci -u after the parent's copy
157
+ cmd = ['ci', '-u', f'-mVersion {plugin.get_version()}']
158
+ if not rcs_path.is_file():
159
+ cmd.append(f'-t-{plugin.get_name()}')
160
+ cmd.append(basename)
161
+ subprocess.run(cmd, check=True, cwd=self.get_path())
161
162
 
162
- LAYOUT = 'directory'
163
163
 
164
- FILE_NAMING_CONVENTION_ABBREVIATED = 'abbreviated'
164
+ PluginRegistryLayout = Annotated[Union[DirectoryPluginRegistryLayout, RcsPluginRegistryLayout], Field(discriminator='type')]
165
165
 
166
- FILE_NAMING_CONVENTION_IDENTIFIER = 'identifier'
167
166
 
168
- FILE_NAMING_CONVENTION_UNDERSCORE = 'underscore'
167
+ PluginRegistryLayerIdentifier = str
169
168
 
170
- DEFAULT_FILE_NAMING_CONVENTION = FILE_NAMING_CONVENTION_IDENTIFIER
171
169
 
172
- def __init__(self, parsed: YamlT) -> None:
173
- super().__init__(parsed)
170
+ class PluginRegistryLayer(BaseModel, ABC):
171
+ PRODUCTION: ClassVar[str] = 'production'
172
+ TESTING: ClassVar[str] = 'testing'
174
173
 
175
- def _make_layer(self, parsed) -> PluginRegistryLayer:
176
- return DirectoryPluginRegistryLayer(self, parsed)
174
+ id: PluginRegistryLayerIdentifier = Field(title='Plugin Registry Layer Identifier', description='An identifier for the plugin registry layer')
175
+ name: str = Field(title='Plugin Registry Layer Name', description='A name for the plugin registry layer')
176
+ path: str = Field(title='Plugin Registry Layer Path', description='A root path for the plugin registry layer')
177
177
 
178
+ def get_id(self) -> PluginRegistryLayerIdentifier:
179
+ return self.id
178
180
 
179
- class DirectoryPluginRegistryLayer(PluginRegistryLayer):
181
+ def get_jars(self) -> list[Path]:
182
+ return sorted(self.get_path().glob('*.jar'))
180
183
 
181
- def __init__(self, plugin_registry: PluginRegistry, parsed: YamlT):
182
- super().__init__(plugin_registry, parsed)
184
+ def get_name(self) -> str:
185
+ return self.name
183
186
 
184
- def deploy_plugin(self, plugin_id: str, src_path: Path, interactive: bool=False) -> Optional[Tuple[Path, Plugin]]:
185
- src_path = path(src_path) # in case it's a string
186
- dst_path = self._get_dstpath(plugin_id)
187
- if not self._proceed_copy(src_path, dst_path, interactive=interactive):
188
- return None
189
- self._copy_jar(src_path, dst_path, interactive=interactive)
190
- return dst_path, Plugin.from_jar(src_path)
187
+ def get_path(self) -> Path:
188
+ return path(self.path)
191
189
 
192
- def get_file_for(self, plugin_id) -> Optional[Path]:
193
- jar_path = self._get_dstpath(plugin_id)
194
- return jar_path if jar_path.is_file() else None
195
190
 
196
- def get_file_naming_convention(self) -> str:
197
- return self.get_plugin_registry()._parsed['layout'].get('file-naming-convention', DirectoryPluginRegistry.DEFAULT_FILE_NAMING_CONVENTION)
191
+ PluginRegistryKind = Literal['PluginRegistry']
198
192
 
199
- def get_jars(self) -> List[Path]:
200
- return sorted(self.get_path().glob('*.jar'))
201
193
 
202
- def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
203
- basename = dst_path.name
204
- subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=self.get_path())
205
- if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
206
- cmd = ['chcon', '-t', 'httpd_sys_content_t', basename]
207
- subprocess.run(cmd, check=True, cwd=self.get_path())
194
+ PluginRegistryIdentifier = str
208
195
 
209
- def _get_dstpath(self, plugin_id: str) -> Path:
210
- return self.get_path().joinpath(self._get_dstfile(plugin_id))
211
196
 
212
- def _get_dstfile(self, plugin_id: str) -> str:
213
- conv = self.get_file_naming_convention()
214
- if conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_IDENTIFIER:
215
- return f'{plugin_id}.jar'
216
- elif conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_UNDERSCORE:
217
- return f'{plugin_id.replace(".", "_")}.jar'
218
- elif conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_ABBREVIATED:
219
- return f'{plugin_id.split(".")[-1]}.jar'
220
- else:
221
- raise RuntimeError(f'{self.get_plugin_registry().get_id()}: unknown file naming convention: {conv}')
197
+ class PluginRegistry(BaseModelWithRoot):
198
+ kind: PluginRegistryKind = Field(description="This object's kind")
199
+ id: PluginRegistryIdentifier = Field(title='Plugin Registry Identifier', description='An identifier for the plugin set')
200
+ name: str = Field(title='Plugin Registry Name', description='A name for the plugin set')
201
+ layout: PluginRegistryLayout = Field(title='Plugin Registry Layout', description='A layout for the plugin registry')
202
+ layers: list[PluginRegistryLayer] = Field(min_length=1, title='Plugin Registry Layers', description="A non-empty list of plugin registry layers", alias='plugin-registry-layers')
203
+ plugin_identifiers: list[PluginIdentifier] = Field(min_length=1, title='Plugin Identifiers', description="A non-empty list of plugin identifiers", alias='plugin-identifiers')
204
+ suppressed_plugin_identifiers: list[PluginIdentifier] = Field([], title='Suppressed Plugin Identifiers', description="A list of suppressed plugin identifiers", alias='suppressed-plugin-identifiers')
222
205
 
223
- def _proceed_copy(self, src_path: Path, dst_path: Path, interactive: bool=False) -> bool:
224
- if not dst_path.exists():
225
- if interactive:
226
- 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'
227
- if i != 'y':
228
- return False
229
- return True
206
+ def get_id(self) -> PluginRegistryIdentifier:
207
+ return self.id
230
208
 
209
+ def get_layer(self, layer_id: PluginRegistryLayerIdentifier) -> Optional[PluginRegistryLayer]:
210
+ for layer in self.get_layers():
211
+ if layer.get_id() == layer_id:
212
+ return layer
213
+ return None
231
214
 
232
- class RcsPluginRegistry(DirectoryPluginRegistry):
215
+ def get_layer_ids(self) -> list[PluginRegistryLayerIdentifier]:
216
+ return [layer.get_id() for layer in self.get_layers()]
233
217
 
234
- LAYOUT = 'rcs'
218
+ def get_layers(self) -> list[PluginRegistryLayer]:
219
+ return self.layers
235
220
 
236
- def __init__(self, parsed: YamlT) -> None:
237
- super().__init__(parsed)
221
+ def get_layout(self) -> BasePluginRegistryLayout:
222
+ return self.layout
238
223
 
239
- def _make_layer(self, parsed: YamlT) -> PluginRegistryLayer:
240
- return RcsPluginRegistryLayer(self, parsed)
224
+ def get_name(self) -> str:
225
+ return self.name
241
226
 
227
+ def get_plugin_identifiers(self) -> list[PluginIdentifier]:
228
+ return self.plugin_identifiers
242
229
 
243
- class RcsPluginRegistryLayer(DirectoryPluginRegistryLayer):
230
+ def get_suppressed_plugin_identifiers(self) -> list[PluginIdentifier]:
231
+ return self.suppressed_plugin_identifiers
244
232
 
245
- def __init__(self, plugin_registry: PluginRegistry, parsed: YamlT) -> None:
246
- super().__init__(plugin_registry, parsed)
233
+ def has_plugin(self, plugin_id: PluginIdentifier) -> bool:
234
+ return plugin_id in self.get_plugin_identifiers()
247
235
 
248
- def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
249
- basename = dst_path.name
250
- plugin = Plugin.from_jar(src_path)
251
- rcs_path = self.get_path().joinpath('RCS', f'{basename},v')
252
- # Maybe do co -l before the parent's copy
253
- if dst_path.exists() and rcs_path.is_file():
254
- cmd = ['co', '-l', basename]
255
- subprocess.run(cmd, check=True, cwd=self.get_path())
256
- # Do the parent's copy
257
- super()._copy_jar(src_path, dst_path)
258
- # Do ci -u after the parent's copy
259
- cmd = ['ci', '-u', f'-mVersion {plugin.get_version()}']
260
- if not rcs_path.is_file():
261
- cmd.append(f'-t-{plugin.get_name()}')
262
- cmd.append(basename)
263
- subprocess.run(cmd, check=True, cwd=self.get_path())
236
+ def model_post_init(self, context: Any) -> None:
237
+ super().model_post_init(context)
238
+ self.get_layout().initialize(self)