lockss-turtles 0.5.0.dev4__py3-none-any.whl → 0.6.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lockss/turtles/plugin.py CHANGED
@@ -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,66 +28,35 @@
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
- import xml.etree.ElementTree
33
- import zipfile
31
+ """
32
+ Library to represent a LOCKSS plugin.
33
+ """
34
34
 
35
- import java_manifest
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
36
38
 
37
- from lockss.turtles.util import _path
39
+ from collections.abc import Callable
40
+ from pathlib import Path, PurePath
41
+ from typing import Any, List, Optional, Union
42
+ import xml.etree.ElementTree as ET
43
+ from zipfile import ZipFile
38
44
 
45
+ import java_manifest as JM
46
+ from lockss.pybasic.fileutil import path
39
47
 
40
- class Plugin(object):
41
-
42
- @staticmethod
43
- def from_jar(jar_path):
44
- jar_path = _path(jar_path) # in case it's a string
45
- plugin_id = Plugin.id_from_jar(jar_path)
46
- plugin_fstr = str(Plugin.id_to_file(plugin_id))
47
- with zipfile.ZipFile(jar_path, 'r') as zip_file:
48
- with zip_file.open(plugin_fstr, 'r') as plugin_file:
49
- return Plugin(plugin_file, plugin_fstr)
50
-
51
- @staticmethod
52
- def from_path(path):
53
- path = _path(path) # in case it's a string
54
- with open(path, 'r') as input_file:
55
- return Plugin(input_file, path)
56
-
57
- @staticmethod
58
- def file_to_id(plugin_fstr):
59
- return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
60
-
61
- @staticmethod
62
- def id_from_jar(jar_path):
63
- jar_path = _path(jar_path) # in case it's a string
64
- manifest = java_manifest.from_jar(jar_path)
65
- for entry in manifest:
66
- if entry.get('Lockss-Plugin') == 'true':
67
- name = entry.get('Name')
68
- if name is None:
69
- raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
70
- return Plugin.file_to_id(name)
71
- else:
72
- raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
73
-
74
- @staticmethod
75
- def id_to_dir(plugin_id):
76
- return Plugin.id_to_file(plugin_id).parent
77
48
 
78
- @staticmethod
79
- def id_to_file(plugin_id):
80
- return Path(f'{plugin_id.replace(".", "/")}.xml')
49
+ class Plugin(object):
81
50
 
82
- def __init__(self, plugin_file, plugin_path):
51
+ def __init__(self, plugin_file, plugin_path) -> None:
83
52
  super().__init__()
84
53
  self._path = plugin_path
85
- self._parsed = xml.etree.ElementTree.parse(plugin_file).getroot()
54
+ self._parsed = ET.parse(plugin_file).getroot()
86
55
  tag = self._parsed.tag
87
56
  if tag != 'map':
88
57
  raise RuntimeError(f'{plugin_path!s}: invalid root element: {tag}')
89
58
 
90
- def get_aux_packages(self):
59
+ def get_aux_packages(self) -> List[str]:
91
60
  key = 'plugin_aux_packages'
92
61
  lst = [x[1] for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
93
62
  if lst is None or len(lst) < 1:
@@ -96,25 +65,65 @@ class Plugin(object):
96
65
  raise ValueError(f'plugin declares {len(lst)} entries for {key}')
97
66
  return [x.text for x in lst[0].findall('string')]
98
67
 
99
- def get_identifier(self):
68
+ def get_identifier(self) -> Optional[str]:
100
69
  return self._only_one('plugin_identifier')
101
70
 
102
- def get_name(self):
71
+ def get_name(self) -> Optional[str]:
103
72
  return self._only_one('plugin_name')
104
73
 
105
- def get_parent_identifier(self):
74
+ def get_parent_identifier(self) -> Optional[str]:
106
75
  return self._only_one('plugin_parent')
107
76
 
108
- def get_parent_version(self):
77
+ def get_parent_version(self) -> Optional[int]:
109
78
  return self._only_one('plugin_parent_version', int)
110
79
 
111
- def get_version(self):
80
+ def get_version(self) -> Optional[int]:
112
81
  return self._only_one('plugin_version', int)
113
82
 
114
- def _only_one(self, key, result=str):
83
+ def _only_one(self, key: str, result: Callable=str) -> Optional[Any]:
115
84
  lst = [x[1].text for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
116
85
  if lst is None or len(lst) < 1:
117
86
  return None
118
87
  if len(lst) > 1:
119
88
  raise ValueError(f'plugin declares {len(lst)} entries for {key}')
120
89
  return result(lst[0])
90
+
91
+ @staticmethod
92
+ def from_jar(jar_path: Union[PurePath, str]) -> Plugin:
93
+ jar_path = path(jar_path) # in case it's a string
94
+ plugin_id = Plugin.id_from_jar(jar_path)
95
+ plugin_fstr = str(Plugin.id_to_file(plugin_id))
96
+ with ZipFile(jar_path, 'r') as zip_file:
97
+ with zip_file.open(plugin_fstr, 'r') as plugin_file:
98
+ return Plugin(plugin_file, plugin_fstr)
99
+
100
+ @staticmethod
101
+ def from_path(fpath: Union[PurePath, str]) -> Plugin:
102
+ fpath = path(fpath) # in case it's a string
103
+ with open(fpath, 'r') as input_file:
104
+ return Plugin(input_file, fpath)
105
+
106
+ @staticmethod
107
+ def file_to_id(plugin_fstr: str) -> str:
108
+ return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
109
+
110
+ @staticmethod
111
+ def id_from_jar(jar_path: Union[PurePath, str]) -> str:
112
+ jar_path = path(jar_path) # in case it's a string
113
+ manifest = JM.from_jar(jar_path)
114
+ for entry in manifest:
115
+ if entry.get('Lockss-Plugin') == 'true':
116
+ name = entry.get('Name')
117
+ if name is None:
118
+ raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
119
+ return Plugin.file_to_id(name)
120
+ else:
121
+ raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
122
+
123
+ @staticmethod
124
+ def id_to_dir(plugin_id: str) -> Path:
125
+ return Plugin.id_to_file(plugin_id).parent
126
+
127
+ @staticmethod
128
+ def id_to_file(plugin_id: str) -> Path:
129
+ return Path(f'{plugin_id.replace(".", "/")}.xml')
@@ -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,121 +28,132 @@
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
31
+ # Remove in Python 3.14
32
+ # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
33
+ from __future__ import annotations
34
+
35
+ from abc import ABC, abstractmethod
36
+ import importlib.resources as IR
32
37
  from pathlib import Path
33
38
  import subprocess
39
+ from typing import List, Optional, Tuple, Union
40
+
41
+ from lockss.pybasic.fileutil import path
34
42
 
35
- from lockss.turtles.plugin import Plugin
36
- import lockss.turtles.resources
37
- from lockss.turtles.util import _load_and_validate, _path
43
+ from . import resources as __resources__
44
+ from .plugin import Plugin
45
+ from .util import YamlT, load_and_validate
38
46
 
39
47
 
40
48
  class PluginRegistryCatalog(object):
41
49
 
42
50
  PLUGIN_REGISTRY_CATALOG_SCHEMA = 'plugin-registry-catalog-schema.json'
43
51
 
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)
50
-
51
- def __init__(self, parsed):
52
+ def __init__(self, parsed: YamlT) -> None:
52
53
  super().__init__()
53
- self._parsed = parsed
54
+ self._parsed: YamlT = parsed
54
55
 
55
- def get_plugin_registry_files(self):
56
+ def get_plugin_registry_files(self) -> List[str]:
56
57
  return self._parsed['plugin-registry-files']
57
58
 
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)
58
65
 
59
- class PluginRegistry(object):
60
-
61
- PLUGIN_REGISTRY_SCHEMA = 'plugin-registry-schema.json'
62
66
 
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]
67
+ class PluginRegistry(ABC):
69
68
 
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)
77
- else:
78
- raise RuntimeError(f'{plugin_registry_file_path!s}: unknown layout type: {typ}')
69
+ PLUGIN_REGISTRY_SCHEMA = 'plugin-registry-schema.json'
79
70
 
80
- def __init__(self, parsed):
71
+ def __init__(self, parsed: YamlT):
81
72
  super().__init__()
82
- self._parsed = parsed
73
+ self._parsed: YamlT = parsed
83
74
 
84
- def get_id(self):
75
+ def get_id(self) -> str:
85
76
  return self._parsed['id']
86
77
 
87
- def get_layer(self, layer_id):
78
+ def get_layer(self, layer_id) -> Optional[PluginRegistryLayer]:
88
79
  for layer in self.get_layers():
89
80
  if layer.get_id() == layer_id:
90
81
  return layer
91
82
  return None
92
83
 
93
- def get_layer_ids(self):
84
+ def get_layer_ids(self) -> List[str]:
94
85
  return [layer.get_id() for layer in self.get_layers()]
95
86
 
96
- def get_layers(self):
87
+ def get_layers(self) -> List[PluginRegistryLayer]:
97
88
  return [self._make_layer(layer_elem) for layer_elem in self._parsed['layers']]
98
89
 
99
- def get_layout_type(self):
90
+ def get_layout_type(self) -> str:
100
91
  return self._parsed['layout']['type']
101
92
 
102
- def get_name(self):
93
+ def get_name(self) -> str:
103
94
  return self._parsed['name']
104
95
 
105
- def get_plugin_identifiers(self):
96
+ def get_plugin_identifiers(self) -> List[str]:
106
97
  return self._parsed['plugin-identifiers']
107
98
 
108
- def has_plugin(self, plugin_id):
99
+ def has_plugin(self, plugin_id) -> bool:
109
100
  return plugin_id in self.get_plugin_identifiers()
110
101
 
111
- def _make_layer(self, parsed):
112
- raise NotImplementedError('_make_layer')
102
+ @abstractmethod
103
+ def _make_layer(self, parsed: YamlT) -> PluginRegistryLayer:
104
+ pass
105
+
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}')
113
122
 
114
123
 
115
- class PluginRegistryLayer(object):
124
+ class PluginRegistryLayer(ABC):
116
125
 
117
126
  PRODUCTION = 'production'
118
127
 
119
128
  TESTING = 'testing'
120
129
 
121
- def __init__(self, plugin_registry, parsed):
130
+ def __init__(self, plugin_registry: PluginRegistry, parsed: YamlT):
122
131
  super().__init__()
123
- self._parsed = parsed
124
- self._plugin_registry = plugin_registry
132
+ self._parsed: YamlT = parsed
133
+ self._plugin_registry: PluginRegistry = plugin_registry
125
134
 
126
- # Returns (dst_path, plugin)
127
- def deploy_plugin(self, plugin_id, jar_path, interactive=False):
128
- raise NotImplementedError('deploy_plugin')
135
+ @abstractmethod
136
+ def deploy_plugin(self, plugin_id: str, jar_path: Path, interactive: bool=False) -> Optional[Tuple[Path, Plugin]]:
137
+ pass
129
138
 
130
- def get_file_for(self, plugin_id):
131
- raise NotImplementedError('get_file_for')
139
+ @abstractmethod
140
+ def get_file_for(self, plugin_id: str) -> Optional[Path]:
141
+ pass
132
142
 
133
- def get_id(self):
143
+ def get_id(self) -> str:
134
144
  return self._parsed['id']
135
145
 
136
- def get_jars(self):
137
- raise NotImplementedError('get_jars')
146
+ @abstractmethod
147
+ def get_jars(self) -> List[Path]:
148
+ pass
138
149
 
139
- def get_name(self):
150
+ def get_name(self) -> str:
140
151
  return self._parsed['name']
141
152
 
142
- def get_path(self):
143
- return _path(self._parsed['path'])
153
+ def get_path(self) -> Path:
154
+ return path(self._parsed['path'])
144
155
 
145
- def get_plugin_registry(self):
156
+ def get_plugin_registry(self) -> PluginRegistry:
146
157
  return self._plugin_registry
147
158
 
148
159
 
@@ -158,47 +169,47 @@ class DirectoryPluginRegistry(PluginRegistry):
158
169
 
159
170
  DEFAULT_FILE_NAMING_CONVENTION = FILE_NAMING_CONVENTION_IDENTIFIER
160
171
 
161
- def __init__(self, parsed):
172
+ def __init__(self, parsed: YamlT) -> None:
162
173
  super().__init__(parsed)
163
174
 
164
- def _make_layer(self, parsed):
175
+ def _make_layer(self, parsed) -> PluginRegistryLayer:
165
176
  return DirectoryPluginRegistryLayer(self, parsed)
166
177
 
167
178
 
168
179
  class DirectoryPluginRegistryLayer(PluginRegistryLayer):
169
180
 
170
- def __init__(self, plugin_registry, parsed):
181
+ def __init__(self, plugin_registry: PluginRegistry, parsed: YamlT):
171
182
  super().__init__(plugin_registry, parsed)
172
183
 
173
- def deploy_plugin(self, plugin_id, src_path, interactive=False):
174
- src_path = _path(src_path) # in case it's a string
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
175
186
  dst_path = self._get_dstpath(plugin_id)
176
187
  if not self._proceed_copy(src_path, dst_path, interactive=interactive):
177
188
  return None
178
189
  self._copy_jar(src_path, dst_path, interactive=interactive)
179
- return (dst_path, Plugin.from_jar(src_path))
190
+ return dst_path, Plugin.from_jar(src_path)
180
191
 
181
- def get_file_for(self, plugin_id):
192
+ def get_file_for(self, plugin_id) -> Optional[Path]:
182
193
  jar_path = self._get_dstpath(plugin_id)
183
194
  return jar_path if jar_path.is_file() else None
184
195
 
185
- def get_file_naming_convention(self):
196
+ def get_file_naming_convention(self) -> str:
186
197
  return self.get_plugin_registry()._parsed['layout'].get('file-naming-convention', DirectoryPluginRegistry.DEFAULT_FILE_NAMING_CONVENTION)
187
198
 
188
- def get_jars(self):
199
+ def get_jars(self) -> List[Path]:
189
200
  return sorted(self.get_path().glob('*.jar'))
190
201
 
191
- def _copy_jar(self, src_path, dst_path, interactive=False):
202
+ def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
192
203
  basename = dst_path.name
193
204
  subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=self.get_path())
194
205
  if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
195
206
  cmd = ['chcon', '-t', 'httpd_sys_content_t', basename]
196
207
  subprocess.run(cmd, check=True, cwd=self.get_path())
197
208
 
198
- def _get_dstpath(self, plugin_id):
199
- return Path(self.get_path(), self._get_dstfile(plugin_id))
209
+ def _get_dstpath(self, plugin_id: str) -> Path:
210
+ return self.get_path().joinpath(self._get_dstfile(plugin_id))
200
211
 
201
- def _get_dstfile(self, plugin_id):
212
+ def _get_dstfile(self, plugin_id: str) -> str:
202
213
  conv = self.get_file_naming_convention()
203
214
  if conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_IDENTIFIER:
204
215
  return f'{plugin_id}.jar'
@@ -209,7 +220,7 @@ class DirectoryPluginRegistryLayer(PluginRegistryLayer):
209
220
  else:
210
221
  raise RuntimeError(f'{self.get_plugin_registry().get_id()}: unknown file naming convention: {conv}')
211
222
 
212
- def _proceed_copy(self, src_path, dst_path, interactive=False):
223
+ def _proceed_copy(self, src_path: Path, dst_path: Path, interactive: bool=False) -> bool:
213
224
  if not dst_path.exists():
214
225
  if interactive:
215
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'
@@ -222,19 +233,19 @@ class RcsPluginRegistry(DirectoryPluginRegistry):
222
233
 
223
234
  LAYOUT = 'rcs'
224
235
 
225
- def __init__(self, parsed):
236
+ def __init__(self, parsed: YamlT) -> None:
226
237
  super().__init__(parsed)
227
238
 
228
- def _make_layer(self, parsed):
239
+ def _make_layer(self, parsed: YamlT) -> PluginRegistryLayer:
229
240
  return RcsPluginRegistryLayer(self, parsed)
230
241
 
231
242
 
232
243
  class RcsPluginRegistryLayer(DirectoryPluginRegistryLayer):
233
244
 
234
- def __init__(self, plugin_registry, parsed):
245
+ def __init__(self, plugin_registry: PluginRegistry, parsed: YamlT) -> None:
235
246
  super().__init__(plugin_registry, parsed)
236
247
 
237
- def _copy_jar(self, src_path, dst_path, interactive=False):
248
+ def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
238
249
  basename = dst_path.name
239
250
  plugin = Plugin.from_jar(src_path)
240
251
  rcs_path = self.get_path().joinpath('RCS', f'{basename},v')