multitool 0.4.0__tar.gz → 0.6.0__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 (27) hide show
  1. {multitool-0.4.0 → multitool-0.6.0}/LICENSE +0 -0
  2. {multitool-0.4.0 → multitool-0.6.0}/PKG-INFO +29 -27
  3. {multitool-0.4.0 → multitool-0.6.0}/README.rst +15 -19
  4. {multitool-0.4.0 → multitool-0.6.0}/multitool/__init__.py +3 -3
  5. {multitool-0.4.0 → multitool-0.6.0}/multitool/__main__.py +7 -19
  6. {multitool-0.4.0 → multitool-0.6.0}/multitool/cls.py +1 -0
  7. multitool-0.6.0/multitool/console.py +25 -0
  8. {multitool-0.4.0 → multitool-0.6.0}/multitool/exceptions.py +1 -0
  9. {multitool-0.4.0 → multitool-0.6.0}/multitool/plugins/__init__.py +0 -0
  10. multitool-0.6.0/multitool/plugins/commands.py +224 -0
  11. {multitool-0.4.0 → multitool-0.6.0}/multitool/silent.py +5 -5
  12. {multitool-0.4.0 → multitool-0.6.0}/multitool/utils.py +15 -16
  13. {multitool-0.4.0 → multitool-0.6.0}/multitool/verbose.py +5 -5
  14. {multitool-0.4.0 → multitool-0.6.0}/multitool.egg-info/PKG-INFO +29 -27
  15. {multitool-0.4.0 → multitool-0.6.0}/multitool.egg-info/SOURCES.txt +1 -1
  16. {multitool-0.4.0 → multitool-0.6.0}/multitool.egg-info/dependency_links.txt +0 -0
  17. {multitool-0.4.0 → multitool-0.6.0}/multitool.egg-info/entry_points.txt +0 -0
  18. multitool-0.6.0/multitool.egg-info/requires.txt +4 -0
  19. {multitool-0.4.0 → multitool-0.6.0}/multitool.egg-info/top_level.txt +0 -1
  20. multitool-0.6.0/pyproject.toml +45 -0
  21. {multitool-0.4.0 → multitool-0.6.0}/setup.cfg +0 -0
  22. {multitool-0.4.0 → multitool-0.6.0}/tests/__init__.py +0 -0
  23. {multitool-0.4.0 → multitool-0.6.0}/tests/test_utils.py +1 -0
  24. multitool-0.4.0/multitool/console.py +0 -23
  25. multitool-0.4.0/multitool/plugins/commands.py +0 -229
  26. multitool-0.4.0/multitool.egg-info/requires.txt +0 -11
  27. multitool-0.4.0/setup.py +0 -53
File without changes
@@ -1,32 +1,30 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: multitool
3
- Version: 0.4.0
4
- Summary: General-purpose command-line interface with plugins support
5
- Home-page: https://github.com/mdelotavo/multitool
6
- Author: Matthew Delotavo
7
- Author-email: matthew.t.delotavo@gmail.com
8
- License: MIT
3
+ Version: 0.6.0
4
+ Summary: Quickly create and distribute 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
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Environment :: Console
11
12
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3.7
15
15
  Requires-Python: >=3.7
16
+ Description-Content-Type: text/x-rst
16
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
17
23
 
18
24
  multitool
19
25
  =========
20
26
 
21
- Multitool is a general-purpose command-line interface with plugins support.
22
-
23
- --------------------
24
- Why does this exist?
25
- --------------------
26
-
27
- I built this tool so that I can quickly create and distribute command-line tools for consulting work and personal use.
28
-
29
- The plugins manager uses ``git`` to manage plugins installed from remote ``git`` repositories.
27
+ Quickly create and distribute command-line tools with plugins.
30
28
 
31
29
  -----
32
30
  Usage
@@ -36,20 +34,14 @@ Usage
36
34
 
37
35
  Usage: multitool [OPTIONS] COMMAND [ARGS]...
38
36
 
39
- Welcome to the Multitool command-line interface!
40
-
41
- PyPI: https://pypi.org/project/multitool/
42
- GitHub: https://github.com/mdelotavo/multitool
37
+ Quickly create and distribute command-line tools.
43
38
 
44
39
  Options:
45
40
  -V, --version Show the version and exit.
46
41
  -h, --help Show this message and exit.
47
42
 
48
43
  Commands:
49
- plugins Simple plugins manager for distributing commands.
50
-
51
- .. ..
52
- The plugins features are based off those found in the `apigeecli`_.
44
+ plugins Plugins manager for distributing commands.
53
45
 
54
46
  ----------------
55
47
  Managing plugins
@@ -122,7 +114,7 @@ This will install and update plugins.
122
114
  Pruning
123
115
  ^^^^^^^
124
116
 
125
- If you specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
117
+ If you remove plugins from the config file or comment them out, and you then specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
126
118
  Otherwise you can run::
127
119
 
128
120
  multitool plugins prune
@@ -146,6 +138,17 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
146
138
 
147
139
  pip3 install $(multitool plugins show -n PLUGIN_NAME --show-dependencies-only)
148
140
 
141
+ ^^^^^^^^^^^^^^^
142
+ Troubleshooting
143
+ ^^^^^^^^^^^^^^^
144
+
145
+ If you encounter issues with plugins or commands, you can find more detailed debug information and error messages in the log file:
146
+ ::
147
+
148
+ ~/.multitool/multitool.log
149
+
150
+ Reviewing this file can help you diagnose installation problems, missing dependencies, or Git-related errors.
151
+
149
152
  .. ..
150
153
  ----------------------------
151
154
  Create a virtual environment
@@ -167,6 +170,5 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
167
170
  deactivate
168
171
 
169
172
  .. _`click`: https://click.palletsprojects.com/
170
- .. _`apigeecli`: https://pypi.org/project/apigeecli/
171
173
  .. _`multitool-plugins`: https://github.com/mdelotavo/multitool-plugins
172
174
  .. _`public plugins repository`: https://github.com/mdelotavo/multitool-plugins
@@ -1,15 +1,7 @@
1
1
  multitool
2
2
  =========
3
3
 
4
- Multitool is a general-purpose command-line interface with plugins support.
5
-
6
- --------------------
7
- Why does this exist?
8
- --------------------
9
-
10
- I built this tool so that I can quickly create and distribute command-line tools for consulting work and personal use.
11
-
12
- The plugins manager uses ``git`` to manage plugins installed from remote ``git`` repositories.
4
+ Quickly create and distribute command-line tools with plugins.
13
5
 
14
6
  -----
15
7
  Usage
@@ -19,20 +11,14 @@ Usage
19
11
 
20
12
  Usage: multitool [OPTIONS] COMMAND [ARGS]...
21
13
 
22
- Welcome to the Multitool command-line interface!
23
-
24
- PyPI: https://pypi.org/project/multitool/
25
- GitHub: https://github.com/mdelotavo/multitool
14
+ Quickly create and distribute command-line tools.
26
15
 
27
16
  Options:
28
17
  -V, --version Show the version and exit.
29
18
  -h, --help Show this message and exit.
30
19
 
31
20
  Commands:
32
- plugins Simple plugins manager for distributing commands.
33
-
34
- .. ..
35
- The plugins features are based off those found in the `apigeecli`_.
21
+ plugins Plugins manager for distributing commands.
36
22
 
37
23
  ----------------
38
24
  Managing plugins
@@ -105,7 +91,7 @@ This will install and update plugins.
105
91
  Pruning
106
92
  ^^^^^^^
107
93
 
108
- If you specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
94
+ If you remove plugins from the config file or comment them out, and you then specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
109
95
  Otherwise you can run::
110
96
 
111
97
  multitool plugins prune
@@ -129,6 +115,17 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
129
115
 
130
116
  pip3 install $(multitool plugins show -n PLUGIN_NAME --show-dependencies-only)
131
117
 
118
+ ^^^^^^^^^^^^^^^
119
+ Troubleshooting
120
+ ^^^^^^^^^^^^^^^
121
+
122
+ If you encounter issues with plugins or commands, you can find more detailed debug information and error messages in the log file:
123
+ ::
124
+
125
+ ~/.multitool/multitool.log
126
+
127
+ Reviewing this file can help you diagnose installation problems, missing dependencies, or Git-related errors.
128
+
132
129
  .. ..
133
130
  ----------------------------
134
131
  Create a virtual environment
@@ -150,6 +147,5 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
150
147
  deactivate
151
148
 
152
149
  .. _`click`: https://click.palletsprojects.com/
153
- .. _`apigeecli`: https://pypi.org/project/apigeecli/
154
150
  .. _`multitool-plugins`: https://github.com/mdelotavo/multitool-plugins
155
151
  .. _`public plugins repository`: https://github.com/mdelotavo/multitool-plugins
@@ -1,7 +1,7 @@
1
1
  APP = 'multitool'
2
- __version__ = '0.4.0'
3
- description = 'General-purpose command-line interface with plugins support'
4
- # long_description = 'General-purpose command-line interface with plugins support'
2
+ __version__ = "0.6.0"
3
+ description = 'Quickly create and distribute command-line tools.'
4
+ long_description = 'Quickly create and distribute command-line tools with plugins.'
5
5
 
6
6
  import builtins
7
7
  from pathlib import Path
@@ -8,28 +8,16 @@ 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 (import_plugins_from_directory,
12
- execute_function_on_directory_files, configure_global_logger)
11
+ from multitool.utils import (configure_global_logger, execute_function_on_directory_files, import_plugins_from_directory)
13
12
 
14
13
  CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
15
14
 
16
15
 
17
- @click.group(
18
- context_settings=CONTEXT_SETTINGS, cls=AliasedGroup, invoke_without_command=False, chain=False
19
- )
16
+ @click.group(context_settings=CONTEXT_SETTINGS, cls=AliasedGroup, invoke_without_command=False, chain=False)
20
17
  @click.version_option(version, '-V', '--version')
21
18
  @click.pass_context
22
19
  def cli(ctx):
23
- """Welcome to the Multitool command-line interface!
24
-
25
- \b
26
- PyPI: https://pypi.org/project/multitool/
27
- GitHub: https://github.com/mdelotavo/multitool
28
-
29
- \f
30
-
31
- :param click.core.Context ctx: Click context.
32
- """
20
+ """Quickly create and distribute command-line tools."""
33
21
  ctx.ensure_object(dict)
34
22
 
35
23
 
@@ -40,10 +28,10 @@ def main():
40
28
  cli_commands = {plugins}
41
29
 
42
30
  execute_function_on_directory_files(
43
- MULTITOOL_PLUGINS_DIRECTORY,
44
- import_plugins_from_directory,
45
- args=(cli_commands,),
46
- glob='[!.][!__]*/__init__.py',
31
+ MULTITOOL_PLUGINS_DIRECTORY,
32
+ import_plugins_from_directory,
33
+ args=(cli_commands, ),
34
+ glob='[!.][!__]*/__init__.py',
47
35
  )
48
36
 
49
37
  for command in cli_commands:
@@ -2,6 +2,7 @@ import click
2
2
 
3
3
 
4
4
  class AliasedGroup(click.Group):
5
+
5
6
  def get_command(self, ctx, cmd_name):
6
7
  rv = click.Group.get_command(self, ctx, cmd_name)
7
8
  if rv is not None:
@@ -0,0 +1,25 @@
1
+ import builtins
2
+ import sys
3
+
4
+
5
+ def echo(
6
+ *message,
7
+ exit_status=None,
8
+ make_silent=False,
9
+ current_verbosity=0,
10
+ expected_verbosity=0,
11
+ line_ending="\n",
12
+ should_flush=False,
13
+ ):
14
+ if make_silent or builtins.MULTITOOL_TOGGLE_SILENT:
15
+ if exit_status is not None:
16
+ sys.exit(exit_status)
17
+ return
18
+
19
+ verbosity = max(current_verbosity, builtins.MULTITOOL_TOGGLE_VERBOSE)
20
+
21
+ if verbosity >= expected_verbosity:
22
+ print(*message, end=line_ending, flush=should_flush)
23
+
24
+ if exit_status is not None:
25
+ sys.exit(exit_status)
@@ -7,6 +7,7 @@ from multitool import console
7
7
 
8
8
 
9
9
  def wrap_with_exception_handling(func):
10
+
10
11
  @functools.wraps(func)
11
12
  def wrapper(*args, **kwargs):
12
13
  try:
@@ -0,0 +1,224 @@
1
+ import configparser
2
+ import os
3
+ import shutil
4
+ import stat
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from click_option_group import MutuallyExclusiveOptionGroup, optgroup
10
+
11
+ from multitool import (
12
+ APP,
13
+ MULTITOOL_PLUGINS_CONFIG_FILE,
14
+ MULTITOOL_PLUGINS_DIRECTORY,
15
+ MULTITOOL_PLUGINS_PATH,
16
+ console,
17
+ )
18
+ from multitool.silent import common_silent_options
19
+ from multitool.utils import (
20
+ create_directory,
21
+ create_empty_file,
22
+ execute_function_on_directory_files,
23
+ is_directory,
24
+ is_regular_file,
25
+ read_file_content,
26
+ )
27
+ from multitool.verbose import common_verbose_options
28
+
29
+ try:
30
+ from git import Repo
31
+
32
+ HAS_GIT = True
33
+ HELP = "Plugins manager for distributing commands."
34
+ except ImportError:
35
+ HAS_GIT = False
36
+ HELP = "Plugins manager. Git is required."
37
+
38
+
39
+ def require_git():
40
+ if not HAS_GIT:
41
+ sys.exit(0)
42
+
43
+
44
+ def init():
45
+ create_directory(MULTITOOL_PLUGINS_DIRECTORY)
46
+ create_empty_file(MULTITOOL_PLUGINS_PATH)
47
+ create_empty_file(MULTITOOL_PLUGINS_CONFIG_FILE)
48
+
49
+
50
+ def config(section="sources"):
51
+ cfg = configparser.ConfigParser(allow_no_value=True)
52
+ cfg.read(MULTITOOL_PLUGINS_CONFIG_FILE)
53
+ return dict(cfg._sections.get(section, {}))
54
+
55
+
56
+ def clone():
57
+ init()
58
+ for name, uri in config().items():
59
+ dest = Path(MULTITOOL_PLUGINS_DIRECTORY) / name
60
+ if is_directory(dest):
61
+ continue
62
+
63
+ console.echo(f"Installing {name}... ", line_ending="", should_flush=True)
64
+ try:
65
+ Repo.clone_from(uri, dest)
66
+ console.echo("Done")
67
+ except Exception as e:
68
+ console.echo(e)
69
+
70
+
71
+ def update_repos():
72
+
73
+ def fn(p):
74
+ if not is_directory(p):
75
+ return
76
+
77
+ console.echo(f"Updating {Path(p).stem}... ", line_ending="", should_flush=True)
78
+
79
+ try:
80
+ repo = Repo(p)
81
+ if not repo.bare:
82
+ repo.remotes["origin"].pull()
83
+ console.echo("Done")
84
+ except Exception as e:
85
+ console.echo(e)
86
+
87
+ execute_function_on_directory_files(MULTITOOL_PLUGINS_DIRECTORY, fn, glob="[!.][!__]*")
88
+
89
+
90
+ def prune():
91
+ sources = config()
92
+
93
+ def fn(p):
94
+ if not is_directory(p):
95
+ return
96
+
97
+ name = Path(p).stem
98
+ if name in sources:
99
+ return
100
+
101
+ console.echo(f"Removing {name}... ", line_ending="", should_flush=True)
102
+
103
+ try:
104
+ shutil.rmtree(p, onerror=_chmod)
105
+ console.echo("Done")
106
+ except Exception as e:
107
+ console.echo(e)
108
+
109
+ execute_function_on_directory_files(MULTITOOL_PLUGINS_DIRECTORY, fn, glob="[!.][!__]*")
110
+
111
+
112
+ def _chmod(func, p, _):
113
+ os.chmod(p, stat.S_IRWXU)
114
+ func(p)
115
+
116
+
117
+ # def prune():
118
+ # sources = config()
119
+
120
+ # def fn(p):
121
+ # if not is_directory(p):
122
+ # return
123
+
124
+ # name = Path(p).stem
125
+ # if name in sources:
126
+ # return
127
+
128
+ # console.echo(f"Removing {name}... ", line_ending="", should_flush=True)
129
+
130
+ # try:
131
+ # shutil.rmtree(p, onexc=_chmod)
132
+ # console.echo("Done")
133
+ # except Exception as e:
134
+ # console.echo(e)
135
+
136
+ # execute_function_on_directory_files(PLUGINS_DIR, fn, glob="[!.][!__]*")
137
+
138
+ # def _chmod(func, path, exc):
139
+ # os.chmod(path, stat.S_IRWXU)
140
+ # func(path)
141
+
142
+
143
+ def plugin_info(name):
144
+ f = Path(MULTITOOL_PLUGINS_DIRECTORY) / name / f"{APP}-info.json"
145
+
146
+ if not is_regular_file(f):
147
+ return None
148
+
149
+ return read_file_content(f, type="json")
150
+
151
+
152
+ def print_commit(name):
153
+ repo = Repo(Path(MULTITOOL_PLUGINS_DIRECTORY) / name)
154
+ console.echo(repo.git.log("--pretty=format:%h - %s (%cr) <%an>", "-1"))
155
+
156
+
157
+ @click.group(help=HELP)
158
+ def plugins():
159
+ pass
160
+
161
+
162
+ @plugins.command()
163
+ @common_silent_options
164
+ @common_verbose_options
165
+ @click.option("-a/-A", "--apply-changes/--no-apply-changes", default=False)
166
+ def configure(silent, verbose, apply_changes):
167
+ require_git()
168
+ init()
169
+
170
+ click.edit(filename=MULTITOOL_PLUGINS_CONFIG_FILE)
171
+
172
+ if apply_changes:
173
+ clone()
174
+ prune()
175
+ else:
176
+ console.echo(f"\nRun `{APP} plugins update` to apply changes.\n")
177
+
178
+
179
+ @plugins.command()
180
+ @common_silent_options
181
+ @common_verbose_options
182
+ def update(silent, verbose):
183
+ require_git()
184
+ clone()
185
+ update_repos()
186
+
187
+
188
+ @plugins.command()
189
+ @common_silent_options
190
+ @common_verbose_options
191
+ @click.option("-n", "--name")
192
+ @optgroup.group("Filter options", cls=MutuallyExclusiveOptionGroup)
193
+ @optgroup.option("--show-commit-only/--no-show-commit-only", default=False)
194
+ @optgroup.option("--show-dependencies-only/--no-show-dependencies-only", default=False)
195
+ def show(silent, verbose, name, show_commit_only, show_dependencies_only):
196
+ if not name:
197
+ for k, v in config().items():
198
+ console.echo(f"{k}: {v}")
199
+ return
200
+
201
+ info = plugin_info(name)
202
+ if not info:
203
+ return
204
+
205
+ if show_commit_only:
206
+ require_git()
207
+ print_commit(name)
208
+ return
209
+
210
+ if show_dependencies_only:
211
+ if info.get("Requires"):
212
+ console.echo(info["Requires"])
213
+ return
214
+
215
+ for k, v in info.items():
216
+ console.echo(f"{k}: {v}")
217
+
218
+
219
+ @plugins.command()
220
+ @common_silent_options
221
+ @common_verbose_options
222
+ def prune_cmd(silent, verbose):
223
+ require_git()
224
+ prune()
@@ -9,10 +9,10 @@ def silent_callback(ctx, param, value):
9
9
 
10
10
  def common_silent_options(func):
11
11
  func = click.option(
12
- '--silent',
13
- show_default='toggle silent output',
14
- flag_value=True,
15
- default=False,
16
- callback=silent_callback,
12
+ '--silent',
13
+ show_default='toggle silent output',
14
+ flag_value=True,
15
+ default=False,
16
+ callback=silent_callback,
17
17
  )(func)
18
18
  return func
@@ -15,7 +15,7 @@ def apply_function_on_iterable(iterable, func, state_op="append", args=(), kwarg
15
15
  kwargs = {}
16
16
  state = []
17
17
  for item in iterable:
18
- _tuple = (item,)
18
+ _tuple = (item, )
19
19
  result = func(*(_tuple + args), **kwargs)
20
20
  if result:
21
21
  getattr(state, state_op)(result)
@@ -26,9 +26,9 @@ def configure_global_logger(log_file):
26
26
  create_empty_file(log_file)
27
27
  remove_file_if_above_size(log_file, size_kb=1000)
28
28
  logging.basicConfig(
29
- filename=log_file,
30
- level=logging.WARNING,
31
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
29
+ filename=log_file,
30
+ level=logging.WARNING,
31
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
32
32
  )
33
33
 
34
34
 
@@ -57,7 +57,7 @@ def execute_function_on_directory_files(dir, func, glob="**/*", args=(), kwargs=
57
57
  kwargs = {}
58
58
  state = []
59
59
  for file_path in Path(get_resolved_directory_path(dir)).resolve().glob(glob):
60
- _tuple = (str(file_path),)
60
+ _tuple = (str(file_path), )
61
61
  result = func(*(_tuple + args), **kwargs)
62
62
  if result:
63
63
  state.append(result)
@@ -72,23 +72,22 @@ def get_resolved_directory_path(target_directory=None):
72
72
  return os.getcwd()
73
73
 
74
74
 
75
- def import_plugins_from_directory(plugins_init_file, existing_commands):
75
+ def import_plugins_from_directory(init_file, commands):
76
76
  try:
77
- spec = importlib.util.spec_from_file_location('plugins_modules', plugins_init_file)
77
+ spec = importlib.util.spec_from_file_location("plugins_modules", init_file)
78
78
  module = importlib.util.module_from_spec(spec)
79
79
  sys.modules[spec.name] = module
80
80
  spec.loader.exec_module(module)
81
- import plugins_modules # type: ignore
82
- from plugins_modules import __all__ as all_plugins_modules # type: ignore
83
81
 
84
- for module in all_plugins_modules:
85
- _module = getattr(plugins_modules, module)
86
- if isinstance(_module, (click.core.Command, click.core.Group)):
87
- existing_commands.add(_module)
82
+ plugins = getattr(module, "__all__", [])
83
+
84
+ for name in plugins:
85
+ obj = getattr(module, name)
86
+ if isinstance(obj, (click.Command, click.Group)):
87
+ commands.add(obj)
88
+
88
89
  except ImportError:
89
- logging.warning(
90
- f'{inspect.stack()[0][3]}; will skip loading plugin: {module}', exc_info=True
91
- )
90
+ logging.warning("Failed to load plugin", exc_info=True)
92
91
 
93
92
 
94
93
  def is_directory(d):
@@ -14,10 +14,10 @@ def verbose_callback(ctx, param, value):
14
14
 
15
15
  def common_verbose_options(func):
16
16
  func = click.option(
17
- '-v',
18
- '--verbose',
19
- show_default='toggle verbose output',
20
- count=True,
21
- callback=verbose_callback,
17
+ '-v',
18
+ '--verbose',
19
+ show_default='toggle verbose output',
20
+ count=True,
21
+ callback=verbose_callback,
22
22
  )(func)
23
23
  return func
@@ -1,32 +1,30 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: multitool
3
- Version: 0.4.0
4
- Summary: General-purpose command-line interface with plugins support
5
- Home-page: https://github.com/mdelotavo/multitool
6
- Author: Matthew Delotavo
7
- Author-email: matthew.t.delotavo@gmail.com
8
- License: MIT
3
+ Version: 0.6.0
4
+ Summary: Quickly create and distribute 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
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Environment :: Console
11
12
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3.7
15
15
  Requires-Python: >=3.7
16
+ Description-Content-Type: text/x-rst
16
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
17
23
 
18
24
  multitool
19
25
  =========
20
26
 
21
- Multitool is a general-purpose command-line interface with plugins support.
22
-
23
- --------------------
24
- Why does this exist?
25
- --------------------
26
-
27
- I built this tool so that I can quickly create and distribute command-line tools for consulting work and personal use.
28
-
29
- The plugins manager uses ``git`` to manage plugins installed from remote ``git`` repositories.
27
+ Quickly create and distribute command-line tools with plugins.
30
28
 
31
29
  -----
32
30
  Usage
@@ -36,20 +34,14 @@ Usage
36
34
 
37
35
  Usage: multitool [OPTIONS] COMMAND [ARGS]...
38
36
 
39
- Welcome to the Multitool command-line interface!
40
-
41
- PyPI: https://pypi.org/project/multitool/
42
- GitHub: https://github.com/mdelotavo/multitool
37
+ Quickly create and distribute command-line tools.
43
38
 
44
39
  Options:
45
40
  -V, --version Show the version and exit.
46
41
  -h, --help Show this message and exit.
47
42
 
48
43
  Commands:
49
- plugins Simple plugins manager for distributing commands.
50
-
51
- .. ..
52
- The plugins features are based off those found in the `apigeecli`_.
44
+ plugins Plugins manager for distributing commands.
53
45
 
54
46
  ----------------
55
47
  Managing plugins
@@ -122,7 +114,7 @@ This will install and update plugins.
122
114
  Pruning
123
115
  ^^^^^^^
124
116
 
125
- If you specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
117
+ If you remove plugins from the config file or comment them out, and you then specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
126
118
  Otherwise you can run::
127
119
 
128
120
  multitool plugins prune
@@ -146,6 +138,17 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
146
138
 
147
139
  pip3 install $(multitool plugins show -n PLUGIN_NAME --show-dependencies-only)
148
140
 
141
+ ^^^^^^^^^^^^^^^
142
+ Troubleshooting
143
+ ^^^^^^^^^^^^^^^
144
+
145
+ If you encounter issues with plugins or commands, you can find more detailed debug information and error messages in the log file:
146
+ ::
147
+
148
+ ~/.multitool/multitool.log
149
+
150
+ Reviewing this file can help you diagnose installation problems, missing dependencies, or Git-related errors.
151
+
149
152
  .. ..
150
153
  ----------------------------
151
154
  Create a virtual environment
@@ -167,6 +170,5 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
167
170
  deactivate
168
171
 
169
172
  .. _`click`: https://click.palletsprojects.com/
170
- .. _`apigeecli`: https://pypi.org/project/apigeecli/
171
173
  .. _`multitool-plugins`: https://github.com/mdelotavo/multitool-plugins
172
174
  .. _`public plugins repository`: https://github.com/mdelotavo/multitool-plugins
@@ -1,6 +1,6 @@
1
1
  LICENSE
2
2
  README.rst
3
- setup.py
3
+ pyproject.toml
4
4
  multitool/__init__.py
5
5
  multitool/__main__.py
6
6
  multitool/cls.py
@@ -0,0 +1,4 @@
1
+ click>=8.1.3
2
+ click-aliases>=1.0.1
3
+ click-option-group>=0.5.5
4
+ GitPython>=3.1.30
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "multitool"
7
+ version = "0.6.0"
8
+ description = "Quickly create and distribute command-line tools."
9
+ readme = { file = "README.rst", content-type = "text/x-rst" }
10
+ requires-python = ">=3.7"
11
+ license = "MIT"
12
+
13
+ authors = [
14
+ { name = "Matthew Delotavo", email = "matthew.t.delotavo@gmail.com" }
15
+ ]
16
+
17
+ keywords = ["cli", "tooling", "automation"]
18
+
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Environment :: Console",
22
+ "Intended Audience :: Developers",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3.7"
25
+ ]
26
+
27
+ dependencies = [
28
+ "click>=8.1.3",
29
+ "click-aliases>=1.0.1",
30
+ "click-option-group>=0.5.5",
31
+ "GitPython>=3.1.30"
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/mdelotavo/multitool"
36
+ Repository = "https://github.com/mdelotavo/multitool"
37
+
38
+ [project.scripts]
39
+ multitool = "multitool.__main__:main"
40
+
41
+ [tool.setuptools]
42
+ include-package-data = true
43
+
44
+ [tool.setuptools.packages.find]
45
+ include = ["multitool*"]
File without changes
File without changes
@@ -5,6 +5,7 @@ from unittest.mock import patch
5
5
 
6
6
  from multitool.utils import show_message
7
7
 
8
+
8
9
  class TestUtils(TestCase):
9
10
 
10
11
  @patch('builtins.print')
@@ -1,23 +0,0 @@
1
- import builtins
2
- import sys
3
-
4
-
5
- def echo(
6
- *message,
7
- exit_status=None,
8
- make_silent=False,
9
- current_verbosity=0,
10
- expected_verbosity=0,
11
- line_ending="\n",
12
- should_flush=False
13
- ):
14
- toggle_silent = builtins.MULTITOOL_TOGGLE_SILENT
15
- toggle_verbose = builtins.MULTITOOL_TOGGLE_VERBOSE
16
- if make_silent or toggle_silent:
17
- if exit_status:
18
- sys.exit(exit_status)
19
- return
20
- if current_verbosity or toggle_verbose >= expected_verbosity:
21
- print(*message, end=line_ending, flush=should_flush)
22
- if exit_status:
23
- sys.exit(exit_status)
@@ -1,229 +0,0 @@
1
- import configparser
2
- import os
3
- import shutil
4
- import stat
5
- import sys
6
- from os import path
7
- from pathlib import Path
8
-
9
- import click
10
- from click_option_group import MutuallyExclusiveOptionGroup, optgroup
11
-
12
- from multitool import (MULTITOOL_PLUGINS_CONFIG_FILE,
13
- MULTITOOL_PLUGINS_DIRECTORY, MULTITOOL_PLUGINS_PATH,
14
- console)
15
- from multitool.silent import common_silent_options
16
- from multitool.utils import (is_directory, is_regular_file, create_directory, read_file_content,
17
- execute_function_on_directory_files, create_empty_file)
18
- from multitool.verbose import common_verbose_options
19
-
20
- is_git_installed = False
21
- plugins_command_help = 'Simple plugins manager for distributing commands.'
22
-
23
- try:
24
- from git import Repo
25
-
26
- is_git_installed = True
27
- except ImportError:
28
- plugins_command_help = '[Git not found! Please install Git first.] Simple plugins manager for distributing commands. This command requires Git to be installed.'
29
-
30
-
31
- def chmod_directory(directory, mode):
32
- """
33
- Recursively changes the permissions of a directory and its contents.
34
-
35
- Reference: https://stackoverflow.com/a/58878271
36
-
37
- Args:
38
- directory (str): Path to the directory.
39
- mode (int): Permissions to be set (e.g., 0o0700).
40
-
41
- """
42
- for root, dirs, files in os.walk(directory):
43
- for dir in dirs:
44
- os.chmod(path.join(root, dir), mode)
45
- for file in files:
46
- os.chmod(path.join(root, file), mode)
47
-
48
-
49
- def clone_plugin_repositories(section="sources"):
50
- initialize_MULTITOOL_plugins()
51
- config = load_plugin_config()
52
- if not config._sections:
53
- return
54
- sources = dict(config._sections[section])
55
- for name, uri in sources.items():
56
- dest = Path(MULTITOOL_PLUGINS_DIRECTORY) / name
57
- if is_directory(dest):
58
- continue
59
- try:
60
- console.echo(f"Installing {name}... ", line_ending="", should_flush=True)
61
- Repo.clone_from(uri, dest)
62
- console.echo("Done")
63
- except Exception as e:
64
- console.echo(e)
65
-
66
-
67
- def exit_quietly_if_git_not_installed():
68
- if not is_git_installed:
69
- sys.exit(0)
70
-
71
-
72
- def initialize_and_save_plugin_config():
73
- initialize_MULTITOOL_plugins()
74
- config = load_plugin_config()
75
- if not config._sections:
76
- return
77
-
78
-
79
- def initialize_MULTITOOL_plugins():
80
- create_directory(MULTITOOL_PLUGINS_DIRECTORY)
81
- create_empty_file(MULTITOOL_PLUGINS_PATH)
82
- create_empty_file(MULTITOOL_PLUGINS_CONFIG_FILE)
83
-
84
-
85
- def load_plugin_config(config_file=MULTITOOL_PLUGINS_CONFIG_FILE):
86
- config = configparser.ConfigParser(allow_no_value=True)
87
- config.read(config_file)
88
- return config
89
-
90
-
91
- def prune_unused_plugin_directories(section="sources"):
92
- initialize_MULTITOOL_plugins()
93
- config = load_plugin_config()
94
- if not config._sections:
95
- return
96
- sources = dict(config._sections[section])
97
-
98
- def _func(path):
99
- if not is_directory(path):
100
- return
101
- name = Path(path).stem
102
- if name in sources:
103
- return
104
- console.echo(f"Removing {name}... ", line_ending="", should_flush=True)
105
- plugin_directory = Path(MULTITOOL_PLUGINS_DIRECTORY) / name
106
- try:
107
- chmod_directory(str(Path(plugin_directory) / ".git"), stat.S_IRWXU)
108
- shutil.rmtree(plugin_directory)
109
- console.echo("Done")
110
- except Exception as e:
111
- console.echo(e)
112
-
113
- return execute_function_on_directory_files(MULTITOOL_PLUGINS_DIRECTORY, _func, glob="[!.][!__]*")
114
-
115
-
116
- def update_plugin_repositories():
117
- def _func(path):
118
- if not is_directory(path):
119
- return
120
- console.echo(f"Updating {Path(path).stem}... ", line_ending="", should_flush=True)
121
- repo = Repo(path)
122
- if repo.bare:
123
- return
124
- try:
125
- repo.remotes["origin"].pull()
126
- console.echo("Done")
127
- except Exception as e:
128
- console.echo(e)
129
-
130
- return execute_function_on_directory_files(MULTITOOL_PLUGINS_DIRECTORY, _func, glob="[!.][!__]*")
131
-
132
-
133
- @click.group(help=plugins_command_help)
134
- def plugins():
135
- pass
136
-
137
-
138
- @plugins.command(help="Edit config file manually.")
139
- @common_silent_options
140
- @common_verbose_options
141
- @click.option(
142
- "-a/-A",
143
- "--apply-changes/--no-apply-changes",
144
- default=False,
145
- help="Install plugins from new sources after exiting the editor.",
146
- show_default=True,
147
- )
148
- def configure(silent, verbose, apply_changes):
149
- exit_quietly_if_git_not_installed()
150
- initialize_MULTITOOL_plugins()
151
- click.edit(filename=MULTITOOL_PLUGINS_CONFIG_FILE)
152
- if apply_changes:
153
- clone_plugin_repositories()
154
- prune_unused_plugin_directories()
155
- else:
156
- console.echo("\n Run `apigee plugins update` to apply any changes,")
157
- console.echo(" or rerun `apigee plugins configure` with `-a`")
158
- console.echo(" to apply changes automatically.\n")
159
-
160
-
161
- @plugins.command(help="Update or install plugins.")
162
- @common_silent_options
163
- @common_verbose_options
164
- def update(silent, verbose, section="sources"):
165
- exit_quietly_if_git_not_installed()
166
- clone_plugin_repositories()
167
- update_plugin_repositories()
168
-
169
-
170
- @plugins.command(help="Show plugins information.")
171
- @common_silent_options
172
- @common_verbose_options
173
- @click.option("-n", "--name", help="name of the plugins package")
174
- @optgroup.group(
175
- "Filter options", cls=MutuallyExclusiveOptionGroup, help="The filter options"
176
- )
177
- @optgroup.option(
178
- "--show-commit-only/--no-show-commit-only",
179
- default=False,
180
- help="only print latest Git commit log",
181
- )
182
- @optgroup.option(
183
- "--show-dependencies-only/--no-show-dependencies-only",
184
- default=False,
185
- help="only print list of required packages",
186
- )
187
- def show(
188
- silent,
189
- verbose,
190
- name,
191
- section="sources",
192
- show_commit_only=False,
193
- show_dependencies_only=False,
194
- ):
195
- if not name:
196
- config = load_plugin_config()
197
- if not config._sections:
198
- return
199
- sources = dict(config._sections[section])
200
- for name, uri in sources.items():
201
- console.echo(f"{name}: {uri}")
202
- return
203
- plugins_info_file = Path(MULTITOOL_PLUGINS_DIRECTORY) / name / "apigee-cli.info"
204
- if not is_regular_file(plugins_info_file):
205
- return
206
- plugins_info = read_file_content(plugins_info_file, type="json")
207
- if show_commit_only:
208
- exit_quietly_if_git_not_installed()
209
- console.echo(
210
- Repo(Path(MULTITOOL_PLUGINS_DIRECTORY) / name).git.log(
211
- "--pretty=format:%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset",
212
- "-1",
213
- )
214
- )
215
- return
216
- if show_dependencies_only:
217
- if plugins_info.get("Requires"):
218
- console.echo(plugins_info.get("Requires"))
219
- return
220
- for k, v in plugins_info.items():
221
- console.echo(f"{k}: {v}")
222
-
223
-
224
- @plugins.command(help="Prune plugins with removed sources.")
225
- @common_silent_options
226
- @common_verbose_options
227
- def prune(silent, verbose, section="sources"):
228
- exit_quietly_if_git_not_installed()
229
- prune_unused_plugin_directories()
@@ -1,11 +0,0 @@
1
- requests>=2.28.1
2
- click>=8.1.3
3
- click-aliases>=1.0.1
4
- click-option-group>=0.5.5
5
- colorama>=0.4.6
6
- pyotp>=2.8.0
7
- tqdm>=4.64.1
8
- tabulate>=0.9.0
9
- pyjwt>=2.6.0
10
- python-gnupg>=0.4.9
11
- gitpython>=3.1.30
multitool-0.4.0/setup.py DELETED
@@ -1,53 +0,0 @@
1
- import os
2
-
3
- from multitool import APP
4
- from multitool import __version__ as version
5
- from multitool import description
6
-
7
- readme = os.path.join(os.path.dirname(__file__), 'README.rst')
8
- long_description = open(readme).read()
9
-
10
- SETUP_ARGS = dict(
11
- name=APP,
12
- version=version,
13
- description=(description),
14
- long_description=long_description,
15
- url='https://github.com/mdelotavo/multitool',
16
- author='Matthew Delotavo',
17
- author_email='matthew.t.delotavo@gmail.com',
18
- license='MIT',
19
- include_package_data=True,
20
- classifiers=[
21
- 'Development Status :: 4 - Beta',
22
- 'Environment :: Console',
23
- 'Intended Audience :: Developers',
24
- 'License :: OSI Approved :: MIT License',
25
- 'Operating System :: OS Independent',
26
- 'Programming Language :: Python :: 3.7',
27
- ],
28
- entry_points = {
29
- 'console_scripts': [
30
- 'multitool=multitool.__main__:main'
31
- ]
32
- },
33
- install_requires = [
34
- 'requests>=2.28.1',
35
- 'click>=8.1.3',
36
- 'click-aliases>=1.0.1',
37
- 'click-option-group>=0.5.5',
38
- 'colorama>=0.4.6',
39
- 'pyotp>=2.8.0',
40
- 'tqdm>=4.64.1',
41
- 'tabulate>=0.9.0',
42
- 'pyjwt>=2.6.0',
43
- 'python-gnupg>=0.4.9',
44
- 'gitpython>=3.1.30'
45
- ],
46
- python_requires=">=3.7",
47
- )
48
-
49
- if __name__ == '__main__':
50
- from setuptools import setup, find_packages
51
-
52
- SETUP_ARGS['packages'] = find_packages()
53
- setup(**SETUP_ARGS)