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
pluginify/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# # # This source code is subject to the license referenced at
|
|
2
|
+
# # # https://github.com/NRLMMD-GEOIPS.
|
|
3
|
+
|
|
4
|
+
"""Pluginify __init__ module."""
|
|
5
|
+
|
|
6
|
+
# NOTE: _version.py is generated automatically during build/install
|
|
7
|
+
from ._version import __version__, __version_tuple__
|
|
8
|
+
from pluginify import interfaces
|
|
9
|
+
from pluginify import utils
|
|
10
|
+
|
|
11
|
+
__all__ = ["interfaces", "utils", "__version__", "__version_tuple__"]
|
pluginify/_version.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Commandline module for Pluginify.
|
|
2
|
+
|
|
3
|
+
Supports two main commands, `pluginify create` and `pluginify delete` as well as
|
|
4
|
+
configuration commands `pluginify config set-namespace`,
|
|
5
|
+
`pluginify config set-rebuild-registries` and
|
|
6
|
+
`pluginify config set-registry-directory`.
|
|
7
|
+
|
|
8
|
+
The main commands create / delete plugin registries for one or more packages under a
|
|
9
|
+
given namespace of a certain file type [.json, .yaml]. If deleting, both file types are
|
|
10
|
+
deleted if found.
|
|
11
|
+
|
|
12
|
+
The config commands set configuration variables which direct pluginify where to look
|
|
13
|
+
for plugins, whether or not it should rebuild registries by default, and where it should
|
|
14
|
+
write registry files to.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import inspect
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from platformdirs import user_config_dir
|
|
22
|
+
import sys
|
|
23
|
+
from typing import List, Literal, Optional
|
|
24
|
+
|
|
25
|
+
import docstring_parser
|
|
26
|
+
import typer
|
|
27
|
+
import yaml
|
|
28
|
+
|
|
29
|
+
from pluginify.config import NAMESPACE
|
|
30
|
+
from pluginify.plugin_registry import PluginRegistry
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def configure_logging(level=logging.INFO):
|
|
34
|
+
"""Configure root logging for the command-line interface.
|
|
35
|
+
|
|
36
|
+
This function attaches a StreamHandler to the root logger so that all log
|
|
37
|
+
messages emitted by this process—including logs from imported modules,
|
|
38
|
+
dynamically loaded plugins, and third-party libraries—are routed to the
|
|
39
|
+
terminal.
|
|
40
|
+
|
|
41
|
+
Logging is configured only once. If handlers are already attached to the
|
|
42
|
+
root logger, this function exits without making changes to avoid duplicate
|
|
43
|
+
log output.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
level : int, optional
|
|
48
|
+
Logging level to apply to the root logger and handler. Defaults to
|
|
49
|
+
logging.INFO. Common values include logging.DEBUG, logging.INFO,
|
|
50
|
+
logging.WARNING, and logging.ERROR.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
None
|
|
55
|
+
"""
|
|
56
|
+
root = logging.getLogger()
|
|
57
|
+
|
|
58
|
+
# Avoid duplicate handlers if CLI is called multiple times
|
|
59
|
+
if root.handlers:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
root.setLevel(level)
|
|
63
|
+
|
|
64
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
65
|
+
formatter = logging.Formatter("%(levelname)s | %(name)s | %(message)s")
|
|
66
|
+
handler.setFormatter(formatter)
|
|
67
|
+
|
|
68
|
+
root.addHandler(handler)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def update_existing_fields(new_data):
|
|
72
|
+
"""Overwrite fields in pluginify's config file if they exist in 'new_data'.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
new_data: dict
|
|
77
|
+
A dictionary containing configuration key, value pairs to overwrite data in
|
|
78
|
+
config_path.
|
|
79
|
+
"""
|
|
80
|
+
config_path = Path(user_config_dir("pluginify")) / "config.yaml"
|
|
81
|
+
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
|
82
|
+
|
|
83
|
+
# Create the file if missing, then write to it
|
|
84
|
+
if not os.path.exists(config_path):
|
|
85
|
+
for key, value in new_data.items():
|
|
86
|
+
print(f"Setting pluginify.config.{key}={value}")
|
|
87
|
+
with open(config_path, "w") as f:
|
|
88
|
+
yaml.safe_dump(new_data, f, default_flow_style=False)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Otherwise read it and overwrite the items that match
|
|
92
|
+
with open(config_path, "r") as f:
|
|
93
|
+
current_data = yaml.safe_load(f) or {}
|
|
94
|
+
|
|
95
|
+
updated = False
|
|
96
|
+
for key, value in new_data.items():
|
|
97
|
+
if key in ["NAMESPACE", "REBUILD_REGISTRIES", "REGISTRY_DIRECTORY"]:
|
|
98
|
+
print(f"Setting pluginify.config.{key}={value}")
|
|
99
|
+
current_data[key] = value
|
|
100
|
+
updated = True
|
|
101
|
+
|
|
102
|
+
# Overwrite only if changes were made
|
|
103
|
+
if updated:
|
|
104
|
+
with open(config_path, "w") as f:
|
|
105
|
+
yaml.safe_dump(current_data, f, default_flow_style=False)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DocstringTyper(typer.Typer):
|
|
109
|
+
"""Subclass of typer.Typer that generates help messages from a command's docstring.
|
|
110
|
+
|
|
111
|
+
Provided that a docstring has a 'parameters' section, all help messages for required
|
|
112
|
+
or optional arguments will be automatically generated.
|
|
113
|
+
|
|
114
|
+
Additionally, shorthand flags will be generated for all flag-based arguments. I.e.
|
|
115
|
+
'--namespace' automatically generates a '-n' flag.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def command(self, *args, **kwargs):
|
|
119
|
+
"""Overridden typer.Typer:command decorator."""
|
|
120
|
+
decorator = super().command(*args, **kwargs)
|
|
121
|
+
|
|
122
|
+
def wrapper(func):
|
|
123
|
+
"""Generate help messages and shorthand aliases for all args of func."""
|
|
124
|
+
sig = inspect.signature(func)
|
|
125
|
+
doc = docstring_parser.parse(func.__doc__ or "")
|
|
126
|
+
|
|
127
|
+
param_help = {p.arg_name: p.description for p in doc.params}
|
|
128
|
+
|
|
129
|
+
# Replace docstring so Typer doesn't show the Parameters section
|
|
130
|
+
new_doc = doc.short_description or ""
|
|
131
|
+
if doc.long_description:
|
|
132
|
+
new_doc += "\n\n" + doc.long_description
|
|
133
|
+
|
|
134
|
+
func.__doc__ = new_doc
|
|
135
|
+
|
|
136
|
+
new_params = []
|
|
137
|
+
|
|
138
|
+
for name, param in sig.parameters.items():
|
|
139
|
+
|
|
140
|
+
default = param.default
|
|
141
|
+
help_text = param_help.get(name)
|
|
142
|
+
|
|
143
|
+
# Required arguments
|
|
144
|
+
if default is inspect._empty:
|
|
145
|
+
argument = typer.Argument(
|
|
146
|
+
...,
|
|
147
|
+
help=help_text,
|
|
148
|
+
)
|
|
149
|
+
new_params.append(param.replace(default=argument))
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Optional arguments
|
|
153
|
+
short = f"-{name[0]}"
|
|
154
|
+
long = f"--{name.replace('_','-')}"
|
|
155
|
+
|
|
156
|
+
option = typer.Option(default, short, long, help=help_text)
|
|
157
|
+
|
|
158
|
+
new_params.append(param.replace(default=option))
|
|
159
|
+
|
|
160
|
+
new_sig = sig.replace(parameters=new_params)
|
|
161
|
+
func.__signature__ = new_sig
|
|
162
|
+
|
|
163
|
+
return decorator(func)
|
|
164
|
+
|
|
165
|
+
return wrapper
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
app = DocstringTyper(context_settings={"help_option_names": ["-h", "--help"]})
|
|
169
|
+
config_app = DocstringTyper()
|
|
170
|
+
|
|
171
|
+
app.add_typer(config_app, name="config", help="Configuration commands for pluginify.")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@app.command()
|
|
175
|
+
def create(
|
|
176
|
+
namespace: str = NAMESPACE,
|
|
177
|
+
packages: Optional[List[str]] = None,
|
|
178
|
+
save_type: Literal["json", "yaml"] = "json",
|
|
179
|
+
):
|
|
180
|
+
"""Create plugin registry files for one or more packages under a given namespace.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
namespace: str, default='pluginify.plugin_packages'
|
|
185
|
+
The namespace which plugin packages are registered under.
|
|
186
|
+
packages: Optional[List[str]], default=None
|
|
187
|
+
A list of strings representing plugin packages to create registries for. If
|
|
188
|
+
None, create registry files for all plugin packages found under 'namespace'.
|
|
189
|
+
save_type: Literal['json', 'yaml'], default='json'
|
|
190
|
+
The file extension to save the registry files as. Defaults to 'json', 'yaml'
|
|
191
|
+
can be specified as well.
|
|
192
|
+
"""
|
|
193
|
+
plugin_registry = PluginRegistry(namespace=namespace)
|
|
194
|
+
plugin_registry.create_registries(packages=packages, save_type=save_type)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command()
|
|
198
|
+
def delete(
|
|
199
|
+
namespace: str = NAMESPACE,
|
|
200
|
+
packages: Optional[List[str]] = None,
|
|
201
|
+
):
|
|
202
|
+
"""Delete plugin registry files for one or more packages under a given namespace.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
namespace: str, default='pluginify.plugin_packages'
|
|
207
|
+
The namespace which plugin packages are registered under.
|
|
208
|
+
packages: Optional[List[str]], default=None
|
|
209
|
+
A list of strings representing plugin packages to create registries for. If
|
|
210
|
+
None, create registry files for all plugin packages found under 'namespace'.
|
|
211
|
+
"""
|
|
212
|
+
plugin_registry = PluginRegistry(namespace=namespace)
|
|
213
|
+
plugin_registry.delete_registries(packages=packages)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@config_app.command("set-rebuild-registries")
|
|
217
|
+
def set_rebuild_registries(rebuild_registries: bool):
|
|
218
|
+
"""Set pluginify's REBUILD_REGISTRIES config variable.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
rebuild_registries: bool
|
|
223
|
+
The default setting for whether or not pluginify should rebuild registries by
|
|
224
|
+
default. This will persist between terminal sessions if set.
|
|
225
|
+
"""
|
|
226
|
+
update_existing_fields({"REBUILD_REGISTRIES": rebuild_registries})
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@config_app.command("set-namespace")
|
|
230
|
+
def set_namespace(namespace: str):
|
|
231
|
+
"""Set pluginify's NAMESPACE config variable.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
namespace: str
|
|
236
|
+
The default namespace you want to pluginify to operate on. This will persist
|
|
237
|
+
between terminal sessions if set.
|
|
238
|
+
"""
|
|
239
|
+
update_existing_fields({"NAMESPACE": namespace})
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@config_app.command("set-registry-directory")
|
|
243
|
+
def set_registry_directory(registry_directory: Path):
|
|
244
|
+
"""Set pluginify's REGISTRY_DIRECTORY config variable.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
registry_directory: Path
|
|
249
|
+
The default path to the directory you want to write registry files to. This will
|
|
250
|
+
persist between terminal sessions if set.
|
|
251
|
+
"""
|
|
252
|
+
update_existing_fields({"REGISTRY_DIRECTORY": registry_directory})
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def main():
|
|
256
|
+
"""Entrypoint function for pluginify's CLI."""
|
|
257
|
+
configure_logging(logging.INFO)
|
|
258
|
+
app()
|
pluginify/config.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Module containing configuration variables needed for pluginify to run."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _get_env_name():
|
|
5
|
+
"""Determine the name of the environment that this code is being executed under.
|
|
6
|
+
|
|
7
|
+
Returns
|
|
8
|
+
-------
|
|
9
|
+
env_name: str
|
|
10
|
+
The name of the environment which was active when this code was ran. If running
|
|
11
|
+
under the base environment, return 'base_env'.
|
|
12
|
+
"""
|
|
13
|
+
# imports buried to avoid polluting this module's import namespace
|
|
14
|
+
from os import environ
|
|
15
|
+
from os.path import basename
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
conda_env = environ.get("CONDA_DEFAULT_ENV")
|
|
19
|
+
virtual_env = environ.get("VIRTUAL_ENV")
|
|
20
|
+
# Conda / Mamba
|
|
21
|
+
if conda_env:
|
|
22
|
+
env_name = conda_env
|
|
23
|
+
# venv / virtualenv
|
|
24
|
+
elif sys.prefix != getattr(sys, "base_prefix", sys.prefix):
|
|
25
|
+
env_name = basename(sys.prefix)
|
|
26
|
+
# virtualenv also sets this sometimes
|
|
27
|
+
elif virtual_env:
|
|
28
|
+
env_name = basename(virtual_env)
|
|
29
|
+
else:
|
|
30
|
+
env_name = "base_env"
|
|
31
|
+
|
|
32
|
+
return env_name
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _env_to_variable(name, default):
|
|
36
|
+
"""Convert an environment variable (str) to a python variable (Any).
|
|
37
|
+
|
|
38
|
+
If an environment variable under 'name' has been set, convert it to a python
|
|
39
|
+
variable under a new type. Since all environment variables must be strings, we
|
|
40
|
+
perform conversion so those variables can be cast into new types.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
name: str
|
|
45
|
+
The name of the environment variable to convert.
|
|
46
|
+
default: Any
|
|
47
|
+
The default value for the environment variable named 'name'..
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
converted: Any
|
|
52
|
+
- The converted value of the environment variable.
|
|
53
|
+
"""
|
|
54
|
+
# imports buried to avoid polluting this module's import namespace
|
|
55
|
+
from os import getenv
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
|
|
58
|
+
env_val = getenv(name)
|
|
59
|
+
|
|
60
|
+
if env_val:
|
|
61
|
+
|
|
62
|
+
if name == "PLUGINIFY_REGISTRY_DIRECTORY":
|
|
63
|
+
return Path(env_val)
|
|
64
|
+
elif name == "PLUGINIFY_REBUILD_REGISTRIES":
|
|
65
|
+
return False if env_val.lower() in ["0", "false"] else True
|
|
66
|
+
else:
|
|
67
|
+
# PLUGINIFY_NAMESPACE
|
|
68
|
+
return env_val
|
|
69
|
+
|
|
70
|
+
return default
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _load_config_values():
|
|
74
|
+
"""Load and override pluginify configuration variables if they exist.
|
|
75
|
+
|
|
76
|
+
The configuration file for this package is expected to be found at
|
|
77
|
+
`~/.config/pluginify/config.yaml`. If present, load the contents of that file and
|
|
78
|
+
look for 'NAMESPACE', 'REBUILD_REGISTRIES', 'REGISTRY_DIRECTORY' key, value pairs.
|
|
79
|
+
|
|
80
|
+
For each key, value pair present, override the default value set in this function
|
|
81
|
+
to what's been set in the configuration file.
|
|
82
|
+
"""
|
|
83
|
+
# imports buried to avoid polluting this module's import namespace
|
|
84
|
+
from os.path import exists
|
|
85
|
+
from pathlib import Path
|
|
86
|
+
from platformdirs import user_config_dir, user_cache_dir
|
|
87
|
+
|
|
88
|
+
from yaml import safe_load
|
|
89
|
+
|
|
90
|
+
config_path = Path(user_config_dir("pluginify")) / "config.yaml"
|
|
91
|
+
|
|
92
|
+
NAMESPACE = "pluginify.plugin_packages"
|
|
93
|
+
REBUILD_REGISTRIES = True
|
|
94
|
+
REGISTRY_DIRECTORY = Path(user_cache_dir(_get_env_name()))
|
|
95
|
+
|
|
96
|
+
# Override defaults with values set in config file if they exist
|
|
97
|
+
if exists(config_path):
|
|
98
|
+
with open(config_path, "r") as file_stream:
|
|
99
|
+
config = safe_load(file_stream)
|
|
100
|
+
|
|
101
|
+
if not isinstance(config, dict):
|
|
102
|
+
config = {}
|
|
103
|
+
|
|
104
|
+
NAMESPACE = config.get("NAMESPACE", NAMESPACE)
|
|
105
|
+
REBUILD_REGISTRIES = config.get("REBUILD_REGISTRIES", REBUILD_REGISTRIES)
|
|
106
|
+
REGISTRY_DIRECTORY = Path(config.get("REGISTRY_DIRECTORY", REGISTRY_DIRECTORY))
|
|
107
|
+
|
|
108
|
+
# Override defaults with values set as environment variables if they exist.
|
|
109
|
+
# This overrides configuration variables if they've been set.
|
|
110
|
+
NAMESPACE = _env_to_variable("PLUGINIFY_NAMESPACE", NAMESPACE)
|
|
111
|
+
REBUILD_REGISTRIES = _env_to_variable(
|
|
112
|
+
"PLUGINIFY_REBUILD_REGISTRIES", REBUILD_REGISTRIES
|
|
113
|
+
)
|
|
114
|
+
REGISTRY_DIRECTORY = _env_to_variable(
|
|
115
|
+
"PLUGINIFY_REGISTRY_DIRECTORY", REGISTRY_DIRECTORY
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return NAMESPACE, REBUILD_REGISTRIES, REGISTRY_DIRECTORY
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
NAMESPACE, REBUILD_REGISTRIES, REGISTRY_DIRECTORY = _load_config_values()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_registry_cache_dir(namespace, package):
|
|
125
|
+
"""Return the path to the parent directory of where to write registry files to.
|
|
126
|
+
|
|
127
|
+
Where the path is formatted '~/.cache/{env_name}/{namespace}/{package}'.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
namespace: str
|
|
132
|
+
Namespace that your plugin packages fall under. The argument parser defaults
|
|
133
|
+
this value to 'pluginify.plugin_packages', but a user can create separate
|
|
134
|
+
namespaces if developing interfaces outside of pluginify.
|
|
135
|
+
package: str
|
|
136
|
+
Name of a plugin package that is registered under 'namespace'.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
cache_dir: Path
|
|
141
|
+
Full path to the parent directory in which registry files should be written
|
|
142
|
+
for a package under namespace.
|
|
143
|
+
"""
|
|
144
|
+
return REGISTRY_DIRECTORY / namespace / package
|