pluginify 0.0.0.post1.dev0__py3-none-any.whl → 0.1.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.
- pluginify/_version.py +2 -2
- pluginify/commandline_typer.py +94 -10
- pluginify/utils/validators.py +183 -0
- {pluginify-0.0.0.post1.dev0.dist-info → pluginify-0.1.0.dist-info}/METADATA +6 -4
- {pluginify-0.0.0.post1.dev0.dist-info → pluginify-0.1.0.dist-info}/RECORD +8 -7
- {pluginify-0.0.0.post1.dev0.dist-info → pluginify-0.1.0.dist-info}/WHEEL +0 -0
- {pluginify-0.0.0.post1.dev0.dist-info → pluginify-0.1.0.dist-info}/entry_points.txt +0 -0
- {pluginify-0.0.0.post1.dev0.dist-info → pluginify-0.1.0.dist-info}/licenses/LICENSE +0 -0
pluginify/_version.py
CHANGED
pluginify/commandline_typer.py
CHANGED
|
@@ -56,8 +56,8 @@ def configure_logging(level=logging.INFO):
|
|
|
56
56
|
root = logging.getLogger()
|
|
57
57
|
|
|
58
58
|
# Avoid duplicate handlers if CLI is called multiple times
|
|
59
|
-
if root.handlers:
|
|
60
|
-
|
|
59
|
+
# if root.handlers:
|
|
60
|
+
# return
|
|
61
61
|
|
|
62
62
|
root.setLevel(level)
|
|
63
63
|
|
|
@@ -131,6 +131,10 @@ class DocstringTyper(typer.Typer):
|
|
|
131
131
|
if doc.long_description:
|
|
132
132
|
new_doc += "\n\n" + doc.long_description
|
|
133
133
|
|
|
134
|
+
# Keep examples portion in command's help output if specified
|
|
135
|
+
if doc.examples:
|
|
136
|
+
new_doc += "\n\nExamples\n--------\n" + doc.examples[0].description
|
|
137
|
+
|
|
134
138
|
func.__doc__ = new_doc
|
|
135
139
|
|
|
136
140
|
new_params = []
|
|
@@ -165,10 +169,32 @@ class DocstringTyper(typer.Typer):
|
|
|
165
169
|
return wrapper
|
|
166
170
|
|
|
167
171
|
|
|
168
|
-
app = DocstringTyper(
|
|
172
|
+
app = DocstringTyper(
|
|
173
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
174
|
+
help="pluginify command line interface.",
|
|
175
|
+
)
|
|
169
176
|
config_app = DocstringTyper()
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
config_set_app = DocstringTyper()
|
|
178
|
+
|
|
179
|
+
config_variable_help = (
|
|
180
|
+
"NAMESPACE specifies the default namespace in which pluginify will manage "
|
|
181
|
+
"registries.\n"
|
|
182
|
+
"REGISTRY_DIRECTORY specifies the base directory where registries and their "
|
|
183
|
+
"supporting directory structure will be written.\n"
|
|
184
|
+
"REBUILD_REGISTRIES informs pluginify whether or not to rebuild registries at "
|
|
185
|
+
"runtime if a plugin cannot be located."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
app.add_typer(
|
|
189
|
+
config_app,
|
|
190
|
+
name="config",
|
|
191
|
+
help=f"Configure pluginify.\n\n{config_variable_help}",
|
|
192
|
+
)
|
|
193
|
+
config_app.add_typer(
|
|
194
|
+
config_set_app,
|
|
195
|
+
name="set",
|
|
196
|
+
help=f"Set pluginify configuration variables.\n\n{config_variable_help}",
|
|
197
|
+
)
|
|
172
198
|
|
|
173
199
|
|
|
174
200
|
@app.command()
|
|
@@ -177,7 +203,37 @@ def create(
|
|
|
177
203
|
packages: Optional[List[str]] = None,
|
|
178
204
|
save_type: Literal["json", "yaml"] = "json",
|
|
179
205
|
):
|
|
180
|
-
"""Create plugin
|
|
206
|
+
"""Create plugin registries.
|
|
207
|
+
|
|
208
|
+
Plugin registries are low-level files which contain dictionaries of metadata for all
|
|
209
|
+
plugins implemented in one or more packages registered under a common namespace.
|
|
210
|
+
|
|
211
|
+
Pluginify creates and makes use of these registry files to properly load and
|
|
212
|
+
instantiate your plugins.
|
|
213
|
+
|
|
214
|
+
Examples
|
|
215
|
+
--------
|
|
216
|
+
Namespace: packageX.plugin_packages
|
|
217
|
+
Directory structure:
|
|
218
|
+
|
|
219
|
+
packageX/
|
|
220
|
+
plugins/
|
|
221
|
+
interfaces/
|
|
222
|
+
utils/
|
|
223
|
+
registered_plugins.json [will create]
|
|
224
|
+
packageY/
|
|
225
|
+
plugins/
|
|
226
|
+
registered_plugins.json [will create]
|
|
227
|
+
|
|
228
|
+
JSON registry files are faster to load and are used by default. YAML
|
|
229
|
+
extensions are also supported for easier viewing.
|
|
230
|
+
|
|
231
|
+
CLI examples
|
|
232
|
+
------------
|
|
233
|
+
pluginify create
|
|
234
|
+
pluginify create -n packageX.plugin_packages
|
|
235
|
+
pluginify create -s yaml
|
|
236
|
+
pluginify create -n packageX.plugin_packages -p packageY
|
|
181
237
|
|
|
182
238
|
Parameters
|
|
183
239
|
----------
|
|
@@ -199,7 +255,35 @@ def delete(
|
|
|
199
255
|
namespace: str = NAMESPACE,
|
|
200
256
|
packages: Optional[List[str]] = None,
|
|
201
257
|
):
|
|
202
|
-
"""Delete plugin
|
|
258
|
+
"""Delete plugin registries.
|
|
259
|
+
|
|
260
|
+
Plugin registries are low-level files which contain dictionaries of metadata for all
|
|
261
|
+
plugins implemented in one or more packages registered under a common namespace.
|
|
262
|
+
|
|
263
|
+
By default, this command will delete every .json and .yaml instance of registry
|
|
264
|
+
files for every package registered under a given namespace. If you only want to
|
|
265
|
+
delete registry files from a subset of packages, use the '-p' flag to specify those
|
|
266
|
+
packages.
|
|
267
|
+
|
|
268
|
+
Examples
|
|
269
|
+
--------
|
|
270
|
+
Namespace: packageX.plugin_packages
|
|
271
|
+
Directory structure:
|
|
272
|
+
|
|
273
|
+
packageX/
|
|
274
|
+
plugins/
|
|
275
|
+
interfaces/
|
|
276
|
+
utils/
|
|
277
|
+
registered_plugins.json [will delete]
|
|
278
|
+
packageY/
|
|
279
|
+
plugins/
|
|
280
|
+
registered_plugins.json [will delete]
|
|
281
|
+
|
|
282
|
+
CLI examples
|
|
283
|
+
------------
|
|
284
|
+
pluginify delete
|
|
285
|
+
pluginify delete -n packageX.plugin_packages
|
|
286
|
+
pluginify delete -n packageX.plugin_packages -p packageY
|
|
203
287
|
|
|
204
288
|
Parameters
|
|
205
289
|
----------
|
|
@@ -213,7 +297,7 @@ def delete(
|
|
|
213
297
|
plugin_registry.delete_registries(packages=packages)
|
|
214
298
|
|
|
215
299
|
|
|
216
|
-
@
|
|
300
|
+
@config_set_app.command("rebuild-registries")
|
|
217
301
|
def set_rebuild_registries(rebuild_registries: bool):
|
|
218
302
|
"""Set pluginify's REBUILD_REGISTRIES config variable.
|
|
219
303
|
|
|
@@ -226,7 +310,7 @@ def set_rebuild_registries(rebuild_registries: bool):
|
|
|
226
310
|
update_existing_fields({"REBUILD_REGISTRIES": rebuild_registries})
|
|
227
311
|
|
|
228
312
|
|
|
229
|
-
@
|
|
313
|
+
@config_set_app.command("namespace")
|
|
230
314
|
def set_namespace(namespace: str):
|
|
231
315
|
"""Set pluginify's NAMESPACE config variable.
|
|
232
316
|
|
|
@@ -239,7 +323,7 @@ def set_namespace(namespace: str):
|
|
|
239
323
|
update_existing_fields({"NAMESPACE": namespace})
|
|
240
324
|
|
|
241
325
|
|
|
242
|
-
@
|
|
326
|
+
@config_set_app.command("registry-directory")
|
|
243
327
|
def set_registry_directory(registry_directory: Path):
|
|
244
328
|
"""Set pluginify's REGISTRY_DIRECTORY config variable.
|
|
245
329
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Pluginify validators module.
|
|
2
|
+
|
|
3
|
+
Currently only implements a PluginRegistryValidator class.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib import metadata, import_module
|
|
7
|
+
import json
|
|
8
|
+
from pprint import pformat
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from pluginify.errors import PluginRegistryError
|
|
13
|
+
from pluginify.plugin_registry import PluginRegistry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PluginRegistryValidator(PluginRegistry):
|
|
17
|
+
"""Subclass of PluginRegistry which adds functionality for unit testing."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, namespace, fpaths=None):
|
|
20
|
+
"""Initialize TestPluginRegistry Class."""
|
|
21
|
+
self.namespace = namespace
|
|
22
|
+
super().__init__(self.namespace, _test_registry_files=fpaths)
|
|
23
|
+
|
|
24
|
+
def validate_plugin_types_exist(self, reg_dict, reg_path):
|
|
25
|
+
"""Test that all top level plugin types exist in each registry file."""
|
|
26
|
+
expected_plugin_types = ["yaml_based", "class_based", "text_based"]
|
|
27
|
+
for p_type in expected_plugin_types:
|
|
28
|
+
if p_type not in reg_dict:
|
|
29
|
+
error_str = f"Expected plugin type '{p_type}' to be in the registry but"
|
|
30
|
+
error_str += f" wasn't. This was in file '{reg_path}'."
|
|
31
|
+
raise PluginRegistryError(error_str)
|
|
32
|
+
|
|
33
|
+
def validate_all_registries(self):
|
|
34
|
+
"""Validate all registries in the current installation.
|
|
35
|
+
|
|
36
|
+
This should be run during testing, but not at runtime.
|
|
37
|
+
Ensure we do not fail catastrophically for a single bad plugin
|
|
38
|
+
at runtime, so test up front to test validity.
|
|
39
|
+
"""
|
|
40
|
+
for reg_path in self.registry_files:
|
|
41
|
+
pkg_plugins = json.load(open(reg_path, "r"))
|
|
42
|
+
self.validate_registry(pkg_plugins, reg_path)
|
|
43
|
+
|
|
44
|
+
def validate_registry(self, current_registry, fpath):
|
|
45
|
+
"""Test all plugins found in registered plugins for their validity."""
|
|
46
|
+
try:
|
|
47
|
+
self.validate_plugin_types_exist(current_registry, fpath)
|
|
48
|
+
except PluginRegistryError as e:
|
|
49
|
+
# xfail if this is a test, otherwise just raise PluginRegistryError
|
|
50
|
+
if self._is_test:
|
|
51
|
+
pytest.xfail(str(e))
|
|
52
|
+
else:
|
|
53
|
+
raise PluginRegistryError(e)
|
|
54
|
+
try:
|
|
55
|
+
self.validate_registry_interfaces(current_registry)
|
|
56
|
+
except PluginRegistryError as e:
|
|
57
|
+
# xfail if this is a test, otherwise just raise PluginRegistryError
|
|
58
|
+
if self._is_test:
|
|
59
|
+
pytest.xfail(str(e))
|
|
60
|
+
else:
|
|
61
|
+
raise PluginRegistryError(e)
|
|
62
|
+
for plugin_type in current_registry:
|
|
63
|
+
for interface in current_registry[plugin_type]:
|
|
64
|
+
for plugin in current_registry[plugin_type][interface]:
|
|
65
|
+
try:
|
|
66
|
+
if interface == "products":
|
|
67
|
+
for subplg in current_registry[plugin_type][interface][
|
|
68
|
+
plugin
|
|
69
|
+
]:
|
|
70
|
+
self.validate_plugin_attrs(
|
|
71
|
+
plugin_type,
|
|
72
|
+
interface,
|
|
73
|
+
(plugin, subplg),
|
|
74
|
+
current_registry[plugin_type][interface][plugin][
|
|
75
|
+
subplg
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
elif plugin_type != "text_based":
|
|
79
|
+
self.validate_plugin_attrs(
|
|
80
|
+
plugin_type,
|
|
81
|
+
interface,
|
|
82
|
+
plugin,
|
|
83
|
+
current_registry[plugin_type][interface][plugin],
|
|
84
|
+
)
|
|
85
|
+
except PluginRegistryError as e:
|
|
86
|
+
# xfail if this is a test,
|
|
87
|
+
# otherwise just raise PluginRegistryError
|
|
88
|
+
if self._is_test:
|
|
89
|
+
pytest.xfail(str(e))
|
|
90
|
+
else:
|
|
91
|
+
raise PluginRegistryError(e)
|
|
92
|
+
|
|
93
|
+
def validate_plugin_attrs(self, plugin_type, interface, name, plugin):
|
|
94
|
+
"""Test non-product plugin for all required attributes."""
|
|
95
|
+
missing = []
|
|
96
|
+
if plugin_type == "yaml_based" and interface != "products":
|
|
97
|
+
attrs = [
|
|
98
|
+
"docstring",
|
|
99
|
+
"family",
|
|
100
|
+
"interface",
|
|
101
|
+
"package",
|
|
102
|
+
"plugin_type",
|
|
103
|
+
"relpath",
|
|
104
|
+
]
|
|
105
|
+
elif plugin_type == "yaml_based":
|
|
106
|
+
attrs = [
|
|
107
|
+
"docstring",
|
|
108
|
+
"family",
|
|
109
|
+
"interface",
|
|
110
|
+
"package",
|
|
111
|
+
"plugin_type",
|
|
112
|
+
"product_defaults",
|
|
113
|
+
"source_names",
|
|
114
|
+
"relpath",
|
|
115
|
+
]
|
|
116
|
+
else:
|
|
117
|
+
attrs = [
|
|
118
|
+
"docstring",
|
|
119
|
+
"family",
|
|
120
|
+
"interface",
|
|
121
|
+
"package",
|
|
122
|
+
"plugin_type",
|
|
123
|
+
"signature",
|
|
124
|
+
"relpath",
|
|
125
|
+
]
|
|
126
|
+
for attr in attrs:
|
|
127
|
+
try:
|
|
128
|
+
plugin[attr]
|
|
129
|
+
except KeyError:
|
|
130
|
+
missing.append(attr)
|
|
131
|
+
if missing:
|
|
132
|
+
raise PluginRegistryError(
|
|
133
|
+
f"Plugin '{name}' is missing the following required "
|
|
134
|
+
f"top-level properties: '{missing}'"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def validate_registry_interfaces(self, current_registry):
|
|
138
|
+
"""Test Plugin Registry interfaces validity."""
|
|
139
|
+
yaml_interfaces = []
|
|
140
|
+
class_interfaces = []
|
|
141
|
+
|
|
142
|
+
# NOTE: all <pkg>/interfaces/__init__.py files MUST
|
|
143
|
+
# contain a "class_based_interfaces" list and a
|
|
144
|
+
# "yaml_based_interfaces" list - this is how we
|
|
145
|
+
# identify the valid interface names.
|
|
146
|
+
# We must avoid actually importing the interfaces within
|
|
147
|
+
# the pluginify repo - or we will end up with a circular import
|
|
148
|
+
# due to BaseInterface.
|
|
149
|
+
for pkg in metadata.entry_points(group=self.namespace):
|
|
150
|
+
try:
|
|
151
|
+
mod = import_module(f"{pkg.value}.interfaces")
|
|
152
|
+
except ModuleNotFoundError as resp:
|
|
153
|
+
if f"No module named '{pkg.value}.interfaces'" in str(resp):
|
|
154
|
+
continue
|
|
155
|
+
else:
|
|
156
|
+
raise ModuleNotFoundError(resp)
|
|
157
|
+
class_interfaces += mod.class_based_interfaces
|
|
158
|
+
yaml_interfaces += mod.yaml_based_interfaces
|
|
159
|
+
|
|
160
|
+
bad_interfaces = []
|
|
161
|
+
for plugin_type in ["class_based", "yaml_based"]:
|
|
162
|
+
for interface in current_registry[plugin_type]:
|
|
163
|
+
if (
|
|
164
|
+
interface not in class_interfaces
|
|
165
|
+
and interface not in yaml_interfaces
|
|
166
|
+
):
|
|
167
|
+
error_str = f"\nPlugin type '{plugin_type}' does not allow "
|
|
168
|
+
error_str += f"interface '{interface}'.\n"
|
|
169
|
+
error_str += "\nValid interfaces: "
|
|
170
|
+
if plugin_type == "class_based":
|
|
171
|
+
interface_list = class_interfaces
|
|
172
|
+
else:
|
|
173
|
+
interface_list = yaml_interfaces
|
|
174
|
+
error_str += f"\n{interface_list}\n"
|
|
175
|
+
error_str += "\nPlease update the following plugins "
|
|
176
|
+
error_str += "to use a valid interface:\n"
|
|
177
|
+
error_str += pformat(current_registry[plugin_type][interface])
|
|
178
|
+
bad_interfaces.append(error_str)
|
|
179
|
+
if bad_interfaces:
|
|
180
|
+
error_str = "The following interfaces were not valid:\n"
|
|
181
|
+
for error in bad_interfaces:
|
|
182
|
+
error_str += error
|
|
183
|
+
raise PluginRegistryError(error_str)
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pluginify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Pluginify package
|
|
5
5
|
License: LICENSE
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Author: Evan Rose
|
|
8
|
-
Requires-Python: >=3.11.0
|
|
8
|
+
Requires-Python: >=3.11.0
|
|
9
9
|
Classifier: License :: Other/Proprietary License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
15
|
Provides-Extra: doc
|
|
14
16
|
Requires-Dist: docstring_parser
|
|
15
17
|
Requires-Dist: jsonschema (>4.18.0)
|
|
@@ -49,9 +51,9 @@ git clone https://github.com/NRLMMD-GEOIPS/pluginify.git
|
|
|
49
51
|
# cd to pluginify's top level dir
|
|
50
52
|
pip install -e .
|
|
51
53
|
```
|
|
52
|
-
|
|
54
|
+
OR
|
|
53
55
|
```bash
|
|
54
|
-
|
|
56
|
+
pip install pluginify
|
|
55
57
|
```
|
|
56
58
|
|
|
57
59
|
Use pluginify
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
pluginify/__init__.py,sha256=NvdicJtfy-dZZ8tWjR74lDBHGV6rlaH8pAJ7UHyTTog,391
|
|
2
|
-
pluginify/_version.py,sha256=
|
|
3
|
-
pluginify/commandline_typer.py,sha256=
|
|
2
|
+
pluginify/_version.py,sha256=RNg2zFDCCvhcMXFL44hFlG_yi0gJFK6LnyG47hjjhqA,209
|
|
3
|
+
pluginify/commandline_typer.py,sha256=g_pT1V8xwAF9KK-ndE-ydIYk2ivZYoO24PBW80CjsoM,11257
|
|
4
4
|
pluginify/config.py,sha256=co4HkXsOrA68M0PlRaUAhz5KMp5pa60ssb7DwLvUuxk,4894
|
|
5
5
|
pluginify/create_plugin_registries.py,sha256=9NGFRzyZIaF7Mj1BMbNLTbyRVtnZWjmwSnCL0qnra_0,48494
|
|
6
6
|
pluginify/errors.py,sha256=kkdipLfxvj8f-Pp53SD3RPnD9QS3samBLlQBjBskZP8,373
|
|
@@ -15,8 +15,9 @@ pluginify/plugins/yaml/configs/stucco.yaml,sha256=OXLWzmmzxSAWs-MzSlmEa-OLGJzVJz
|
|
|
15
15
|
pluginify/pydantic_models/v1/configs.py,sha256=As15amNtdFwAobufh0d6mDlF3mMS23ThwTKQy9q25bc,2122
|
|
16
16
|
pluginify/utils/__init__.py,sha256=gdy4E4yEI9lrHbWPeBn0Bspj7iz4z6r8BAjbmHf_hpM,2088
|
|
17
17
|
pluginify/utils/context_managers.py,sha256=1-bREbijNNc5PrqMIihKqxlxNaK-YZ3hj5XGpHiYmfw,926
|
|
18
|
-
pluginify
|
|
19
|
-
pluginify-0.
|
|
20
|
-
pluginify-0.
|
|
21
|
-
pluginify-0.
|
|
22
|
-
pluginify-0.
|
|
18
|
+
pluginify/utils/validators.py,sha256=gCESprY4mZgE0KXigjsgHOipjZAg8OUF5n3-SGQ7PG4,7577
|
|
19
|
+
pluginify-0.1.0.dist-info/METADATA,sha256=kfK2YM_CTB5Sy1UWrDE_QG5eMAaa5_f7BhS61QdrYGA,2153
|
|
20
|
+
pluginify-0.1.0.dist-info/WHEEL,sha256=Vz2fHgx6HFtSwhs8KvkHLqH5Ea4w1_rner5uNVGCeIE,88
|
|
21
|
+
pluginify-0.1.0.dist-info/entry_points.txt,sha256=kCm7HOEEzSnPyDpy98W-LYYmlLYyTnRqOj40UqxX6-w,111
|
|
22
|
+
pluginify-0.1.0.dist-info/licenses/LICENSE,sha256=sIBqTiU78gw5bC9MZMySNxzCE7DIlOcDqUWdmflkDsY,7266
|
|
23
|
+
pluginify-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|