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 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,7 @@
1
+ # # # This source code is subject to the license referenced at
2
+ # # # https://github.com/NRLMMD-GEOIPS.
3
+
4
+ # DO NOT EDIT
5
+ # managed by poetry-dynamic-versioning
6
+ __version__ = "0.0.0.post1.dev0"
7
+ __version_tuple__ = (0, 0, 0, "post1", "dev0")
@@ -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