pluginify 0.0.0.post1.dev0__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.
- pluginify/__init__.py +11 -0
- pluginify/_version.py +7 -0
- pluginify/commandline_typer.py +258 -0
- pluginify/config.py +144 -0
- pluginify/create_plugin_registries.py +1037 -0
- pluginify/errors.py +16 -0
- pluginify/interfaces/__init__.py +9 -0
- pluginify/interfaces/base.py +665 -0
- pluginify/interfaces/class_based/data_modifiers.py +38 -0
- pluginify/interfaces/class_based_plugin.py +210 -0
- pluginify/interfaces/yaml_based/configs.py +27 -0
- pluginify/plugin_registry.py +925 -0
- pluginify/plugins/classes/data_modifiers/cuboid.py +41 -0
- pluginify/plugins/yaml/configs/stucco.yaml +16 -0
- pluginify/pydantic_models/v1/configs.py +58 -0
- pluginify/utils/__init__.py +62 -0
- pluginify/utils/context_managers.py +32 -0
- pluginify-0.0.0.post1.dev0.dist-info/METADATA +66 -0
- pluginify-0.0.0.post1.dev0.dist-info/RECORD +22 -0
- pluginify-0.0.0.post1.dev0.dist-info/WHEEL +4 -0
- pluginify-0.0.0.post1.dev0.dist-info/entry_points.txt +6 -0
- pluginify-0.0.0.post1.dev0.dist-info/licenses/LICENSE +128 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
# # # This source code is subject to the license referenced at
|
|
2
|
+
# # # https://github.com/NRLMMD-GEOIPS.
|
|
3
|
+
|
|
4
|
+
"""PluginRegistry class to interface with the JSON plugin registries.
|
|
5
|
+
|
|
6
|
+
The "pluginify create" utility generates a JSON file at the top
|
|
7
|
+
level of every plugin package under a given namespace
|
|
8
|
+
(default='pluginify.plugin_packages') with a complete list of all plugins with the
|
|
9
|
+
associated metadata (everything except the actual contents of the plugin itself).
|
|
10
|
+
|
|
11
|
+
Once all of the registered_plugins.json files have been generated via
|
|
12
|
+
pluginify create, this class uses those registries to quickly
|
|
13
|
+
identify and open plugins as required. Previously the individual
|
|
14
|
+
interface classes would open all plugins every time one was required,
|
|
15
|
+
so moving this process into a single PluginRegistry object allows us to
|
|
16
|
+
more effectively cache plugins across all interfaces, and avoid reading
|
|
17
|
+
in all plugins multiple times.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from importlib import import_module, util, metadata, resources
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from types import SimpleNamespace
|
|
26
|
+
|
|
27
|
+
from lexeme_type.lexeme import Lexeme
|
|
28
|
+
from pydantic import BaseModel
|
|
29
|
+
import yaml
|
|
30
|
+
|
|
31
|
+
from pluginify.config import NAMESPACE, REBUILD_REGISTRIES, get_registry_cache_dir
|
|
32
|
+
from pluginify.create_plugin_registries import create_plugin_registries
|
|
33
|
+
from pluginify.errors import PluginError, PluginRegistryError
|
|
34
|
+
from pluginify.utils import merge_nested_dicts
|
|
35
|
+
|
|
36
|
+
LOG = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PluginRegistry:
|
|
40
|
+
"""Plugin Registry class definition.
|
|
41
|
+
|
|
42
|
+
Represents all of the plugins found in all of the available plugin packages of a
|
|
43
|
+
given namespace (default='pluginify.plugin_packages').
|
|
44
|
+
|
|
45
|
+
This class will load a plugin when requested, rather than loading all plugins when
|
|
46
|
+
pluginify is instantiated.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, namespace, _test_registry_files=[]):
|
|
50
|
+
"""Initialize the plugin registry for the namespace provided.
|
|
51
|
+
|
|
52
|
+
Where namespace is the group of plugin packages used to create the plugin
|
|
53
|
+
registry.
|
|
54
|
+
"""
|
|
55
|
+
self.namespace = namespace
|
|
56
|
+
# Use this for unit testing
|
|
57
|
+
if _test_registry_files:
|
|
58
|
+
self.registry_files = _test_registry_files
|
|
59
|
+
self._is_test = True
|
|
60
|
+
# Use this for normal operation and collect the registry files
|
|
61
|
+
else:
|
|
62
|
+
self._is_test = False
|
|
63
|
+
self.registry_files = self._find_registry_files(self.namespace)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def registered_plugins(self):
|
|
67
|
+
"""Dictionary of every plugin's metadata found within self.namespace.
|
|
68
|
+
|
|
69
|
+
self.namespace usually should correspond to 'pluginify.plugin_packages' unless
|
|
70
|
+
you're creating registries for plugins that exist outside this namespace.
|
|
71
|
+
"""
|
|
72
|
+
if not hasattr(self, "_registered_plugins"):
|
|
73
|
+
self._set_class_properties()
|
|
74
|
+
return self._registered_plugins
|
|
75
|
+
|
|
76
|
+
@registered_plugins.setter
|
|
77
|
+
def registered_plugins(self, new_value):
|
|
78
|
+
"""Set the registered_plugins class attribute.
|
|
79
|
+
|
|
80
|
+
See the registered_plugins property for more information.
|
|
81
|
+
"""
|
|
82
|
+
self._registered_plugins = new_value
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def interface_mapping(self):
|
|
86
|
+
"""Dictionary of interface types and interfaces of that type.
|
|
87
|
+
|
|
88
|
+
pluginify has three types of interfaces, though only two are commonly used
|
|
89
|
+
(yaml_based, module_based). This dictionary has top level keys of all interface
|
|
90
|
+
types, with their values being the list of unique interfaces that inherit that
|
|
91
|
+
type.
|
|
92
|
+
"""
|
|
93
|
+
if not hasattr(self, "_interface_mapping"):
|
|
94
|
+
self._set_class_properties()
|
|
95
|
+
return self._interface_mapping
|
|
96
|
+
|
|
97
|
+
@interface_mapping.setter
|
|
98
|
+
def interface_mapping(self, new_value):
|
|
99
|
+
"""Set the interface_mapping class attribute.
|
|
100
|
+
|
|
101
|
+
See the interface_mapping property for more information.
|
|
102
|
+
"""
|
|
103
|
+
self._registered_plugins = new_value
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def registered_yaml_based_plugins(self):
|
|
107
|
+
"""A dictionary of registered YAML-based plugins."""
|
|
108
|
+
if not hasattr(self, "_registered_plugins"):
|
|
109
|
+
self._set_class_properties()
|
|
110
|
+
return self._registered_plugins["yaml_based"]
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def registered_class_based_plugins(self):
|
|
114
|
+
"""A dictionary of registered class-based plugins."""
|
|
115
|
+
if not hasattr(self, "_registered_plugins"):
|
|
116
|
+
self._set_class_properties()
|
|
117
|
+
return self._registered_plugins["class_based"]
|
|
118
|
+
|
|
119
|
+
def _set_class_properties(
|
|
120
|
+
self,
|
|
121
|
+
force_reset=False,
|
|
122
|
+
rebuild_registries_override=None,
|
|
123
|
+
):
|
|
124
|
+
"""Find all plugins in registered plugin packages.
|
|
125
|
+
|
|
126
|
+
Traverse the ``registered_plugins.json`` of each registered plugin package under
|
|
127
|
+
self.namespace and add their entries to the _registered_plugins dictionary
|
|
128
|
+
attribute.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
force_reset: bool, default=False
|
|
133
|
+
- Whether or not we want to force the plugin registry to recreate its
|
|
134
|
+
'registered_plugins' attribute. This essentially forces a re-read of all
|
|
135
|
+
of the registered_plugins.json files and recomputes the master dictionary.
|
|
136
|
+
- Useful when we have rebuilt the registry files during runtime.
|
|
137
|
+
rebuild_registries_override: bool, default=None
|
|
138
|
+
- Whether or not we want to override the value of REBUILD_REGISTIRES set in
|
|
139
|
+
the config file for this package to a different boolean value. If None,
|
|
140
|
+
use the default value set in the config file.
|
|
141
|
+
- Used for unit tests.
|
|
142
|
+
"""
|
|
143
|
+
# Load the registries here and return them as a dictionary
|
|
144
|
+
if not hasattr(self, "_registered_plugins") or force_reset:
|
|
145
|
+
|
|
146
|
+
# Complete dictionary of all available plugins found in every plugin package
|
|
147
|
+
self._registered_plugins = {}
|
|
148
|
+
# A mapping of interfaces to plugin_types. Ie:
|
|
149
|
+
# {
|
|
150
|
+
# "yaml_based": [products, sectors, ...],
|
|
151
|
+
# "module_based": [algorithms, readers, ...],
|
|
152
|
+
# "text_based": [tpw_cimss, ...]
|
|
153
|
+
# }
|
|
154
|
+
self._interface_mapping = {}
|
|
155
|
+
for reg_path in self.registry_files:
|
|
156
|
+
try:
|
|
157
|
+
registry = self._load_registry(reg_path)
|
|
158
|
+
except FileNotFoundError as e:
|
|
159
|
+
if REBUILD_REGISTRIES and rebuild_registries_override in [
|
|
160
|
+
None,
|
|
161
|
+
True,
|
|
162
|
+
]:
|
|
163
|
+
# This will be hit if we have this configuration variable set to
|
|
164
|
+
# True
|
|
165
|
+
LOG.warning(
|
|
166
|
+
f"Plugin registry {reg_path} does not exist, "
|
|
167
|
+
"please run 'pluginify create'"
|
|
168
|
+
)
|
|
169
|
+
# We attempt to create plugin registries under self.namespace
|
|
170
|
+
# if one or more plugin packages' registry file is missing and
|
|
171
|
+
# the REBUILD_REGISTRIES configuration variable is set to true.
|
|
172
|
+
# This should not be hit twice.
|
|
173
|
+
|
|
174
|
+
# Create plugin registries
|
|
175
|
+
self.create_registries()
|
|
176
|
+
registry = self._load_registry(reg_path)
|
|
177
|
+
else:
|
|
178
|
+
raise FileNotFoundError(
|
|
179
|
+
f"Plugin registry {reg_path} does not exist and "
|
|
180
|
+
"PLUGINIFY_REBUILD_REGISTRIES isn't set to True. To "
|
|
181
|
+
"manually create these files, run 'pluginify create'."
|
|
182
|
+
) from e
|
|
183
|
+
return_tuple = self._parse_registry(
|
|
184
|
+
self.interface_mapping, self.registered_plugins, registry
|
|
185
|
+
)
|
|
186
|
+
self.interface_mapping = return_tuple.interface_mapping
|
|
187
|
+
self.registered_plugins = return_tuple.registered_plugins
|
|
188
|
+
# Let's test this separately, not at runtime (see validate_all_registries).
|
|
189
|
+
# Assume it was tested up front, and no longer needs testing at
|
|
190
|
+
# runtime, so we don't fail catastrophically for a single bad
|
|
191
|
+
# plugin that may not even be used.
|
|
192
|
+
# if not self._is_test:
|
|
193
|
+
# self.validate_registry(
|
|
194
|
+
# self._registered_plugins,
|
|
195
|
+
# "all_registered_plugins",
|
|
196
|
+
# )
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _find_registry_files(namespace):
|
|
200
|
+
"""Locate all plugin registry files found under 'namespace'.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
namespace: str
|
|
205
|
+
- The namespace in which plugin packages are registered to. Usually, this
|
|
206
|
+
will be the 'pluginify.plugin_package' namespace, however this can be
|
|
207
|
+
changed by providing a different top-level namespace to the PluginRegistry
|
|
208
|
+
class.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
registry_files: list[str]
|
|
213
|
+
- A list of filepaths corresponding to expected registered_plugins.json
|
|
214
|
+
files for all plugin packages found under namespace.
|
|
215
|
+
|
|
216
|
+
Raises
|
|
217
|
+
------
|
|
218
|
+
PluginRegistryError:
|
|
219
|
+
- Occurs if a package is potentially missing an __init__.py file.
|
|
220
|
+
"""
|
|
221
|
+
registry_files = [] # Collect the paths to the registry files here
|
|
222
|
+
for pkg in metadata.entry_points(group=namespace):
|
|
223
|
+
write_dir = get_registry_cache_dir(namespace, pkg.value)
|
|
224
|
+
try:
|
|
225
|
+
registry_files.append(str(write_dir / "registered_plugins.json"))
|
|
226
|
+
except TypeError as e:
|
|
227
|
+
raise PluginRegistryError(
|
|
228
|
+
f"resources.files('{pkg.value}') failed\n"
|
|
229
|
+
f"pkg {pkg}\n"
|
|
230
|
+
"Potentially missing __init__.py file? Try:\n"
|
|
231
|
+
f" touch {pkg.value}/__init__.py\n"
|
|
232
|
+
"and try again.\n"
|
|
233
|
+
"Note you will need to add a docstring to "
|
|
234
|
+
f"{pkg.value}/__init__.py in order for all tests to pass"
|
|
235
|
+
) from e
|
|
236
|
+
return registry_files
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _load_registry(reg_path):
|
|
240
|
+
"""Load the plugin registry found at 'reg_path'.
|
|
241
|
+
|
|
242
|
+
All files provided to this function share the same namespace. Usually, this
|
|
243
|
+
will be the 'pluginify.plugin_package' namespace, however this can be changed by
|
|
244
|
+
providing a different top-level namespace to the PluginRegistry class.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
reg_path: str
|
|
249
|
+
- The absolute path to the plugin registry file for a certain plugin
|
|
250
|
+
package.
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
registry: dict
|
|
255
|
+
- A dictionary representing the contents of a plugin registry file. Can
|
|
256
|
+
include top-level keys such as 'yaml_based' or 'module_based' each of
|
|
257
|
+
which is another dictionary containing interfaces of that type, which
|
|
258
|
+
are also dictionaries containing plugins that correspond to that
|
|
259
|
+
interface.
|
|
260
|
+
|
|
261
|
+
Raises
|
|
262
|
+
------
|
|
263
|
+
FileNotFoundError:
|
|
264
|
+
- Raised if 'reg_path' doesn't exist.
|
|
265
|
+
"""
|
|
266
|
+
# if this is a yaml file, this is used for testing
|
|
267
|
+
if Path(reg_path).suffix == ".yaml":
|
|
268
|
+
with open(reg_path, "r") as fo:
|
|
269
|
+
registry = yaml.safe_load(fo)
|
|
270
|
+
else:
|
|
271
|
+
with open(reg_path, "r") as fo:
|
|
272
|
+
registry = json.load(fo)
|
|
273
|
+
|
|
274
|
+
return registry
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _parse_registry(interface_mapping, registered_plugins, registry):
|
|
278
|
+
"""Parse all plugins found under a package's plugin registry.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
interface_mapping: dict
|
|
283
|
+
- Dictionary of interface types and interfaces of that type.
|
|
284
|
+
pluginify has three types of interfaces, though only two are commonly used
|
|
285
|
+
(yaml_based, class_based). This dictionary has top level keys of all
|
|
286
|
+
interface types, with their values being the list of unique interfaces
|
|
287
|
+
that inherit that type.
|
|
288
|
+
registered_plugins: dict
|
|
289
|
+
- A dictionary of every plugin's metadata found within the plugin_registry's
|
|
290
|
+
namespace.
|
|
291
|
+
registry: dict
|
|
292
|
+
- A dictionary representing the contents of a plugin registry file. Can
|
|
293
|
+
include top-level keys such as 'yaml_based' or 'class_based' each of
|
|
294
|
+
which is another dictionary containing interfaces of that type, which
|
|
295
|
+
are also dictionaries containing plugins that correspond to that
|
|
296
|
+
interface.
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
return_tuple: SimpleNamespace
|
|
301
|
+
- A SimpleNamespace object containing updated values of the variables
|
|
302
|
+
['interface_mapping', 'registered_plugins'] which are dictionaries. See
|
|
303
|
+
comments about those two variables in the parameters section above.
|
|
304
|
+
"""
|
|
305
|
+
for plugin_type in registry:
|
|
306
|
+
if plugin_type not in registered_plugins:
|
|
307
|
+
registered_plugins[plugin_type] = {}
|
|
308
|
+
interface_mapping[plugin_type] = []
|
|
309
|
+
|
|
310
|
+
for interface in registry[plugin_type]:
|
|
311
|
+
interface_dict = registry[plugin_type][interface]
|
|
312
|
+
|
|
313
|
+
if interface not in registered_plugins[plugin_type]:
|
|
314
|
+
registered_plugins[plugin_type][interface] = interface_dict
|
|
315
|
+
interface_mapping[plugin_type].append(interface)
|
|
316
|
+
else:
|
|
317
|
+
merge_nested_dicts(
|
|
318
|
+
registered_plugins[plugin_type][interface],
|
|
319
|
+
interface_dict,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return_tuple = SimpleNamespace(
|
|
323
|
+
interface_mapping=interface_mapping, registered_plugins=registered_plugins
|
|
324
|
+
)
|
|
325
|
+
return return_tuple
|
|
326
|
+
|
|
327
|
+
def get_plugin_metadata(self, interface_obj, plugin_name):
|
|
328
|
+
"""Retrieve a plugin's metadata.
|
|
329
|
+
|
|
330
|
+
Where the metadata of the plugin matches the plugin's corresponding entry in the
|
|
331
|
+
plugin registry.
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
interface_obj: Interface Object
|
|
336
|
+
- The object representing the interface class requesting plugin metadata.
|
|
337
|
+
plugin_name: str or tuple(str)
|
|
338
|
+
- The name of the plugin whose metadata we want.
|
|
339
|
+
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
metadata: dict
|
|
343
|
+
- A dictionary of metadata for the requested plugin.
|
|
344
|
+
"""
|
|
345
|
+
interface_registry = self.registered_plugins.get(
|
|
346
|
+
interface_obj.interface_type, {}
|
|
347
|
+
).get(interface_obj.name)
|
|
348
|
+
|
|
349
|
+
if interface_registry is None:
|
|
350
|
+
raise KeyError(
|
|
351
|
+
"Error: There is no interface in the plugin registry of type '"
|
|
352
|
+
f"{interface_obj.interface_type}' called '{interface_obj.name}'."
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if isinstance(plugin_name, tuple):
|
|
356
|
+
# This occurs for product plugins: i.e. ('abi', 'Infrared')
|
|
357
|
+
metadata = interface_registry.get(plugin_name[0], {}).get(plugin_name[1])
|
|
358
|
+
elif isinstance(plugin_name, str):
|
|
359
|
+
metadata = interface_registry.get(plugin_name)
|
|
360
|
+
else:
|
|
361
|
+
raise TypeError(
|
|
362
|
+
f"Error: cannot search the plugin registry with the provided name = "
|
|
363
|
+
f"{plugin_name}. Please provide either a string or a tuple of strings."
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if metadata is None:
|
|
367
|
+
raise PluginRegistryError(
|
|
368
|
+
f"Error: There is no associated plugin under interface "
|
|
369
|
+
f"'{interface_obj.name}' called '{plugin_name}'. If you're sure this "
|
|
370
|
+
"plugin exists, please run 'pluginify create'."
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return metadata
|
|
374
|
+
|
|
375
|
+
def load_plugin(self, data: dict) -> BaseModel:
|
|
376
|
+
"""
|
|
377
|
+
Dynamically load and validate pydantic models based on apiVersion and interface.
|
|
378
|
+
|
|
379
|
+
This method parses the `apiVersion` field from the input dictionary to
|
|
380
|
+
determine the package and pydantic model version to use. It then dynamically
|
|
381
|
+
imports the corresponding Pydantic model classes based on the `interface` field
|
|
382
|
+
to validate the input data.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
data : dict
|
|
387
|
+
Dictionary representing a plugin definition. Must include the `interface`
|
|
388
|
+
field. May optionally include `apiVersion`. If not present, "pluginify/v1"
|
|
389
|
+
is assumed.
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
BaseModel
|
|
394
|
+
A validated Pydantic model instance.
|
|
395
|
+
|
|
396
|
+
Raises
|
|
397
|
+
------
|
|
398
|
+
ValueError
|
|
399
|
+
If `apiVersion` is improperly formatted or if `interface` field is missing.
|
|
400
|
+
ImportError
|
|
401
|
+
If the specified module for the given model version cannot be imported.
|
|
402
|
+
"""
|
|
403
|
+
api_version = data.get("apiVersion", "pluginify/v1")
|
|
404
|
+
|
|
405
|
+
# Split "package_name/model_version"
|
|
406
|
+
# Use package_name to select the appropriate package to search for the api.
|
|
407
|
+
try:
|
|
408
|
+
package_name, model_version = api_version.split("/")
|
|
409
|
+
except ValueError as e:
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"Invalid apiVersion format: {api_version}. "
|
|
412
|
+
f"Expected format: 'package_name/version'"
|
|
413
|
+
) from e
|
|
414
|
+
|
|
415
|
+
interface = data.get("interface")
|
|
416
|
+
if not interface:
|
|
417
|
+
raise ValueError("Missing 'interface' field for plugin dispatch")
|
|
418
|
+
|
|
419
|
+
# Construct module path and import
|
|
420
|
+
try:
|
|
421
|
+
module = import_module(
|
|
422
|
+
f"{package_name}.pydantic_models.{model_version}.{interface}"
|
|
423
|
+
)
|
|
424
|
+
except ImportError as e:
|
|
425
|
+
raise ImportError(
|
|
426
|
+
f"Could not import models from '{api_version}': {e}"
|
|
427
|
+
) from e
|
|
428
|
+
|
|
429
|
+
interface_base = str(Lexeme(interface).singular)
|
|
430
|
+
model_name = f"{interface_base.title().replace('_', '')}PluginModel"
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
model_class = getattr(module, model_name)
|
|
434
|
+
print("model class \t", model_class)
|
|
435
|
+
except AttributeError as e:
|
|
436
|
+
raise ValueError(
|
|
437
|
+
f"Model '{model_name}' not found in '{api_version}'"
|
|
438
|
+
) from e
|
|
439
|
+
|
|
440
|
+
return model_class.model_validate(data)
|
|
441
|
+
|
|
442
|
+
def get_yaml_plugin(self, interface_obj, name, rebuild_registries=None):
|
|
443
|
+
"""Get a YAML plugin by its name.
|
|
444
|
+
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
interface_obj: Interface Object
|
|
448
|
+
- The object representing the interface class requesting this yaml plugin.
|
|
449
|
+
name: str or tuple(str)
|
|
450
|
+
- The name of the yaml-based plugin. Either a single string or a tuple of
|
|
451
|
+
strings for product plugins.
|
|
452
|
+
rebuild_registries: bool (default=None)
|
|
453
|
+
- Whether or not to rebuild the registries if get_plugin fails. If set to
|
|
454
|
+
None, default to True. If specified, use the input value of
|
|
455
|
+
rebuild_registries, which should be a boolean value. If rebuild registries
|
|
456
|
+
is true and get_plugin fails, rebuild the plugin registry, call then call
|
|
457
|
+
get_plugin once more with rebuild_registries toggled off, so it only gets
|
|
458
|
+
rebuilt once.
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
registered_yaml_plugins = self.registered_plugins["yaml_based"]
|
|
462
|
+
except KeyError:
|
|
463
|
+
# Very likely could occur if registries haven't been built yet
|
|
464
|
+
err_str = (
|
|
465
|
+
"No YAML-based plugins found. There likely have been no plugin "
|
|
466
|
+
"registries built yet. If automatic registry creation has been disabled"
|
|
467
|
+
", please run 'pluginify create'."
|
|
468
|
+
)
|
|
469
|
+
self.retry_get_plugin(interface_obj, name, rebuild_registries, err_str)
|
|
470
|
+
|
|
471
|
+
if rebuild_registries is None:
|
|
472
|
+
rebuild_registries = interface_obj.rebuild_registries
|
|
473
|
+
elif not isinstance(rebuild_registries, bool):
|
|
474
|
+
raise TypeError(
|
|
475
|
+
"Error: Argument 'rebuild_registries' was specified but isn't a boolean"
|
|
476
|
+
f" value. Encountered this '{rebuild_registries}' instead."
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
interface_entry = registered_yaml_plugins[interface_obj.name]
|
|
480
|
+
# This occurs for product plugins
|
|
481
|
+
if isinstance(name, tuple):
|
|
482
|
+
source_name = name[0]
|
|
483
|
+
plg_name = name[1]
|
|
484
|
+
extra_info = f"under source_name '{source_name}', "
|
|
485
|
+
# This occurs for every other YAML plugin
|
|
486
|
+
else:
|
|
487
|
+
source_name = None
|
|
488
|
+
plg_name = name
|
|
489
|
+
extra_info = ""
|
|
490
|
+
|
|
491
|
+
if plg_name not in interface_entry.get(source_name, interface_entry):
|
|
492
|
+
err_str = (
|
|
493
|
+
f"Plugin '{plg_name}', {extra_info}"
|
|
494
|
+
f"from interface '{interface_obj.name}' "
|
|
495
|
+
f"appears to not exist."
|
|
496
|
+
f"\nCreate plugin, then call pluginify create."
|
|
497
|
+
)
|
|
498
|
+
return self.retry_get_plugin(
|
|
499
|
+
interface_obj, name, rebuild_registries, err_str
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
relpath = interface_entry.get(source_name, interface_entry)[plg_name]["relpath"]
|
|
503
|
+
package = interface_entry.get(source_name, interface_entry)[plg_name]["package"]
|
|
504
|
+
abspath = str(resources.files(package) / relpath)
|
|
505
|
+
|
|
506
|
+
# If abspath doesn't exist the registry is out of date with the actual
|
|
507
|
+
# contents of all, or a certain plugin package.
|
|
508
|
+
if not os.path.exists(abspath):
|
|
509
|
+
err_str = (
|
|
510
|
+
f"Products plugin source: '{source_name}', plugin: '{plg_name} "
|
|
511
|
+
f"exists in the registry but its corresponding file at '{abspath}' "
|
|
512
|
+
"cannot be found. Reinstall your package and re-run "
|
|
513
|
+
"'pluginify create'."
|
|
514
|
+
)
|
|
515
|
+
# This error should never occur, but we're adding error handling here
|
|
516
|
+
# just in case. The reason it will never occur is that, if the path
|
|
517
|
+
# to such plugin does not exist, when pluginify create is
|
|
518
|
+
# re-run that syncs up the path to the associated plugin. It cannot
|
|
519
|
+
# reach this point if the plugin name is invalid, so this point couldn't
|
|
520
|
+
# be hit twice
|
|
521
|
+
return self.retry_get_plugin(
|
|
522
|
+
interface_obj,
|
|
523
|
+
name,
|
|
524
|
+
rebuild_registries,
|
|
525
|
+
err_str,
|
|
526
|
+
PluginRegistryError,
|
|
527
|
+
)
|
|
528
|
+
with open(abspath, "r") as file:
|
|
529
|
+
documents = yaml.safe_load_all(file)
|
|
530
|
+
plugin_found = False
|
|
531
|
+
for plugin in documents:
|
|
532
|
+
# This occurs for product plugins
|
|
533
|
+
if source_name:
|
|
534
|
+
for product in plugin["spec"]["products"]:
|
|
535
|
+
if (
|
|
536
|
+
product["name"] == plg_name
|
|
537
|
+
and source_name in product["source_names"]
|
|
538
|
+
):
|
|
539
|
+
plugin_found = True
|
|
540
|
+
plugin = product
|
|
541
|
+
break
|
|
542
|
+
# Every other type of YAML plugin
|
|
543
|
+
else:
|
|
544
|
+
if plugin["name"] == plg_name:
|
|
545
|
+
plugin_found = True
|
|
546
|
+
|
|
547
|
+
if plugin_found:
|
|
548
|
+
break
|
|
549
|
+
|
|
550
|
+
if not plugin_found:
|
|
551
|
+
err_str = (
|
|
552
|
+
f"Error: {interface_obj.name} YAML plugin under name '{name}' could"
|
|
553
|
+
" not be found. Please ensure this plugin exists, and if it does, "
|
|
554
|
+
"run 'pluginify create'."
|
|
555
|
+
)
|
|
556
|
+
return self.retry_get_plugin(
|
|
557
|
+
interface_obj, name, rebuild_registries, err_str
|
|
558
|
+
)
|
|
559
|
+
plugin["interface"] = interface_obj.name
|
|
560
|
+
plugin["package"] = package
|
|
561
|
+
plugin["abspath"] = abspath
|
|
562
|
+
plugin["relpath"] = relpath
|
|
563
|
+
|
|
564
|
+
if getattr(interface_obj, "use_pydantic", False):
|
|
565
|
+
return self.load_plugin(plugin).model_dump()
|
|
566
|
+
else:
|
|
567
|
+
validated = interface_obj.validator.validate(plugin)
|
|
568
|
+
return interface_obj._plugin_yaml_to_obj(name, validated)
|
|
569
|
+
|
|
570
|
+
def get_yaml_plugins(self, interface_obj):
|
|
571
|
+
"""Retrieve all yaml plugin objects for this interface.
|
|
572
|
+
|
|
573
|
+
Parameters
|
|
574
|
+
----------
|
|
575
|
+
interface_obj: Interface Object
|
|
576
|
+
- The object representing the interface class requesting all plugins.
|
|
577
|
+
"""
|
|
578
|
+
plugins = []
|
|
579
|
+
registered_yaml_plugins = self.registered_yaml_based_plugins
|
|
580
|
+
if interface_obj.name not in registered_yaml_plugins:
|
|
581
|
+
LOG.debug("No plugins found for '%s' interface.", interface_obj.name)
|
|
582
|
+
return plugins
|
|
583
|
+
for name in registered_yaml_plugins[interface_obj.name].keys():
|
|
584
|
+
plugins.append(self.get_yaml_plugin(interface_obj, name))
|
|
585
|
+
return plugins
|
|
586
|
+
|
|
587
|
+
def get_class_plugin(self, interface_obj, name, rebuild_registries=None):
|
|
588
|
+
"""Retrieve a class plugin from this interface by name.
|
|
589
|
+
|
|
590
|
+
In pluginify's current state, a class plugin can be a derived plugin object
|
|
591
|
+
(I.e. legacy module-based plugins) or true class-based plugins which are not
|
|
592
|
+
derived.
|
|
593
|
+
|
|
594
|
+
Parameters
|
|
595
|
+
----------
|
|
596
|
+
interface_obj: Interface Object
|
|
597
|
+
- The object representing the interface class requesting this class plugin.
|
|
598
|
+
name: str
|
|
599
|
+
- The name the desired plugin.
|
|
600
|
+
rebuild_registries: bool (default=None)
|
|
601
|
+
- Whether or not to rebuild the registries if get_plugin fails. If set to
|
|
602
|
+
None, default to True. If specified, use the input value of
|
|
603
|
+
rebuild_registries, which should be a boolean value. If rebuild registries
|
|
604
|
+
is true and get_plugin fails, rebuild the plugin registry, call then call
|
|
605
|
+
get_plugin once more with rebuild_registries toggled off, so it only gets
|
|
606
|
+
rebuilt once.
|
|
607
|
+
|
|
608
|
+
Returns
|
|
609
|
+
-------
|
|
610
|
+
An object of type ``<interface>Plugin`` where ``<interface>`` is the name of
|
|
611
|
+
this interface.
|
|
612
|
+
|
|
613
|
+
Raises
|
|
614
|
+
------
|
|
615
|
+
PluginError
|
|
616
|
+
If the specified plugin isn't found within the interface.
|
|
617
|
+
"""
|
|
618
|
+
try:
|
|
619
|
+
registered_class_plugins = self.registered_plugins["class_based"]
|
|
620
|
+
except KeyError:
|
|
621
|
+
# Very likely could occur if registries haven't been built yet
|
|
622
|
+
err_str = f"No plugins found for '{interface_obj.name}' interface."
|
|
623
|
+
self.retry_get_plugin(interface_obj, name, rebuild_registries, err_str)
|
|
624
|
+
|
|
625
|
+
if rebuild_registries is None:
|
|
626
|
+
rebuild_registries = interface_obj.rebuild_registries
|
|
627
|
+
elif not isinstance(rebuild_registries, bool):
|
|
628
|
+
raise TypeError(
|
|
629
|
+
"Error: Argument 'rebuild_registries' was specified but isn't a boolean"
|
|
630
|
+
f" value. Encountered this '{rebuild_registries}' instead."
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
if name not in registered_class_plugins[interface_obj.name]:
|
|
634
|
+
err_str = (
|
|
635
|
+
f"Plugin '{name}', "
|
|
636
|
+
f"from interface '{interface_obj.name}' "
|
|
637
|
+
f"appears to not exist."
|
|
638
|
+
f"\nCreate plugin, then call pluginify create."
|
|
639
|
+
)
|
|
640
|
+
return self.retry_get_plugin(
|
|
641
|
+
interface_obj, name, rebuild_registries, err_str
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
package = registered_class_plugins[interface_obj.name][name]["package"]
|
|
645
|
+
relpath = registered_class_plugins[interface_obj.name][name]["relpath"]
|
|
646
|
+
module_path = os.path.splitext(relpath.replace("/", "."))[0]
|
|
647
|
+
module_path = f"{package}.{module_path}"
|
|
648
|
+
abspath = resources.files(package) / relpath
|
|
649
|
+
# If abspath doesn't exist the registry is out of date with the actual contents
|
|
650
|
+
# of all, or a certain plugin package.
|
|
651
|
+
if not os.path.exists(abspath):
|
|
652
|
+
err_str = (
|
|
653
|
+
f"Plugin '{name}' exists in the registry but its corresponding file at "
|
|
654
|
+
f"'{abspath}' cannot be found. Reinstall your package and re-run "
|
|
655
|
+
"'pluginify create'."
|
|
656
|
+
)
|
|
657
|
+
# This error should never occur, but we're adding error handling here
|
|
658
|
+
# just in case. The reason it will never occur is that, if the path
|
|
659
|
+
# to such plugin does not exist, when pluginify create is
|
|
660
|
+
# re-run that syncs up the path to the associated plugin. It cannot reach
|
|
661
|
+
# this point if the plugin name is invalid, so this point couldn't be hit
|
|
662
|
+
# twice
|
|
663
|
+
return self.retry_get_plugin(
|
|
664
|
+
interface_obj, name, rebuild_registries, err_str, PluginRegistryError
|
|
665
|
+
)
|
|
666
|
+
spec = util.spec_from_file_location(module_path, abspath)
|
|
667
|
+
module = util.module_from_spec(spec)
|
|
668
|
+
spec.loader.exec_module(module)
|
|
669
|
+
|
|
670
|
+
if registered_class_plugins[interface_obj.name][name]["is_derived_from_module"]:
|
|
671
|
+
# If the requested plugin was derived from a module, use the class factory
|
|
672
|
+
# code.
|
|
673
|
+
plugin = interface_obj._plugin_module_to_obj(name, module)
|
|
674
|
+
else:
|
|
675
|
+
# Otherwise, just grab the associated plugin class and instantiate it.
|
|
676
|
+
PLUGIN_CLASS = getattr(module, "PLUGIN_CLASS")
|
|
677
|
+
plugin = PLUGIN_CLASS(module)
|
|
678
|
+
# Set attributes required to test the interface of a given plugin
|
|
679
|
+
plugin.id = plugin.name
|
|
680
|
+
plugin.docstring = plugin.__doc__
|
|
681
|
+
# This function might raise a PluginError with pertinent information on why
|
|
682
|
+
# the plugin is invalid. Don't catch that, we want the error to be raised.
|
|
683
|
+
interface_obj.plugin_is_valid(plugin)
|
|
684
|
+
return plugin
|
|
685
|
+
|
|
686
|
+
def get_class_plugins(self, interface_obj):
|
|
687
|
+
"""Retrieve all class plugins for this interface.
|
|
688
|
+
|
|
689
|
+
In pluginify's current state, this will grab all true class-based plugins and
|
|
690
|
+
those plugins who were derived into a plugin object from a legacy module-based
|
|
691
|
+
plugin.
|
|
692
|
+
|
|
693
|
+
Parameters
|
|
694
|
+
----------
|
|
695
|
+
interface_obj: Interface Object
|
|
696
|
+
- The object representing the interface class requesting all plugins.
|
|
697
|
+
"""
|
|
698
|
+
plugins = []
|
|
699
|
+
# All plugin interfaces are explicitly imported in
|
|
700
|
+
# pluginify/interfaces/__init__.py
|
|
701
|
+
# self.name comes explicitly from one of the interfaces that are
|
|
702
|
+
# found by default on pluginify.interfaces.
|
|
703
|
+
# If there is a defined interface with no plugins available in the current
|
|
704
|
+
# pluginify installation (in any currently installed plugin package),
|
|
705
|
+
# then there will NOT be an entry within registered plugins
|
|
706
|
+
# for that interface, and a KeyError will be raised in the for loop
|
|
707
|
+
# below.
|
|
708
|
+
# Check if the current interface (self.name) is found in the
|
|
709
|
+
# registered_plugins dictionary - if it is not, that means there
|
|
710
|
+
# are no plugins for that interface, so return an empty list.
|
|
711
|
+
|
|
712
|
+
registered_class_plugins = self.registered_plugins["class_based"]
|
|
713
|
+
if interface_obj.name not in registered_class_plugins:
|
|
714
|
+
LOG.debug("No plugins found for '%s' interface.", interface_obj.name)
|
|
715
|
+
return plugins
|
|
716
|
+
|
|
717
|
+
for plugin_name in registered_class_plugins[interface_obj.name]:
|
|
718
|
+
try:
|
|
719
|
+
plugins.append(self.get_class_plugin(interface_obj, plugin_name))
|
|
720
|
+
except AttributeError as resp:
|
|
721
|
+
raise PluginError(
|
|
722
|
+
f"Plugin '{plugin_name}' is missing the 'name' attribute, "
|
|
723
|
+
f"\nfrom package '{plugin_name['package']},' "
|
|
724
|
+
f"'{plugin_name['relpath']}' module,"
|
|
725
|
+
) from resp
|
|
726
|
+
|
|
727
|
+
return plugins
|
|
728
|
+
|
|
729
|
+
def retry_get_plugin(
|
|
730
|
+
self, interface_obj, name, rebuild_registries, err_str, err_type=PluginError
|
|
731
|
+
):
|
|
732
|
+
"""Rerun self.get_plugin, but call 'pluginify create' beforehand.
|
|
733
|
+
|
|
734
|
+
By running 'pluginify create', we automate the registration of plugins under a
|
|
735
|
+
given namespace (default='pluginify.plugin_packages'). If the plugin
|
|
736
|
+
persists not to be found, then we'll raise an appropriate PluginError as denoted
|
|
737
|
+
by 'err_str'.
|
|
738
|
+
|
|
739
|
+
Parameters
|
|
740
|
+
----------
|
|
741
|
+
interface_obj: Interface Object
|
|
742
|
+
- The object representing the interface class requesting this plugin.
|
|
743
|
+
name: str or tuple(str)
|
|
744
|
+
- The name of the yaml plugin. Either a single string or a tuple of strings
|
|
745
|
+
for product plugins.
|
|
746
|
+
rebuild_registries: bool
|
|
747
|
+
- Whether or not to rebuild the registries if get_plugin fails. If set to
|
|
748
|
+
true and get_plugin fails, rebuild the plugin registry, call then call
|
|
749
|
+
get_plugin once more with rebuild_registries toggled off, so it only gets
|
|
750
|
+
rebuilt once.
|
|
751
|
+
err_str: string
|
|
752
|
+
- The error to be reported.
|
|
753
|
+
err_type: Exception-based Class
|
|
754
|
+
- The class of exception to be raised.
|
|
755
|
+
"""
|
|
756
|
+
if rebuild_registries:
|
|
757
|
+
LOG.info(
|
|
758
|
+
"Running 'pluginify create' due to a missing plugin "
|
|
759
|
+
f"located under interface: '{interface_obj.name}', plugin_name: "
|
|
760
|
+
f"'{name}'."
|
|
761
|
+
)
|
|
762
|
+
self.create_registries()
|
|
763
|
+
# Force a rebuild of the master 'registered_plugins' dictionary.
|
|
764
|
+
self._set_class_properties(force_reset=True)
|
|
765
|
+
# This is done as some interface classes override 'get_plugin' with
|
|
766
|
+
# additional parameters it it's call signature. We only want to call
|
|
767
|
+
# BaseYamlInterface or BaseModuleInterface 'get_plugin', then return such
|
|
768
|
+
# information to the child class which overrode 'get_plugin'. Implementing
|
|
769
|
+
# it this way ensures that will happen.
|
|
770
|
+
base_interface_class = interface_obj.__class__.__base__()
|
|
771
|
+
base_interface_class.name = interface_obj.name
|
|
772
|
+
return base_interface_class.get_plugin(name, rebuild_registries=False)
|
|
773
|
+
else:
|
|
774
|
+
raise err_type(err_str)
|
|
775
|
+
|
|
776
|
+
def create_registries(self, packages=None, save_type="json") -> None:
|
|
777
|
+
"""Create one or more plugin registry files.
|
|
778
|
+
|
|
779
|
+
By default, this command will create all plugin registry files for all
|
|
780
|
+
installed plugin packages under a given namespace
|
|
781
|
+
(default=pluginify.plugin_packages). If packages is provided via the argument
|
|
782
|
+
above, create registry files associated with each of those packages.
|
|
783
|
+
|
|
784
|
+
Parameters
|
|
785
|
+
----------
|
|
786
|
+
packages: list[str], default=None
|
|
787
|
+
- A list of plugin package names corresponding to a namespace whose
|
|
788
|
+
registries we want to create.
|
|
789
|
+
save_type: str, default="json"
|
|
790
|
+
- Format to write registries to. This will also be the file extension. Valid
|
|
791
|
+
options are either 'json' or 'yaml'.
|
|
792
|
+
|
|
793
|
+
Raises
|
|
794
|
+
------
|
|
795
|
+
ValueError:
|
|
796
|
+
- Raised if 'save_type' is provided but is not one of ['json', 'yaml']
|
|
797
|
+
TypeError:
|
|
798
|
+
- Raised if packages is provided and is not a list of strings.
|
|
799
|
+
PluginRegistryError:
|
|
800
|
+
- Raised if one or more of the packages provided is not a valid
|
|
801
|
+
plugin package under the provided namespace, or if the associated
|
|
802
|
+
namespace provided is not a valid namespace.
|
|
803
|
+
"""
|
|
804
|
+
if save_type not in ["json", "yaml"]:
|
|
805
|
+
raise ValueError(
|
|
806
|
+
"Error: 'save_type' kwarg was provided but is not one of "
|
|
807
|
+
"['json', 'yaml']."
|
|
808
|
+
)
|
|
809
|
+
plugin_packages = metadata.entry_points(group=self.namespace)
|
|
810
|
+
if len(plugin_packages) == 0:
|
|
811
|
+
raise PluginRegistryError(
|
|
812
|
+
f"Error: namespace '{self.namespace}' does not exist or has no plugin "
|
|
813
|
+
"packages associated with it. Please resolve this before continuing."
|
|
814
|
+
)
|
|
815
|
+
if packages:
|
|
816
|
+
self._validate_packages_input(packages)
|
|
817
|
+
filtered_packages = []
|
|
818
|
+
for pkg in plugin_packages:
|
|
819
|
+
if pkg.value in packages:
|
|
820
|
+
filtered_packages.append(pkg)
|
|
821
|
+
plugin_packages = filtered_packages
|
|
822
|
+
|
|
823
|
+
LOG.debug(plugin_packages)
|
|
824
|
+
create_plugin_registries(plugin_packages, save_type, self.namespace)
|
|
825
|
+
|
|
826
|
+
def delete_registries(self, packages=None) -> None:
|
|
827
|
+
"""Delete one or more plugin registry files.
|
|
828
|
+
|
|
829
|
+
By default, this command will delete all plugin registry files found in all
|
|
830
|
+
installed plugin packages packages (default=pluginify.plugin_packages). If
|
|
831
|
+
packages is provided via the argument above, delete the registry file(s)
|
|
832
|
+
associated with each of those packages.
|
|
833
|
+
|
|
834
|
+
Parameters
|
|
835
|
+
----------
|
|
836
|
+
packages: list[str], default=None
|
|
837
|
+
- A list of plugin package names corresponding to a namespace whose
|
|
838
|
+
registries we want to delete.
|
|
839
|
+
|
|
840
|
+
Raises
|
|
841
|
+
------
|
|
842
|
+
TypeError:
|
|
843
|
+
- Raised if packages is provided and is not a list of strings.
|
|
844
|
+
FileNotFoundError:
|
|
845
|
+
- Raised if a registry file could not be found in one or more plugin
|
|
846
|
+
packages under self.namespace.
|
|
847
|
+
PluginRegistryError:
|
|
848
|
+
- Raised if one or more of the packages provided is not a valid
|
|
849
|
+
plugin package under the provided namespace, or if the associated
|
|
850
|
+
namespace provided is not a valid namespace.
|
|
851
|
+
"""
|
|
852
|
+
if packages:
|
|
853
|
+
self._validate_packages_input(packages)
|
|
854
|
+
else:
|
|
855
|
+
packages = [ep.value for ep in metadata.entry_points(group=self.namespace)]
|
|
856
|
+
if len(packages) == 0:
|
|
857
|
+
raise PluginRegistryError(
|
|
858
|
+
f"Error: namespace '{self.namespace}' does not exist or has no "
|
|
859
|
+
"plugin packages associated with it. Please resolve this before "
|
|
860
|
+
"continuing."
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
for pkg in packages:
|
|
864
|
+
write_dir = get_registry_cache_dir(self.namespace, pkg)
|
|
865
|
+
# If packages is provided and the current package is in that list, or if
|
|
866
|
+
# we're using the default value for that argument, delete the associated
|
|
867
|
+
# registry
|
|
868
|
+
if (packages and pkg in packages) or packages is None:
|
|
869
|
+
yaml_plug_path = str(write_dir / "registered_plugins.yaml")
|
|
870
|
+
json_plug_path = str(write_dir / "registered_plugins.json")
|
|
871
|
+
for path in [json_plug_path, yaml_plug_path]:
|
|
872
|
+
# Attempt to remove the files, pass silently if they don't exist.
|
|
873
|
+
try:
|
|
874
|
+
os.remove(path)
|
|
875
|
+
print(f"Removed registry file @ {path} from package '{pkg}'.")
|
|
876
|
+
except FileNotFoundError:
|
|
877
|
+
print(
|
|
878
|
+
f"Unable to remove registry file @ {path} from package "
|
|
879
|
+
f"'{pkg}'."
|
|
880
|
+
)
|
|
881
|
+
continue
|
|
882
|
+
|
|
883
|
+
def _validate_packages_input(self, packages):
|
|
884
|
+
"""Validate that packages is a list of strings.
|
|
885
|
+
|
|
886
|
+
If not, then raise a TypeError indicating what was formatted incorrectly.
|
|
887
|
+
|
|
888
|
+
Parameters
|
|
889
|
+
----------
|
|
890
|
+
packages: list[str], default=None
|
|
891
|
+
- A list of plugin package names corresponding to a namespace whose
|
|
892
|
+
registries we want to delete.
|
|
893
|
+
|
|
894
|
+
Raises
|
|
895
|
+
------
|
|
896
|
+
TypeError:
|
|
897
|
+
- Raised if packages is provided and is not a list of strings.
|
|
898
|
+
PluginRegistryError:
|
|
899
|
+
- Raised if one or more of the packages provided is not a valid
|
|
900
|
+
plugin package under the provided namespace, or if the associated
|
|
901
|
+
namespace provided is not a valid namespace.
|
|
902
|
+
"""
|
|
903
|
+
if not isinstance(packages, list):
|
|
904
|
+
raise TypeError(
|
|
905
|
+
"Error: 'packages' kwarg was provided but it is not a list object."
|
|
906
|
+
)
|
|
907
|
+
elif any([not isinstance(pkg, str) for pkg in packages]):
|
|
908
|
+
raise TypeError(
|
|
909
|
+
"Error: 'packages' kwarg was provided but one or more of it's "
|
|
910
|
+
"items were not a string."
|
|
911
|
+
)
|
|
912
|
+
# If any package name in 'packages' could not be associated with an entry point
|
|
913
|
+
# found in pypi's package registry associated with 'self.namespace', raise a
|
|
914
|
+
# plugin registry error.
|
|
915
|
+
found_packages = [
|
|
916
|
+
pkg.value for pkg in metadata.entry_points(group=self.namespace)
|
|
917
|
+
]
|
|
918
|
+
if any([pkg_name not in found_packages for pkg_name in packages]):
|
|
919
|
+
raise PluginRegistryError(
|
|
920
|
+
"Error: either the namespace provided was invalid or one or more of "
|
|
921
|
+
"the packages whose registry you requested to delete does not exist."
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
plugin_registry = PluginRegistry(NAMESPACE)
|