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 CHANGED
@@ -3,5 +3,5 @@
3
3
 
4
4
  # DO NOT EDIT
5
5
  # managed by poetry-dynamic-versioning
6
- __version__ = "0.0.0.post1.dev0"
7
- __version_tuple__ = (0, 0, 0, "post1", "dev0")
6
+ __version__ = "0.1.0"
7
+ __version_tuple__ = (0, 1, 0)
@@ -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
- return
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(context_settings={"help_option_names": ["-h", "--help"]})
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
- app.add_typer(config_app, name="config", help="Configuration commands for pluginify.")
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 registry files for one or more packages under a given namespace.
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 registry files for one or more packages under a given namespace.
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
- @config_app.command("set-rebuild-registries")
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
- @config_app.command("set-namespace")
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
- @config_app.command("set-registry-directory")
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.0.0.post1.dev0
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,<3.13.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
- In the future:
54
+ OR
53
55
  ```bash
54
- pip install pluginify
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=I_xg_zrKOMl6Y8jLXjH6TifTHdty9HEaI8c-DJniO18,237
3
- pluginify/commandline_typer.py,sha256=v9r7oBnD39pFLBqhoUNefGEwyRFCzDKpiPa3cvXXzCk,8676
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-0.0.0.post1.dev0.dist-info/METADATA,sha256=YoQUcPZOkt58ozii6zhO33T9-N11vPIxl-uxXl1aWsU,2086
19
- pluginify-0.0.0.post1.dev0.dist-info/WHEEL,sha256=Vz2fHgx6HFtSwhs8KvkHLqH5Ea4w1_rner5uNVGCeIE,88
20
- pluginify-0.0.0.post1.dev0.dist-info/entry_points.txt,sha256=kCm7HOEEzSnPyDpy98W-LYYmlLYyTnRqOj40UqxX6-w,111
21
- pluginify-0.0.0.post1.dev0.dist-info/licenses/LICENSE,sha256=sIBqTiU78gw5bC9MZMySNxzCE7DIlOcDqUWdmflkDsY,7266
22
- pluginify-0.0.0.post1.dev0.dist-info/RECORD,,
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,,