multitool 0.6.0__tar.gz → 0.7.1__tar.gz

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.
Files changed (30) hide show
  1. multitool-0.7.1/PKG-INFO +256 -0
  2. multitool-0.7.1/README.rst +233 -0
  3. multitool-0.7.1/multitool/__init__.py +26 -0
  4. {multitool-0.6.0 → multitool-0.7.1}/multitool/__main__.py +23 -12
  5. multitool-0.7.1/multitool/console.py +37 -0
  6. multitool-0.7.1/multitool/plugins/commands.py +425 -0
  7. {multitool-0.6.0 → multitool-0.7.1}/multitool/utils.py +56 -46
  8. multitool-0.7.1/multitool/utils_init.py +17 -0
  9. multitool-0.7.1/multitool.egg-info/PKG-INFO +256 -0
  10. {multitool-0.6.0 → multitool-0.7.1}/multitool.egg-info/SOURCES.txt +1 -0
  11. {multitool-0.6.0 → multitool-0.7.1}/pyproject.toml +2 -2
  12. multitool-0.6.0/PKG-INFO +0 -174
  13. multitool-0.6.0/README.rst +0 -151
  14. multitool-0.6.0/multitool/__init__.py +0 -23
  15. multitool-0.6.0/multitool/console.py +0 -25
  16. multitool-0.6.0/multitool/plugins/commands.py +0 -224
  17. multitool-0.6.0/multitool.egg-info/PKG-INFO +0 -174
  18. {multitool-0.6.0 → multitool-0.7.1}/LICENSE +0 -0
  19. {multitool-0.6.0 → multitool-0.7.1}/multitool/cls.py +0 -0
  20. {multitool-0.6.0 → multitool-0.7.1}/multitool/exceptions.py +0 -0
  21. {multitool-0.6.0 → multitool-0.7.1}/multitool/plugins/__init__.py +0 -0
  22. {multitool-0.6.0 → multitool-0.7.1}/multitool/silent.py +0 -0
  23. {multitool-0.6.0 → multitool-0.7.1}/multitool/verbose.py +0 -0
  24. {multitool-0.6.0 → multitool-0.7.1}/multitool.egg-info/dependency_links.txt +0 -0
  25. {multitool-0.6.0 → multitool-0.7.1}/multitool.egg-info/entry_points.txt +0 -0
  26. {multitool-0.6.0 → multitool-0.7.1}/multitool.egg-info/requires.txt +0 -0
  27. {multitool-0.6.0 → multitool-0.7.1}/multitool.egg-info/top_level.txt +0 -0
  28. {multitool-0.6.0 → multitool-0.7.1}/setup.cfg +0 -0
  29. {multitool-0.6.0 → multitool-0.7.1}/tests/__init__.py +0 -0
  30. {multitool-0.6.0 → multitool-0.7.1}/tests/test_utils.py +0 -0
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: multitool
3
+ Version: 0.7.1
4
+ Summary: Create and run plugin-based command-line tools.
5
+ Author-email: Matthew Delotavo <matthew.t.delotavo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mdelotavo/multitool
8
+ Project-URL: Repository, https://github.com/mdelotavo/multitool
9
+ Keywords: cli,tooling,automation
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Requires-Python: >=3.7
16
+ Description-Content-Type: text/x-rst
17
+ License-File: LICENSE
18
+ Requires-Dist: click>=8.1.3
19
+ Requires-Dist: click-aliases>=1.0.1
20
+ Requires-Dist: click-option-group>=0.5.5
21
+ Requires-Dist: GitPython>=3.1.30
22
+ Dynamic: license-file
23
+
24
+ multitool
25
+ =========
26
+
27
+ Create and run plugin-based command-line tools.
28
+
29
+ ----------
30
+ Quickstart
31
+ ----------
32
+
33
+ This section shows how to start prototyping plugins on your local machine.
34
+ See the sections below to learn how to distribute your commands as remote
35
+ plugins that can be installed and updated from Git repositories.
36
+
37
+ Create a new local plugin::
38
+
39
+ multitool plugins new test
40
+
41
+ Show the generated command help::
42
+
43
+ multitool run test hello --help
44
+
45
+ Run the example command::
46
+
47
+ multitool run test hello \
48
+ "Hello, World!" \
49
+ --count 3 \
50
+ --format json \
51
+ -vv \
52
+ --enabled \
53
+ --tag alpha \
54
+ --tag beta \
55
+ --output result.json
56
+
57
+ Edit the generated source code::
58
+
59
+ vim ~/.multitool/plugins/test/plugin_*.py
60
+
61
+ -----
62
+ Usage
63
+ -----
64
+
65
+ .. code-block:: text
66
+
67
+ Usage: multitool [OPTIONS] COMMAND [ARGS]...
68
+
69
+ Create and run plugin-based command-line tools.
70
+
71
+ Options:
72
+ -V, --version Show the version and exit.
73
+ -h, --help Show this message and exit.
74
+
75
+ Commands:
76
+ plugins Manage plugin repositories.
77
+ run Run installed plugin commands.
78
+
79
+ ----------------
80
+ Managing plugins
81
+ ----------------
82
+
83
+ Plugins are distributed as Git repositories containing Click commands.
84
+
85
+ The ``plugins`` command manages plugin repositories, while installed plugin
86
+ commands are available under ``multitool run``.
87
+
88
+ Git is required to install or update plugins. If Git is unavailable, the
89
+ ``plugins`` command cannot install repositories, although plugins can still be
90
+ installed manually by copying them into::
91
+
92
+ ~/.multitool/plugins/PLUGIN_NAME/
93
+
94
+ ^^^^^^^^^^^^^^^^
95
+ Creating plugins
96
+ ^^^^^^^^^^^^^^^^
97
+
98
+ Create a new plugin scaffold with::
99
+
100
+ multitool plugins new PLUGIN_NAME
101
+
102
+ This creates a local plugin repository under::
103
+
104
+ ~/.multitool/plugins/PLUGIN_NAME/
105
+
106
+ The generated structure includes::
107
+
108
+ PLUGIN_NAME/
109
+ ├── __init__.py
110
+ ├── plugin_<unique-id>.py
111
+ ├── multitool-info.json
112
+ ├── README.md
113
+ └── LICENSE
114
+
115
+ The generated plugin contains a Click command group named
116
+ ``PLUGIN_NAME`` and an example ``hello`` command. Add additional commands
117
+ to the generated ``plugin_<unique-id>.py`` module.
118
+
119
+ Test the plugin locally with::
120
+
121
+ multitool run PLUGIN_NAME -h
122
+
123
+ To distribute the plugin:
124
+
125
+ 1. Initialize the plugin directory as a Git repository::
126
+
127
+ cd ~/.multitool/plugins/PLUGIN_NAME
128
+ git init
129
+
130
+ 2. Commit and push it to a remote Git repository such as GitHub or GitLab.
131
+
132
+ 3. Add the repository URL to the Multitool configuration::
133
+
134
+ [sources]
135
+ PLUGIN_NAME = https://github.com/<user>/PLUGIN_NAME.git
136
+
137
+ 4. Install or update plugins::
138
+
139
+ multitool plugins update
140
+
141
+ Alternatively, copy the plugin directory directly into another Multitool
142
+ plugins directory to use it locally.
143
+
144
+ ^^^^^^^^^^^
145
+ Configuring
146
+ ^^^^^^^^^^^
147
+
148
+ Configure plugin repositories with::
149
+
150
+ multitool plugins configure -a
151
+
152
+ This opens your editor to modify the plugin configuration. Omit ``-a`` if you
153
+ don't want changes applied automatically.
154
+
155
+ Example configuration::
156
+
157
+ [sources]
158
+ mdelotavo-multitool-plugins = https://github.com/mdelotavo/multitool-plugins.git
159
+
160
+ After saving, Multitool clones each configured repository into::
161
+
162
+ ~/.multitool/plugins/
163
+
164
+ You can configure multiple repositories as long as each key is unique.
165
+
166
+ ^^^^^^^^^^
167
+ Installing
168
+ ^^^^^^^^^^
169
+
170
+ Install the example plugins::
171
+
172
+ echo -e '[sources]\nmdelotavo-multitool-plugins = https://github.com/mdelotavo/multitool-plugins.git' >> ~/.multitool/plugins/config
173
+
174
+ multitool plugins update
175
+ multitool plugins show
176
+ multitool plugins show -n mdelotavo-multitool-plugins
177
+ multitool plugins show -n mdelotavo-multitool-plugins --show-commit-only
178
+ multitool plugins show -n mdelotavo-multitool-plugins --show-dependencies-only
179
+ pip3 install $(multitool plugins show -n mdelotavo-multitool-plugins --show-dependencies-only)
180
+
181
+ multitool run examples -h
182
+
183
+ ^^^^^^^^
184
+ Updating
185
+ ^^^^^^^^
186
+
187
+ Install new plugins and update existing ones::
188
+
189
+ multitool plugins update
190
+
191
+ ^^^^^^^
192
+ Pruning
193
+ ^^^^^^^
194
+
195
+ Remove repositories no longer listed in the configuration::
196
+
197
+ multitool plugins prune
198
+
199
+ ^^^^^^^
200
+ Showing
201
+ ^^^^^^^
202
+
203
+ Show configured repositories::
204
+
205
+ multitool plugins show
206
+
207
+ Or inspect a specific repository::
208
+
209
+ multitool plugins show -n PLUGIN_NAME --show-commit-only
210
+ multitool plugins show -n PLUGIN_NAME --show-dependencies-only
211
+
212
+ If a plugin declares Python dependencies in ``multitool-info.json``, install
213
+ them with::
214
+
215
+ pip3 install $(multitool plugins show -n PLUGIN_NAME --show-dependencies-only)
216
+
217
+ ---------------
218
+ Troubleshooting
219
+ ---------------
220
+
221
+ If a plugin fails to install or load, check the log file::
222
+
223
+ ~/.multitool/multitool.log
224
+
225
+ It contains installation, dependency, and Git-related errors.
226
+
227
+ -----------
228
+ Limitations
229
+ -----------
230
+
231
+ Plugin command names must be unique across all installed repositories.
232
+
233
+ To avoid naming conflicts, plugin modules should follow the convention of
234
+ including the repository owner and repository name in the command name.
235
+
236
+ For example, a repository configured as::
237
+
238
+ [sources]
239
+ mdelotavo-multitool-plugins = https://github.com/mdelotavo/multitool-plugins.git
240
+
241
+ should expose commands using a unique name such as::
242
+
243
+ mdelotavo-multitool-plugins
244
+
245
+ This reduces the likelihood of collisions when multiple repositories expose
246
+ plugins with the same command name.
247
+
248
+ If duplicate command names are detected, Multitool will display an error
249
+ prompting you to delete the detected conflicting plugin repositories and, if
250
+ applicable, remove the corresponding remote sources from your plugin
251
+ configuration.
252
+
253
+
254
+ .. _`click`: https://click.palletsprojects.com/
255
+ .. _`multitool-plugins`: https://github.com/mdelotavo/multitool-plugins
256
+ .. _`public plugins repository`: https://github.com/mdelotavo/multitool-plugins
@@ -0,0 +1,233 @@
1
+ multitool
2
+ =========
3
+
4
+ Create and run plugin-based command-line tools.
5
+
6
+ ----------
7
+ Quickstart
8
+ ----------
9
+
10
+ This section shows how to start prototyping plugins on your local machine.
11
+ See the sections below to learn how to distribute your commands as remote
12
+ plugins that can be installed and updated from Git repositories.
13
+
14
+ Create a new local plugin::
15
+
16
+ multitool plugins new test
17
+
18
+ Show the generated command help::
19
+
20
+ multitool run test hello --help
21
+
22
+ Run the example command::
23
+
24
+ multitool run test hello \
25
+ "Hello, World!" \
26
+ --count 3 \
27
+ --format json \
28
+ -vv \
29
+ --enabled \
30
+ --tag alpha \
31
+ --tag beta \
32
+ --output result.json
33
+
34
+ Edit the generated source code::
35
+
36
+ vim ~/.multitool/plugins/test/plugin_*.py
37
+
38
+ -----
39
+ Usage
40
+ -----
41
+
42
+ .. code-block:: text
43
+
44
+ Usage: multitool [OPTIONS] COMMAND [ARGS]...
45
+
46
+ Create and run plugin-based command-line tools.
47
+
48
+ Options:
49
+ -V, --version Show the version and exit.
50
+ -h, --help Show this message and exit.
51
+
52
+ Commands:
53
+ plugins Manage plugin repositories.
54
+ run Run installed plugin commands.
55
+
56
+ ----------------
57
+ Managing plugins
58
+ ----------------
59
+
60
+ Plugins are distributed as Git repositories containing Click commands.
61
+
62
+ The ``plugins`` command manages plugin repositories, while installed plugin
63
+ commands are available under ``multitool run``.
64
+
65
+ Git is required to install or update plugins. If Git is unavailable, the
66
+ ``plugins`` command cannot install repositories, although plugins can still be
67
+ installed manually by copying them into::
68
+
69
+ ~/.multitool/plugins/PLUGIN_NAME/
70
+
71
+ ^^^^^^^^^^^^^^^^
72
+ Creating plugins
73
+ ^^^^^^^^^^^^^^^^
74
+
75
+ Create a new plugin scaffold with::
76
+
77
+ multitool plugins new PLUGIN_NAME
78
+
79
+ This creates a local plugin repository under::
80
+
81
+ ~/.multitool/plugins/PLUGIN_NAME/
82
+
83
+ The generated structure includes::
84
+
85
+ PLUGIN_NAME/
86
+ ├── __init__.py
87
+ ├── plugin_<unique-id>.py
88
+ ├── multitool-info.json
89
+ ├── README.md
90
+ └── LICENSE
91
+
92
+ The generated plugin contains a Click command group named
93
+ ``PLUGIN_NAME`` and an example ``hello`` command. Add additional commands
94
+ to the generated ``plugin_<unique-id>.py`` module.
95
+
96
+ Test the plugin locally with::
97
+
98
+ multitool run PLUGIN_NAME -h
99
+
100
+ To distribute the plugin:
101
+
102
+ 1. Initialize the plugin directory as a Git repository::
103
+
104
+ cd ~/.multitool/plugins/PLUGIN_NAME
105
+ git init
106
+
107
+ 2. Commit and push it to a remote Git repository such as GitHub or GitLab.
108
+
109
+ 3. Add the repository URL to the Multitool configuration::
110
+
111
+ [sources]
112
+ PLUGIN_NAME = https://github.com/<user>/PLUGIN_NAME.git
113
+
114
+ 4. Install or update plugins::
115
+
116
+ multitool plugins update
117
+
118
+ Alternatively, copy the plugin directory directly into another Multitool
119
+ plugins directory to use it locally.
120
+
121
+ ^^^^^^^^^^^
122
+ Configuring
123
+ ^^^^^^^^^^^
124
+
125
+ Configure plugin repositories with::
126
+
127
+ multitool plugins configure -a
128
+
129
+ This opens your editor to modify the plugin configuration. Omit ``-a`` if you
130
+ don't want changes applied automatically.
131
+
132
+ Example configuration::
133
+
134
+ [sources]
135
+ mdelotavo-multitool-plugins = https://github.com/mdelotavo/multitool-plugins.git
136
+
137
+ After saving, Multitool clones each configured repository into::
138
+
139
+ ~/.multitool/plugins/
140
+
141
+ You can configure multiple repositories as long as each key is unique.
142
+
143
+ ^^^^^^^^^^
144
+ Installing
145
+ ^^^^^^^^^^
146
+
147
+ Install the example plugins::
148
+
149
+ echo -e '[sources]\nmdelotavo-multitool-plugins = https://github.com/mdelotavo/multitool-plugins.git' >> ~/.multitool/plugins/config
150
+
151
+ multitool plugins update
152
+ multitool plugins show
153
+ multitool plugins show -n mdelotavo-multitool-plugins
154
+ multitool plugins show -n mdelotavo-multitool-plugins --show-commit-only
155
+ multitool plugins show -n mdelotavo-multitool-plugins --show-dependencies-only
156
+ pip3 install $(multitool plugins show -n mdelotavo-multitool-plugins --show-dependencies-only)
157
+
158
+ multitool run examples -h
159
+
160
+ ^^^^^^^^
161
+ Updating
162
+ ^^^^^^^^
163
+
164
+ Install new plugins and update existing ones::
165
+
166
+ multitool plugins update
167
+
168
+ ^^^^^^^
169
+ Pruning
170
+ ^^^^^^^
171
+
172
+ Remove repositories no longer listed in the configuration::
173
+
174
+ multitool plugins prune
175
+
176
+ ^^^^^^^
177
+ Showing
178
+ ^^^^^^^
179
+
180
+ Show configured repositories::
181
+
182
+ multitool plugins show
183
+
184
+ Or inspect a specific repository::
185
+
186
+ multitool plugins show -n PLUGIN_NAME --show-commit-only
187
+ multitool plugins show -n PLUGIN_NAME --show-dependencies-only
188
+
189
+ If a plugin declares Python dependencies in ``multitool-info.json``, install
190
+ them with::
191
+
192
+ pip3 install $(multitool plugins show -n PLUGIN_NAME --show-dependencies-only)
193
+
194
+ ---------------
195
+ Troubleshooting
196
+ ---------------
197
+
198
+ If a plugin fails to install or load, check the log file::
199
+
200
+ ~/.multitool/multitool.log
201
+
202
+ It contains installation, dependency, and Git-related errors.
203
+
204
+ -----------
205
+ Limitations
206
+ -----------
207
+
208
+ Plugin command names must be unique across all installed repositories.
209
+
210
+ To avoid naming conflicts, plugin modules should follow the convention of
211
+ including the repository owner and repository name in the command name.
212
+
213
+ For example, a repository configured as::
214
+
215
+ [sources]
216
+ mdelotavo-multitool-plugins = https://github.com/mdelotavo/multitool-plugins.git
217
+
218
+ should expose commands using a unique name such as::
219
+
220
+ mdelotavo-multitool-plugins
221
+
222
+ This reduces the likelihood of collisions when multiple repositories expose
223
+ plugins with the same command name.
224
+
225
+ If duplicate command names are detected, Multitool will display an error
226
+ prompting you to delete the detected conflicting plugin repositories and, if
227
+ applicable, remove the corresponding remote sources from your plugin
228
+ configuration.
229
+
230
+
231
+ .. _`click`: https://click.palletsprojects.com/
232
+ .. _`multitool-plugins`: https://github.com/mdelotavo/multitool-plugins
233
+ .. _`public plugins repository`: https://github.com/mdelotavo/multitool-plugins
@@ -0,0 +1,26 @@
1
+ APP = "multitool"
2
+ __version__ = "0.7.1"
3
+ description = "Create and run plugin-based command-line tools."
4
+ long_description = ("Create and run plugin-based command-line tools by dynamically loading Click commands from local or Git-managed plugin repositories.")
5
+
6
+ import builtins
7
+ from pathlib import Path
8
+
9
+ from multitool.utils_init import join_path
10
+
11
+ # Runtime configuration
12
+ MULTITOOL_TOGGLE_SILENT = False
13
+ MULTITOOL_TOGGLE_VERBOSE = 0
14
+
15
+ # Application directories and files
16
+ MULTITOOL_DIRECTORY = join_path(Path.home(), ".multitool")
17
+ MULTITOOL_LOG_FILE = join_path(MULTITOOL_DIRECTORY, f"{APP}.log")
18
+
19
+ # Plugin directories and files
20
+ MULTITOOL_PLUGINS_DIRECTORY = join_path(MULTITOOL_DIRECTORY, "plugins")
21
+ MULTITOOL_PLUGINS_CONFIG_FILE = join_path(MULTITOOL_PLUGINS_DIRECTORY, "config")
22
+ MULTITOOL_PLUGINS_INIT_FILE = join_path(MULTITOOL_PLUGINS_DIRECTORY, "__init__.py")
23
+
24
+ # Expose runtime configuration globally
25
+ builtins.MULTITOOL_TOGGLE_SILENT = MULTITOOL_TOGGLE_SILENT
26
+ builtins.MULTITOOL_TOGGLE_VERBOSE = MULTITOOL_TOGGLE_VERBOSE
@@ -8,37 +8,48 @@ from multitool import __version__ as version
8
8
  from multitool.cls import AliasedGroup
9
9
  from multitool.exceptions import wrap_with_exception_handling
10
10
  from multitool.plugins.commands import plugins
11
- from multitool.utils import (configure_global_logger, execute_function_on_directory_files, import_plugins_from_directory)
11
+ from multitool.utils import (configure_root_logger, for_each_file, load_plugins)
12
12
 
13
- CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
13
+ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
14
14
 
15
15
 
16
16
  @click.group(context_settings=CONTEXT_SETTINGS, cls=AliasedGroup, invoke_without_command=False, chain=False)
17
- @click.version_option(version, '-V', '--version')
17
+ @click.version_option(version, "-V", "--version")
18
18
  @click.pass_context
19
19
  def cli(ctx):
20
- """Quickly create and distribute command-line tools."""
20
+ """Create and run plugin-based command-line tools."""
21
21
  ctx.ensure_object(dict)
22
22
 
23
23
 
24
+ @click.group(cls=AliasedGroup)
25
+ def run():
26
+ """Run installed plugin commands."""
27
+ pass
28
+
29
+
24
30
  @wrap_with_exception_handling
25
31
  def main():
26
- configure_global_logger(MULTITOOL_LOG_FILE)
32
+ configure_root_logger(MULTITOOL_LOG_FILE)
27
33
 
28
- cli_commands = {plugins}
34
+ cli_commands = {plugins, run}
29
35
 
30
- execute_function_on_directory_files(
36
+ run_commands = {}
37
+
38
+ for_each_file(
31
39
  MULTITOOL_PLUGINS_DIRECTORY,
32
- import_plugins_from_directory,
33
- args=(cli_commands, ),
34
- glob='[!.][!__]*/__init__.py',
40
+ load_plugins,
41
+ args=(run_commands, ),
42
+ glob="[!.][!__]*/__init__.py",
35
43
  )
36
44
 
45
+ for command in run_commands.values():
46
+ run.add_command(command)
47
+
37
48
  for command in cli_commands:
38
49
  cli.add_command(command)
39
50
 
40
51
  cli(prog_name=APP, obj={})
41
52
 
42
53
 
43
- if __name__ == '__main__':
44
- main() # pragma: no cover
54
+ if __name__ == "__main__":
55
+ main()
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass, fields, replace
2
+ import builtins
3
+ import sys
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class EchoOptions:
8
+ exit_status: int | None = None
9
+ silent: bool = False
10
+ verbosity: int = 0
11
+ level: int = 0
12
+ end: str = "\n"
13
+ flush: bool = False
14
+
15
+
16
+ def echo(*msg, options: EchoOptions | None = None, **kwargs):
17
+ options = options or EchoOptions()
18
+
19
+ valid_fields = {f.name for f in fields(EchoOptions)}
20
+
21
+ for key, value in kwargs.items():
22
+ if key not in valid_fields:
23
+ raise TypeError(f"echo() got an unexpected keyword argument '{key}'")
24
+ options = replace(options, **{key: value})
25
+
26
+ if options.silent or builtins.MULTITOOL_TOGGLE_SILENT:
27
+ if options.exit_status is not None:
28
+ sys.exit(options.exit_status)
29
+ return
30
+
31
+ verbosity = max(options.verbosity, builtins.MULTITOOL_TOGGLE_VERBOSE)
32
+
33
+ if verbosity >= options.level:
34
+ print(*msg, end=options.end, flush=options.flush)
35
+
36
+ if options.exit_status is not None:
37
+ sys.exit(options.exit_status)