multitool 0.3.2__tar.gz → 0.5.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.3.2 → multitool-0.5.0}/LICENSE +0 -0
  2. {multitool-0.3.2 → multitool-0.5.0}/PKG-INFO +15 -10
  3. {multitool-0.3.2 → multitool-0.5.0}/README.rst +13 -8
  4. multitool-0.5.0/multitool/__init__.py +23 -0
  5. {multitool-0.3.2 → multitool-0.5.0}/multitool/__main__.py +8 -20
  6. {multitool-0.3.2 → multitool-0.5.0}/multitool/cls.py +1 -1
  7. multitool-0.5.0/multitool/console.py +23 -0
  8. {multitool-0.3.2 → multitool-0.5.0}/multitool/exceptions.py +1 -2
  9. {multitool-0.3.2 → multitool-0.5.0}/multitool/plugins/__init__.py +0 -0
  10. multitool-0.5.0/multitool/plugins/commands.py +266 -0
  11. {multitool-0.3.2 → multitool-0.5.0}/multitool/silent.py +0 -0
  12. {multitool-0.3.2 → multitool-0.5.0}/multitool/utils.py +79 -75
  13. {multitool-0.3.2 → multitool-0.5.0}/multitool/verbose.py +0 -1
  14. {multitool-0.3.2 → multitool-0.5.0}/multitool.egg-info/PKG-INFO +15 -10
  15. {multitool-0.3.2 → multitool-0.5.0}/multitool.egg-info/SOURCES.txt +0 -0
  16. {multitool-0.3.2 → multitool-0.5.0}/multitool.egg-info/dependency_links.txt +0 -0
  17. {multitool-0.3.2 → multitool-0.5.0}/multitool.egg-info/entry_points.txt +0 -0
  18. multitool-0.5.0/multitool.egg-info/requires.txt +4 -0
  19. {multitool-0.3.2 → multitool-0.5.0}/multitool.egg-info/top_level.txt +0 -0
  20. {multitool-0.3.2 → multitool-0.5.0}/setup.cfg +0 -0
  21. {multitool-0.3.2 → multitool-0.5.0}/setup.py +11 -20
  22. {multitool-0.3.2 → multitool-0.5.0}/tests/__init__.py +0 -0
  23. {multitool-0.3.2 → multitool-0.5.0}/tests/test_utils.py +0 -0
  24. multitool-0.3.2/multitool/__init__.py +0 -23
  25. multitool-0.3.2/multitool/console.py +0 -17
  26. multitool-0.3.2/multitool/plugins/commands.py +0 -231
  27. multitool-0.3.2/multitool.egg-info/requires.txt +0 -12
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multitool
3
- Version: 0.3.2
3
+ Version: 0.5.0
4
4
  Summary: General-purpose command-line interface with plugins support
5
5
  Home-page: https://github.com/mdelotavo/multitool
6
6
  Author: Matthew Delotavo
@@ -13,7 +13,7 @@ Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3.7
16
- Requires-Python: >=3.6
16
+ Requires-Python: >=3.7
17
17
  License-File: LICENSE
18
18
 
19
19
  multitool
@@ -25,9 +25,7 @@ Multitool is a general-purpose command-line interface with plugins support.
25
25
  Why does this exist?
26
26
  --------------------
27
27
 
28
- I built this tool so that I can quickly create and distribute command-line tools for consulting work and personal use.
29
-
30
- The plugins manager uses ``git`` to manage plugins installed from remote ``git`` repositories.
28
+ I built this tool so I can quickly create and distribute command-line tools for consulting work and personal use.
31
29
 
32
30
  -----
33
31
  Usage
@@ -49,9 +47,6 @@ Usage
49
47
  Commands:
50
48
  plugins Simple plugins manager for distributing commands.
51
49
 
52
- .. ..
53
- The plugins features are based off those found in the `apigeecli`_.
54
-
55
50
  ----------------
56
51
  Managing plugins
57
52
  ----------------
@@ -123,7 +118,7 @@ This will install and update plugins.
123
118
  Pruning
124
119
  ^^^^^^^
125
120
 
126
- If you specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
121
+ 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.
127
122
  Otherwise you can run::
128
123
 
129
124
  multitool plugins prune
@@ -147,6 +142,17 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
147
142
 
148
143
  pip3 install $(multitool plugins show -n PLUGIN_NAME --show-dependencies-only)
149
144
 
145
+ ^^^^^^^^^^^^^^^
146
+ Troubleshooting
147
+ ^^^^^^^^^^^^^^^
148
+
149
+ If you encounter issues with plugins or commands, you can find more detailed debug information and error messages in the log file:
150
+ ::
151
+
152
+ ~/.multitool/multitool.log
153
+
154
+ Reviewing this file can help you diagnose installation problems, missing dependencies, or Git-related errors.
155
+
150
156
  .. ..
151
157
  ----------------------------
152
158
  Create a virtual environment
@@ -168,7 +174,6 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
168
174
  deactivate
169
175
 
170
176
  .. _`click`: https://click.palletsprojects.com/
171
- .. _`apigeecli`: https://pypi.org/project/apigeecli/
172
177
  .. _`multitool-plugins`: https://github.com/mdelotavo/multitool-plugins
173
178
  .. _`public plugins repository`: https://github.com/mdelotavo/multitool-plugins
174
179
 
@@ -7,9 +7,7 @@ Multitool is a general-purpose command-line interface with plugins support.
7
7
  Why does this exist?
8
8
  --------------------
9
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.
10
+ I built this tool so I can quickly create and distribute command-line tools for consulting work and personal use.
13
11
 
14
12
  -----
15
13
  Usage
@@ -31,9 +29,6 @@ Usage
31
29
  Commands:
32
30
  plugins Simple plugins manager for distributing commands.
33
31
 
34
- .. ..
35
- The plugins features are based off those found in the `apigeecli`_.
36
-
37
32
  ----------------
38
33
  Managing plugins
39
34
  ----------------
@@ -105,7 +100,7 @@ This will install and update plugins.
105
100
  Pruning
106
101
  ^^^^^^^
107
102
 
108
- If you specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
103
+ 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
104
  Otherwise you can run::
110
105
 
111
106
  multitool plugins prune
@@ -129,6 +124,17 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
129
124
 
130
125
  pip3 install $(multitool plugins show -n PLUGIN_NAME --show-dependencies-only)
131
126
 
127
+ ^^^^^^^^^^^^^^^
128
+ Troubleshooting
129
+ ^^^^^^^^^^^^^^^
130
+
131
+ If you encounter issues with plugins or commands, you can find more detailed debug information and error messages in the log file:
132
+ ::
133
+
134
+ ~/.multitool/multitool.log
135
+
136
+ Reviewing this file can help you diagnose installation problems, missing dependencies, or Git-related errors.
137
+
132
138
  .. ..
133
139
  ----------------------------
134
140
  Create a virtual environment
@@ -150,6 +156,5 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
150
156
  deactivate
151
157
 
152
158
  .. _`click`: https://click.palletsprojects.com/
153
- .. _`apigeecli`: https://pypi.org/project/apigeecli/
154
159
  .. _`multitool-plugins`: https://github.com/mdelotavo/multitool-plugins
155
160
  .. _`public plugins repository`: https://github.com/mdelotavo/multitool-plugins
@@ -0,0 +1,23 @@
1
+ APP = 'multitool'
2
+ __version__ = '0.5.0'
3
+ description = 'General-purpose command-line interface with plugins support'
4
+ # long_description = 'General-purpose command-line interface with plugins support'
5
+
6
+ import builtins
7
+ from pathlib import Path
8
+
9
+ from multitool.utils import join_path_components
10
+
11
+ MULTITOOL_TOGGLE_SILENT = False
12
+ MULTITOOL_TOGGLE_VERBOSE = 0
13
+
14
+ MULTITOOL_DIRECTORY = join_path_components(Path.home(), '.multitool')
15
+
16
+ MULTITOOL_LOG_FILE = join_path_components(MULTITOOL_DIRECTORY, f'{APP}.log')
17
+
18
+ MULTITOOL_PLUGINS_DIRECTORY = join_path_components(MULTITOOL_DIRECTORY, 'plugins')
19
+ MULTITOOL_PLUGINS_CONFIG_FILE = join_path_components(MULTITOOL_PLUGINS_DIRECTORY, 'config')
20
+ MULTITOOL_PLUGINS_PATH = join_path_components(MULTITOOL_PLUGINS_DIRECTORY, '__init__.py')
21
+
22
+ builtins.MULTITOOL_TOGGLE_SILENT = MULTITOOL_TOGGLE_SILENT
23
+ builtins.MULTITOOL_TOGGLE_VERBOSE = MULTITOOL_TOGGLE_VERBOSE
@@ -1,27 +1,15 @@
1
1
  #!/usr/bin/env python
2
2
  # __main__.py
3
3
 
4
- import codecs
5
- import configparser
6
- import importlib
7
- import os
8
- import re
9
- import sys
10
- import time
11
- from functools import update_wrapper
12
-
13
4
  import click
14
- import requests
15
5
 
16
- from multitool import (APP, MULTITOOL_LOG_FILE, MULTITOOL_PLUGINS_DIRECTORY,
17
- MULTITOOL_PLUGINS_PATH)
6
+ from multitool import APP, MULTITOOL_LOG_FILE, MULTITOOL_PLUGINS_DIRECTORY
18
7
  from multitool import __version__ as version
19
8
  from multitool.cls import AliasedGroup
20
- from multitool.exceptions import exception_handler
9
+ from multitool.exceptions import wrap_with_exception_handling
21
10
  from multitool.plugins.commands import plugins
22
- from multitool.utils import (import_all_modules_in_directory,
23
- run_func_on_dir_files, setup_global_logger,
24
- show_message)
11
+ from multitool.utils import (import_plugins_from_directory,
12
+ execute_function_on_directory_files, configure_global_logger)
25
13
 
26
14
  CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
27
15
 
@@ -45,15 +33,15 @@ def cli(ctx):
45
33
  ctx.ensure_object(dict)
46
34
 
47
35
 
48
- @exception_handler
36
+ @wrap_with_exception_handling
49
37
  def main():
50
- setup_global_logger(MULTITOOL_LOG_FILE)
38
+ configure_global_logger(MULTITOOL_LOG_FILE)
51
39
 
52
40
  cli_commands = {plugins}
53
41
 
54
- run_func_on_dir_files(
42
+ execute_function_on_directory_files(
55
43
  MULTITOOL_PLUGINS_DIRECTORY,
56
- import_all_modules_in_directory,
44
+ import_plugins_from_directory,
57
45
  args=(cli_commands,),
58
46
  glob='[!.][!__]*/__init__.py',
59
47
  )
@@ -11,4 +11,4 @@ class AliasedGroup(click.Group):
11
11
  return None
12
12
  elif len(matches) == 1:
13
13
  return click.Group.get_command(self, ctx, matches[0])
14
- ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))
14
+ ctx.fail(f'Too many matches: {", ".join(sorted(matches))}')
@@ -0,0 +1,23 @@
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,13 +1,12 @@
1
1
  import functools
2
2
  import inspect
3
- import json
4
3
  import logging
5
4
  import sys
6
5
 
7
6
  from multitool import console
8
7
 
9
8
 
10
- def exception_handler(func):
9
+ def wrap_with_exception_handling(func):
11
10
  @functools.wraps(func)
12
11
  def wrapper(*args, **kwargs):
13
12
  try:
@@ -0,0 +1,266 @@
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 (
13
+ APP,
14
+ MULTITOOL_PLUGINS_CONFIG_FILE,
15
+ MULTITOOL_PLUGINS_DIRECTORY,
16
+ MULTITOOL_PLUGINS_PATH,
17
+ console,
18
+ )
19
+ from multitool.silent import common_silent_options
20
+ from multitool.utils import (
21
+ is_directory,
22
+ is_regular_file,
23
+ create_directory,
24
+ read_file_content,
25
+ execute_function_on_directory_files,
26
+ create_empty_file,
27
+ )
28
+ from multitool.verbose import common_verbose_options
29
+
30
+
31
+ # ---- Global Variables ----
32
+ plugins_command_help = 'Simple plugins manager for distributing commands.'
33
+
34
+ # ---- Git Check ----
35
+ is_git_installed = False
36
+ try:
37
+ from git import Repo
38
+ is_git_installed = True
39
+ except ImportError:
40
+ plugins_command_help = (
41
+ "Simple plugins manager for distributing commands. "
42
+ "[Warning: Git must be installed to use plugin commands]"
43
+ )
44
+
45
+
46
+ # ---- Helper Classes ----
47
+ class FileUtils:
48
+ """Internal helper class for file/directory operations."""
49
+
50
+ @staticmethod
51
+ def chmod_directory(directory, mode):
52
+ """Recursively change permissions of a directory and its contents."""
53
+ for root, dirs, files in os.walk(directory):
54
+ for d in dirs:
55
+ os.chmod(path.join(root, d), mode)
56
+ for f in files:
57
+ os.chmod(path.join(root, f), mode)
58
+
59
+
60
+ class GitUtils:
61
+ """Internal helper class for Git operations."""
62
+
63
+ @staticmethod
64
+ def exit_if_not_installed():
65
+ if not is_git_installed:
66
+ sys.exit(0)
67
+
68
+
69
+ class PluginManager:
70
+ """Internal helper class for MULTITOOL plugin management."""
71
+
72
+ @staticmethod
73
+ def initialize_plugins():
74
+ create_directory(MULTITOOL_PLUGINS_DIRECTORY)
75
+ create_empty_file(MULTITOOL_PLUGINS_PATH)
76
+ create_empty_file(MULTITOOL_PLUGINS_CONFIG_FILE)
77
+
78
+ @staticmethod
79
+ def load_config(config_file=MULTITOOL_PLUGINS_CONFIG_FILE):
80
+ config = configparser.ConfigParser(allow_no_value=True)
81
+ config.read(config_file)
82
+ return config
83
+
84
+ @staticmethod
85
+ def clone_repositories(section="sources"):
86
+ PluginManager.initialize_plugins()
87
+ config = PluginManager.load_config()
88
+ if not config._sections:
89
+ return
90
+ sources = dict(config._sections[section])
91
+ for name, uri in sources.items():
92
+ dest = Path(MULTITOOL_PLUGINS_DIRECTORY) / name
93
+ if is_directory(dest):
94
+ continue
95
+ try:
96
+ console.echo(f"Installing {name}... ", line_ending="", should_flush=True)
97
+ Repo.clone_from(uri, dest)
98
+ console.echo("Done")
99
+ except Exception as e:
100
+ console.echo(e)
101
+
102
+ @staticmethod
103
+ def prune_unused_directories(section="sources"):
104
+ PluginManager.initialize_plugins()
105
+ config = PluginManager.load_config()
106
+ if not config._sections:
107
+ return
108
+ sources = dict(config._sections[section])
109
+
110
+ def _func(path_):
111
+ if not is_directory(path_):
112
+ return
113
+ name = Path(path_).stem
114
+ if name in sources:
115
+ return
116
+ console.echo(f"Removing {name}... ", line_ending="", should_flush=True)
117
+ plugin_directory = Path(MULTITOOL_PLUGINS_DIRECTORY) / name
118
+ try:
119
+ FileUtils.chmod_directory(str(Path(plugin_directory) / ".git"), stat.S_IRWXU)
120
+ shutil.rmtree(plugin_directory)
121
+ console.echo("Done")
122
+ except Exception as e:
123
+ console.echo(e)
124
+
125
+ return execute_function_on_directory_files(
126
+ MULTITOOL_PLUGINS_DIRECTORY, _func, glob="[!.][!__]*"
127
+ )
128
+
129
+ @staticmethod
130
+ def update_repositories():
131
+ def _func(path_):
132
+ if not is_directory(path_):
133
+ return
134
+ console.echo(f"Updating {Path(path_).stem}... ", line_ending="", should_flush=True)
135
+ repo = Repo(path_)
136
+ if repo.bare:
137
+ return
138
+ try:
139
+ repo.remotes["origin"].pull()
140
+ console.echo("Done")
141
+ except Exception as e:
142
+ console.echo(e)
143
+
144
+ return execute_function_on_directory_files(
145
+ MULTITOOL_PLUGINS_DIRECTORY, _func, glob="[!.][!__]*"
146
+ )
147
+
148
+ @staticmethod
149
+ def list_sources(section="sources"):
150
+ """Return a dictionary of plugin sources from config."""
151
+ config = PluginManager.load_config()
152
+ return dict(config._sections.get(section, {}))
153
+
154
+ @staticmethod
155
+ def get_plugin_info(name):
156
+ """Return the parsed plugin info JSON or None if file missing."""
157
+ info_file = Path(MULTITOOL_PLUGINS_DIRECTORY) / name / f"{APP}-info.json"
158
+ if not is_regular_file(info_file):
159
+ return None
160
+ return read_file_content(info_file, type="json")
161
+
162
+ @staticmethod
163
+ def print_latest_commit(name):
164
+ """Print the latest Git commit of a plugin repository."""
165
+ GitUtils.exit_if_not_installed()
166
+ repo = Repo(Path(MULTITOOL_PLUGINS_DIRECTORY) / name)
167
+ console.echo(repo.git.log(
168
+ "--pretty=format:%Cred%h%Creset -%C(yellow)%d%Creset %s "
169
+ "%Cgreen(%cr) %C(bold blue)<%an>%Creset", "-1"
170
+ ))
171
+
172
+ @staticmethod
173
+ def print_dependencies(plugins_info):
174
+ """Print the 'Requires' field from plugin info, if any."""
175
+ requires = plugins_info.get("Requires")
176
+ if requires:
177
+ console.echo(requires)
178
+
179
+ @staticmethod
180
+ def print_full_info(plugins_info):
181
+ """Print all key/value pairs from plugin info."""
182
+ for key, value in plugins_info.items():
183
+ console.echo(f"{key}: {value}")
184
+
185
+
186
+ # ---- CLI Commands ----
187
+ @click.group(help=plugins_command_help)
188
+ def plugins():
189
+ pass
190
+
191
+
192
+ @plugins.command(help="Edit config file manually.")
193
+ @common_silent_options
194
+ @common_verbose_options
195
+ @click.option(
196
+ "-a/-A",
197
+ "--apply-changes/--no-apply-changes",
198
+ default=False,
199
+ help="Install plugins from new sources after exiting the editor.",
200
+ show_default=True,
201
+ )
202
+ def configure(silent, verbose, apply_changes):
203
+ GitUtils.exit_if_not_installed()
204
+ PluginManager.initialize_plugins()
205
+ click.edit(filename=MULTITOOL_PLUGINS_CONFIG_FILE)
206
+ if apply_changes:
207
+ PluginManager.clone_repositories()
208
+ PluginManager.prune_unused_directories()
209
+ else:
210
+ console.echo(f"\n Run `{APP} plugins update` to apply any changes,")
211
+ console.echo(f" or rerun `{APP} plugins configure` with `-a`")
212
+ console.echo(" to apply changes automatically.\n")
213
+
214
+
215
+ @plugins.command(help="Update or install plugins.")
216
+ @common_silent_options
217
+ @common_verbose_options
218
+ def update(silent, verbose, section="sources"):
219
+ GitUtils.exit_if_not_installed()
220
+ PluginManager.clone_repositories()
221
+ PluginManager.update_repositories()
222
+
223
+
224
+ @plugins.command(help="Show plugins information.")
225
+ @common_silent_options
226
+ @common_verbose_options
227
+ @click.option("-n", "--name", help="Name of the plugins package")
228
+ @optgroup.group("Filter options", cls=MutuallyExclusiveOptionGroup, help="The filter options")
229
+ @optgroup.option(
230
+ "--show-commit-only/--no-show-commit-only",
231
+ default=False,
232
+ help="Only print latest Git commit log",
233
+ )
234
+ @optgroup.option(
235
+ "--show-dependencies-only/--no-show-dependencies-only",
236
+ default=False,
237
+ help="Only print list of required packages",
238
+ )
239
+ def show(silent, verbose, name, section="sources", show_commit_only=False, show_dependencies_only=False):
240
+ """Show plugins sources, info, commits, or dependencies based on options."""
241
+
242
+ if not name:
243
+ # List all plugin sources
244
+ sources = PluginManager.list_sources(section)
245
+ for plugin_name, uri in sources.items():
246
+ console.echo(f"{plugin_name}: {uri}")
247
+ return
248
+
249
+ plugins_info = PluginManager.get_plugin_info(name)
250
+ if not plugins_info:
251
+ return
252
+
253
+ if show_commit_only:
254
+ PluginManager.print_latest_commit(name)
255
+ elif show_dependencies_only:
256
+ PluginManager.print_dependencies(plugins_info)
257
+ else:
258
+ PluginManager.print_full_info(plugins_info)
259
+
260
+
261
+ @plugins.command(help="Prune plugins with removed sources.")
262
+ @common_silent_options
263
+ @common_verbose_options
264
+ def prune(silent, verbose, section="sources"):
265
+ GitUtils.exit_if_not_installed()
266
+ PluginManager.prune_unused_directories()
File without changes
@@ -10,38 +10,76 @@ from pathlib import Path
10
10
  import click
11
11
 
12
12
 
13
- def gen_path_str(*args):
14
- if not args:
15
- return
16
- path = None
17
- for arg in args:
18
- if not path:
19
- path = Path(arg)
20
- else:
21
- path /= arg
22
- return str(path)
13
+ def apply_function_on_iterable(iterable, func, state_op="append", args=(), kwargs=None):
14
+ if kwargs is None:
15
+ kwargs = {}
16
+ state = []
17
+ for item in iterable:
18
+ _tuple = (item,)
19
+ result = func(*(_tuple + args), **kwargs)
20
+ if result:
21
+ getattr(state, state_op)(result)
22
+ return state
23
23
 
24
24
 
25
- def is_dir(d):
26
- return os.path.isdir(d)
25
+ def configure_global_logger(log_file):
26
+ create_empty_file(log_file)
27
+ remove_file_if_above_size(log_file, size_kb=1000)
28
+ logging.basicConfig(
29
+ filename=log_file,
30
+ level=logging.WARNING,
31
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
32
+ )
27
33
 
28
34
 
29
- def is_envvar_true(value):
30
- return value in (True, 'True', 'true', '1')
35
+ def create_empty_file(path):
36
+ try:
37
+ create_directory(os.path.split(path)[0])
38
+ if not os.path.exists(path):
39
+ with open(path, "x"):
40
+ os.utime(path, None)
41
+ except FileExistsError:
42
+ logging.warning(f"{inspect.stack()[0][3]}; will ignore FileExistsError")
31
43
 
32
44
 
33
- def is_file(f):
34
- return os.path.isfile(f)
45
+ def create_directory(path):
46
+ if not path:
47
+ return
48
+ if not os.path.exists(path):
49
+ try:
50
+ os.makedirs(path)
51
+ except FileExistsError:
52
+ logging.warning(f"{inspect.stack()[0][3]}; will ignore FileExistsError")
53
+
35
54
 
55
+ def execute_function_on_directory_files(dir, func, glob="**/*", args=(), kwargs=None):
56
+ if kwargs is None:
57
+ kwargs = {}
58
+ state = []
59
+ for file_path in Path(get_resolved_directory_path(dir)).resolve().glob(glob):
60
+ _tuple = (str(file_path),)
61
+ result = func(*(_tuple + args), **kwargs)
62
+ if result:
63
+ state.append(result)
64
+ return state
36
65
 
37
- def import_all_modules_in_directory(plugins_init_file, existing_commands):
66
+
67
+ def get_resolved_directory_path(target_directory=None):
68
+ if target_directory:
69
+ if not os.path.exists(target_directory):
70
+ os.makedirs(target_directory)
71
+ return str(Path(target_directory).resolve())
72
+ return os.getcwd()
73
+
74
+
75
+ def import_plugins_from_directory(plugins_init_file, existing_commands):
38
76
  try:
39
77
  spec = importlib.util.spec_from_file_location('plugins_modules', plugins_init_file)
40
78
  module = importlib.util.module_from_spec(spec)
41
79
  sys.modules[spec.name] = module
42
80
  spec.loader.exec_module(module)
43
- import plugins_modules
44
- from plugins_modules import __all__ as all_plugins_modules
81
+ import plugins_modules # type: ignore
82
+ from plugins_modules import __all__ as all_plugins_modules # type: ignore
45
83
 
46
84
  for module in all_plugins_modules:
47
85
  _module = getattr(plugins_modules, module)
@@ -53,73 +91,39 @@ def import_all_modules_in_directory(plugins_init_file, existing_commands):
53
91
  )
54
92
 
55
93
 
56
- def make_dirs(path):
57
- if not os.path.exists(path):
58
- try:
59
- os.makedirs(path)
60
- except FileExistsError:
61
- logging.warning(f'{inspect.stack()[0][3]}; will ignore FileExistsError')
94
+ def is_directory(d):
95
+ return os.path.isdir(d)
62
96
 
63
97
 
64
- def read_file(file, type='text'):
65
- with open(file, 'r') as f:
66
- if type == 'json':
67
- return json.loads(f.read())
68
- return f.read()
98
+ def is_regular_file(f):
99
+ return os.path.isfile(f)
69
100
 
70
101
 
71
- def remove_file_above_size(file, size_kb=100):
72
- if os.path.getsize(file) > size_kb * 1024:
73
- os.remove(file)
102
+ def is_truthy_envvar(value):
103
+ return value in (True, "True", "true", "1")
74
104
 
75
105
 
76
- def resolve_target_directory(target_directory=None):
77
- if target_directory:
78
- if not os.path.exists(target_directory):
79
- os.makedirs(target_directory)
80
- return str(Path(target_directory).resolve())
81
- return os.getcwd()
82
-
83
-
84
- def run_func_on_dir_files(dir, func, glob='**/*', args=(), kwargs={}):
85
- state = []
86
- for file_path in Path(resolve_target_directory(dir)).resolve().glob(glob):
87
- _tuple = (str(file_path),)
88
- result = func(*(_tuple + args), **kwargs)
89
- if result:
90
- state.append(result)
91
- return state
106
+ def join_path_components(*components):
107
+ if not components:
108
+ return
109
+ path = None
110
+ for component in components:
111
+ if not path:
112
+ path = Path(component)
113
+ else:
114
+ path /= component
115
+ return str(path)
92
116
 
93
117
 
94
- def run_func_on_iterable(iterable, func, state_op='append', args=(), kwargs={}):
95
- state = []
96
- for item in iterable:
97
- _tuple = (item,)
98
- result = func(*(_tuple + args), **kwargs)
99
- if result:
100
- getattr(state, state_op)(result)
101
- return state
118
+ def read_file_content(file, type="text"):
119
+ with open(file, "r") as f:
120
+ return json.loads(f.read()) if type == "json" else f.read()
102
121
 
103
122
 
104
- def setup_global_logger(log_file):
105
- touch(log_file)
106
- logging.basicConfig(
107
- filename=log_file,
108
- level=logging.DEBUG,
109
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
110
- )
111
- remove_file_above_size(log_file, size_kb=1000)
123
+ def remove_file_if_above_size(file, size_kb=100):
124
+ if os.path.getsize(file) > size_kb * 1024:
125
+ os.remove(file)
112
126
 
113
127
 
114
128
  def show_message(msg):
115
129
  print(msg)
116
-
117
-
118
- def touch(path):
119
- try:
120
- make_dirs(os.path.split(path)[0])
121
- if not os.path.exists(path):
122
- with open(path, 'x'):
123
- os.utime(path, None)
124
- except FileExistsError:
125
- logging.warning(f'{inspect.stack()[0][3]}; will ignore FileExistsError')
@@ -3,7 +3,6 @@ import http.client as http_client
3
3
  import logging
4
4
 
5
5
  import click
6
- import requests
7
6
 
8
7
 
9
8
  def verbose_callback(ctx, param, value):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multitool
3
- Version: 0.3.2
3
+ Version: 0.5.0
4
4
  Summary: General-purpose command-line interface with plugins support
5
5
  Home-page: https://github.com/mdelotavo/multitool
6
6
  Author: Matthew Delotavo
@@ -13,7 +13,7 @@ Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3.7
16
- Requires-Python: >=3.6
16
+ Requires-Python: >=3.7
17
17
  License-File: LICENSE
18
18
 
19
19
  multitool
@@ -25,9 +25,7 @@ Multitool is a general-purpose command-line interface with plugins support.
25
25
  Why does this exist?
26
26
  --------------------
27
27
 
28
- I built this tool so that I can quickly create and distribute command-line tools for consulting work and personal use.
29
-
30
- The plugins manager uses ``git`` to manage plugins installed from remote ``git`` repositories.
28
+ I built this tool so I can quickly create and distribute command-line tools for consulting work and personal use.
31
29
 
32
30
  -----
33
31
  Usage
@@ -49,9 +47,6 @@ Usage
49
47
  Commands:
50
48
  plugins Simple plugins manager for distributing commands.
51
49
 
52
- .. ..
53
- The plugins features are based off those found in the `apigeecli`_.
54
-
55
50
  ----------------
56
51
  Managing plugins
57
52
  ----------------
@@ -123,7 +118,7 @@ This will install and update plugins.
123
118
  Pruning
124
119
  ^^^^^^^
125
120
 
126
- If you specified the ``-a`` option when running ``multitool plugins configure`` then the removal of plugins will occur automatically.
121
+ 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.
127
122
  Otherwise you can run::
128
123
 
129
124
  multitool plugins prune
@@ -147,6 +142,17 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
147
142
 
148
143
  pip3 install $(multitool plugins show -n PLUGIN_NAME --show-dependencies-only)
149
144
 
145
+ ^^^^^^^^^^^^^^^
146
+ Troubleshooting
147
+ ^^^^^^^^^^^^^^^
148
+
149
+ If you encounter issues with plugins or commands, you can find more detailed debug information and error messages in the log file:
150
+ ::
151
+
152
+ ~/.multitool/multitool.log
153
+
154
+ Reviewing this file can help you diagnose installation problems, missing dependencies, or Git-related errors.
155
+
150
156
  .. ..
151
157
  ----------------------------
152
158
  Create a virtual environment
@@ -168,7 +174,6 @@ In order for this to work, the plugin needs to have the ``Requires`` key in the
168
174
  deactivate
169
175
 
170
176
  .. _`click`: https://click.palletsprojects.com/
171
- .. _`apigeecli`: https://pypi.org/project/apigeecli/
172
177
  .. _`multitool-plugins`: https://github.com/mdelotavo/multitool-plugins
173
178
  .. _`public plugins repository`: https://github.com/mdelotavo/multitool-plugins
174
179
 
@@ -0,0 +1,4 @@
1
+ click-aliases>=1.0.1
2
+ click-option-group>=0.5.5
3
+ click>=8.1.3
4
+ gitpython>=3.1.30
File without changes
@@ -1,4 +1,4 @@
1
- import os, sys, re
1
+ import os
2
2
 
3
3
  from multitool import APP
4
4
  from multitool import __version__ as version
@@ -25,27 +25,18 @@ SETUP_ARGS = dict(
25
25
  'Operating System :: OS Independent',
26
26
  'Programming Language :: Python :: 3.7',
27
27
  ],
28
- # py_modules = ['multitool',],
29
- # entry_points='''
30
- # [console_scripts]
31
- # multitool=multitool.__main__:cli
32
- # ''',
33
- entry_points={'console_scripts': ['multitool=multitool.__main__:main']},
28
+ entry_points = {
29
+ 'console_scripts': [
30
+ 'multitool=multitool.__main__:main'
31
+ ]
32
+ },
34
33
  install_requires = [
35
- 'requests>=2.22',
36
- 'click',
37
- 'click-aliases',
38
- 'click-option-group',
39
- 'colorama',
40
- 'pyotp',
41
- 'requests',
42
- 'tqdm',
43
- 'tabulate',
44
- 'pyjwt',
45
- 'python-gnupg>=0.3.5',
46
- 'gitpython'
34
+ 'click>=8.1.3',
35
+ 'click-aliases>=1.0.1',
36
+ 'click-option-group>=0.5.5',
37
+ 'gitpython>=3.1.30'
47
38
  ],
48
- python_requires=">=3.6",
39
+ python_requires=">=3.7",
49
40
  )
50
41
 
51
42
  if __name__ == '__main__':
File without changes
File without changes
@@ -1,23 +0,0 @@
1
- APP = 'multitool'
2
- __version__ = '0.3.2'
3
- description = 'General-purpose command-line interface with plugins support'
4
- # long_description = 'General-purpose command-line interface with plugins support'
5
-
6
- import builtins
7
- from pathlib import Path
8
-
9
- from multitool.utils import gen_path_str
10
-
11
- MULTITOOL_TOGGLE_SILENT = False
12
- MULTITOOL_TOGGLE_VERBOSE = 0
13
-
14
- MULTITOOL_DIRECTORY = gen_path_str(Path.home(), '.multitool')
15
-
16
- MULTITOOL_LOG_FILE = gen_path_str(MULTITOOL_DIRECTORY, f'{APP}.log')
17
-
18
- MULTITOOL_PLUGINS_DIRECTORY = gen_path_str(MULTITOOL_DIRECTORY, 'plugins')
19
- MULTITOOL_PLUGINS_CONFIG_FILE = gen_path_str(MULTITOOL_PLUGINS_DIRECTORY, 'config')
20
- MULTITOOL_PLUGINS_PATH = gen_path_str(MULTITOOL_PLUGINS_DIRECTORY, '__init__.py')
21
-
22
- builtins.MULTITOOL_TOGGLE_SILENT = MULTITOOL_TOGGLE_SILENT
23
- builtins.MULTITOOL_TOGGLE_VERBOSE = MULTITOOL_TOGGLE_VERBOSE
@@ -1,17 +0,0 @@
1
- import builtins
2
- import sys
3
-
4
-
5
- def echo(
6
- *message, status=None, silent=False, curr_verbosity=0, expc_verbosity=0, end='\n', flush=False
7
- ):
8
- toggle_silent = builtins.MULTITOOL_TOGGLE_SILENT
9
- toggle_verbose = builtins.MULTITOOL_TOGGLE_VERBOSE
10
- if silent or toggle_silent:
11
- if status:
12
- sys.exit(status)
13
- return
14
- if curr_verbosity or toggle_verbose >= expc_verbosity:
15
- print(*message, end=end, flush=flush)
16
- if status:
17
- sys.exit(status)
@@ -1,231 +0,0 @@
1
- import configparser
2
- import json
3
- import os
4
- import shutil
5
- import stat
6
- import sys
7
- from os import path
8
- from pathlib import Path
9
-
10
- import click
11
- from click_option_group import MutuallyExclusiveOptionGroup, optgroup
12
-
13
- from multitool import (MULTITOOL_PLUGINS_CONFIG_FILE,
14
- MULTITOOL_PLUGINS_DIRECTORY, MULTITOOL_PLUGINS_PATH,
15
- console)
16
- from multitool.silent import common_silent_options
17
- from multitool.utils import (is_dir, is_file, make_dirs, read_file,
18
- run_func_on_dir_files, touch)
19
- from multitool.verbose import common_verbose_options
20
-
21
- is_git_installed = False
22
- plugins_command_help = 'Simple plugins manager for distributing commands.'
23
-
24
- try:
25
- import git
26
- from git import Git, Repo
27
-
28
- is_git_installed = True
29
- except ImportError:
30
- plugins_command_help = '[Git not found! Please install Git first.] Simple plugins manager for distributing commands. This command requires Git to be installed.'
31
-
32
-
33
- def exit_if_git_not_installed():
34
- if not is_git_installed:
35
- sys.exit(0)
36
-
37
-
38
- @click.group(help=plugins_command_help)
39
- def plugins():
40
- pass
41
-
42
-
43
- def init():
44
- make_dirs(MULTITOOL_PLUGINS_DIRECTORY)
45
- touch(MULTITOOL_PLUGINS_PATH)
46
- touch(MULTITOOL_PLUGINS_CONFIG_FILE)
47
-
48
-
49
- def load_config(config_file=MULTITOOL_PLUGINS_CONFIG_FILE):
50
- config = configparser.ConfigParser(allow_no_value=True)
51
- config.read(config_file)
52
- return config
53
-
54
-
55
- def save_config(plugins_file, config_file, section='sources'):
56
- init()
57
- config = load_config()
58
- if not config._sections:
59
- return
60
-
61
-
62
- def clone_all(section='sources'):
63
- init()
64
- config = load_config()
65
- if not config._sections:
66
- return
67
- sources = dict(config._sections[section])
68
- for name, uri in sources.items():
69
- dest = Path(MULTITOOL_PLUGINS_DIRECTORY) / name
70
- if is_dir(dest):
71
- continue
72
- try:
73
- console.echo(f'Installing {name}... ', end='', flush=True)
74
- Repo.clone_from(uri, dest)
75
- console.echo('Done')
76
- except Exception as e:
77
- console.echo(e)
78
-
79
-
80
- def pull_all():
81
- def _func(path):
82
- if not is_dir(path):
83
- return
84
- console.echo(f'Updating {Path(path).stem}... ', end='', flush=True)
85
- repo = Repo(path)
86
- if repo.bare:
87
- return
88
- try:
89
- repo.remotes['origin'].pull()
90
- console.echo('Done')
91
- except Exception as e:
92
- console.echo(e)
93
-
94
- return run_func_on_dir_files(MULTITOOL_PLUGINS_DIRECTORY, _func, glob='[!.][!__]*')
95
-
96
-
97
- @plugins.command(help='Edit config file manually.')
98
- @common_silent_options
99
- @common_verbose_options
100
- @click.option(
101
- '-a/-A',
102
- '--apply-changes/--no-apply-changes',
103
- default=False,
104
- help='Install plugins from new sources after exiting the editor.',
105
- show_default=True,
106
- )
107
- def configure(silent, verbose, apply_changes):
108
- exit_if_git_not_installed()
109
- init()
110
- click.edit(filename=MULTITOOL_PLUGINS_CONFIG_FILE)
111
- if apply_changes:
112
- clone_all()
113
- prune_all()
114
- else:
115
- console.echo('\n Run `multitool plugins update` to apply any changes,')
116
- console.echo(' or rerun `multitool plugins configure` with `-a`')
117
- console.echo(' to apply changes automatically.\n')
118
-
119
-
120
- def install():
121
- pass
122
-
123
-
124
- @plugins.command(help='Update or install plugins.')
125
- @common_silent_options
126
- @common_verbose_options
127
- def update(silent, verbose, section='sources'):
128
- exit_if_git_not_installed()
129
- clone_all()
130
- pull_all()
131
-
132
-
133
- @plugins.command(help='Show plugins information.')
134
- @common_silent_options
135
- @common_verbose_options
136
- @click.option('-n', '--name', help='name of the plugins package')
137
- @optgroup.group('Filter options', cls=MutuallyExclusiveOptionGroup, help='The filter options')
138
- @optgroup.option(
139
- '--show-commit-only/--no-show-commit-only',
140
- default=False,
141
- help='only print latest Git commit log',
142
- )
143
- @optgroup.option(
144
- '--show-dependencies-only/--no-show-dependencies-only',
145
- default=False,
146
- help='only print list of required packages',
147
- )
148
- def show(
149
- silent, verbose, name, section='sources', show_commit_only=False, show_dependencies_only=False
150
- ):
151
- if not name:
152
- config = load_config()
153
- if not config._sections:
154
- return
155
- sources = dict(config._sections[section])
156
- for name, uri in sources.items():
157
- console.echo(f'{name}: {uri}')
158
- return
159
- plugins_info_file = Path(MULTITOOL_PLUGINS_DIRECTORY) / name / 'multitool-info.json'
160
- if not is_file(plugins_info_file):
161
- return
162
- plugins_info = read_file(plugins_info_file, type='json')
163
- if show_commit_only:
164
- exit_if_git_not_installed()
165
- console.echo(
166
- Repo(Path(MULTITOOL_PLUGINS_DIRECTORY) / name).git.log(
167
- '--pretty=format:%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset',
168
- '-1',
169
- )
170
- )
171
- return
172
- if show_dependencies_only:
173
- if plugins_info.get('Requires'):
174
- console.echo(plugins_info.get('Requires'))
175
- return
176
- for k, v in plugins_info.items():
177
- console.echo(f'{k}: {v}')
178
-
179
-
180
- def info():
181
- pass
182
-
183
-
184
- def chmod_directory(directory, mode):
185
- """https://stackoverflow.com/a/58878271"""
186
- for root, dirs, files in os.walk(directory):
187
- for dir in dirs:
188
- os.chmod(path.join(root, dir), mode)
189
- for file in files:
190
- os.chmod(path.join(root, file), mode)
191
-
192
-
193
- def prune_all(section='sources'):
194
- init()
195
- config = load_config()
196
- if not config._sections:
197
- return
198
- sources = dict(config._sections[section])
199
-
200
- def _func(path):
201
- if not is_dir(path):
202
- return
203
- name = Path(path).stem
204
- if name in sources.keys():
205
- return
206
- console.echo(f'Removing {name}... ', end='', flush=True)
207
- plugin_directory = Path(MULTITOOL_PLUGINS_DIRECTORY) / name
208
- try:
209
- chmod_directory(str(Path(plugin_directory) / '.git'), stat.S_IRWXU)
210
- shutil.rmtree(plugin_directory)
211
- console.echo('Done')
212
- except Exception as e:
213
- console.echo(e)
214
-
215
- return run_func_on_dir_files(MULTITOOL_PLUGINS_DIRECTORY, _func, glob='[!.][!__]*')
216
-
217
-
218
- @plugins.command(help='Prune plugins with removed sources.')
219
- @common_silent_options
220
- @common_verbose_options
221
- def prune(silent, verbose, section='sources'):
222
- exit_if_git_not_installed()
223
- prune_all()
224
-
225
-
226
- def uninstall():
227
- pass
228
-
229
-
230
- def clean():
231
- pass
@@ -1,12 +0,0 @@
1
- requests>=2.22
2
- click
3
- click-aliases
4
- click-option-group
5
- colorama
6
- pyotp
7
- requests
8
- tqdm
9
- tabulate
10
- pyjwt
11
- python-gnupg>=0.3.5
12
- gitpython