speedtest-ssh 2.6.2__tar.gz → 2.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 (22) hide show
  1. {speedtest_ssh-2.6.2/speedtest_ssh.egg-info → speedtest_ssh-2.7.1}/PKG-INFO +1 -1
  2. speedtest_ssh-2.7.1/speedtest_ssh/_version.py +1 -0
  3. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/data_transfer.py +20 -19
  4. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/ping.py +10 -9
  5. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/sftp_wrapper.py +14 -0
  6. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/speedtest_ssh.py +34 -26
  7. speedtest_ssh-2.7.1/speedtest_ssh/util.py +66 -0
  8. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1/speedtest_ssh.egg-info}/PKG-INFO +1 -1
  9. speedtest_ssh-2.6.2/speedtest_ssh/_version.py +0 -1
  10. speedtest_ssh-2.6.2/speedtest_ssh/util.py +0 -30
  11. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/LICENSE +0 -0
  12. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/README.md +0 -0
  13. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/pyproject.toml +0 -0
  14. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/setup.cfg +0 -0
  15. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/__init__.py +0 -0
  16. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/config.py +0 -0
  17. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/py.typed +0 -0
  18. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/SOURCES.txt +0 -0
  19. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/dependency_links.txt +0 -0
  20. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/entry_points.txt +0 -0
  21. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/requires.txt +0 -0
  22. {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: speedtest_ssh
3
- Version: 2.6.2
3
+ Version: 2.7.1
4
4
  Summary: A tool meant to check inter-device speeds via ssh
5
5
  License: GPL
6
6
  Project-URL: Homepage, https://github.com/zwimer/speedtest_ssh
@@ -0,0 +1 @@
1
+ __version__: str = "2.7.1"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from subprocess import CalledProcessError, PIPE
3
+ from logging import getLogger, DEBUG, INFO
3
4
  from typing import TYPE_CHECKING
4
5
  from datetime import datetime
5
6
  from pathlib import Path
@@ -46,31 +47,29 @@ class DataTransfer:
46
47
  This class is a context manager; on exit will remove the remote file on deletion
47
48
  """
48
49
 
49
- def __init__(self, config: Config, verbose: bool):
50
+ _LOG = "DataTransfer"
51
+
52
+ def __init__(self, config: Config):
50
53
  """
51
54
  :param config: The Config object the DataTransfer instance should use
52
- :param verbose: If true, be more verbose
53
55
  """
54
- self._verbose = verbose
56
+ self._l = getLogger(self._LOG)
55
57
  rand = lambda: random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits)
56
58
  self._remote_f: str = f"/tmp/speedtest-ssh_{datetime.now()}_{''.join(rand() for _ in range(8))}.tmp"
57
59
  self._remote_f = self._remote_f.replace(":", "-").replace(" ", "_")
58
60
  # We promise that _remote_f components match: ^[a-zA-Z\d_.-]+$ (old rsync args suck)
59
- if self._verbose:
60
- print("Parsing ssh config and loading keys...")
61
+ self._l.debug("Parsing ssh config and loading keys...")
61
62
  self._sftp_cm = sftp_wrapper(config)
62
63
  self._sftp: SFTPClient # Defined in __enter__
63
64
 
64
65
  def __enter__(self) -> DataTransfer:
65
- if self._verbose:
66
- print("Establishing SFTP connection...")
66
+ self._l.debug("Chosen remote file name: %s", self._remote_f)
67
67
  self._sftp = self._sftp_cm.__enter__()
68
68
  return self
69
69
 
70
70
  def __exit__(self, *args, **kwargs):
71
71
  self.clean_remote()
72
- if self._verbose:
73
- print("Terminating SFTP connection...")
72
+ self._l.debug("Terminating SFTP connection...")
74
73
  self._sftp_cm.__exit__(*args, **kwargs)
75
74
 
76
75
  def put(self, local: Path) -> None:
@@ -90,8 +89,7 @@ class DataTransfer:
90
89
  Removes the remote file
91
90
  """
92
91
  try:
93
- if self._verbose:
94
- print(f"Removing remote file (if it exists): {self._remote_f}")
92
+ self._l.debug("Removing remote file (if it exists): %s", self._remote_f)
95
93
  self._sftp.remove(self._remote_f)
96
94
  except FileNotFoundError:
97
95
  pass
@@ -117,22 +115,23 @@ class Rsync(DataTransfer):
117
115
  Requires rsync be installed
118
116
  """
119
117
 
120
- def __init__(self, config: Config, verbose: bool):
118
+ def __init__(self, config: Config):
121
119
  """
122
120
  :param host: The hostname ssh uses for the remote client
123
- :param verbose: If true, be more verbose
124
121
  """
125
- super().__init__(config, verbose)
126
- self._remote_f = re.sub("[^a-zA-Z\\d_-]", "_", self._remote_f) # Old rsync args suck
122
+ super().__init__(config)
123
+ self._remote_f = re.sub(r"[^a-zA-Z\d_/-]", "_", self._remote_f) # Old rsync args suck
127
124
  self._rsync = find_exe("rsync")
128
125
  # Determine rsync version
129
126
  try:
130
- p = run_cmd(self._rsync, "--version", verbose=self._verbose, stdout=PIPE, check=True)
127
+ p = run_cmd(self._rsync, "--version", stdout=PIPE, check=True)
131
128
  output = p.stdout.decode().split("version")[1].split("protocol")[0].strip()
132
129
  version: tuple[int, ...] = tuple(int(i) for i in output.split("."))
133
130
  self._old = version <= (3, 1, 0) # macOS has an old version by default
134
131
  if self._old:
135
132
  print("\tOld version of rsync detected. Output will be more verbose.")
133
+ else:
134
+ self._l.debug("Newer version of rsync detected")
136
135
  except (CalledProcessError, KeyError) as e:
137
136
  raise RuntimeError(f"Could not determine rsync version from {self._rsync}") from e
138
137
  # Determine rsync command
@@ -143,8 +142,10 @@ class Rsync(DataTransfer):
143
142
  self._flags = [sshpass, "-e"] + self._flags # type: ignore
144
143
  self._env["SSHPASS"] = config.password
145
144
  self._flags.extend(("-hh", "--progress" if self._old else "--info=progress2"))
146
- if self._verbose:
147
- self._flags.extend(["--verbose", "-vvv"])
145
+ if self._l.isEnabledFor(INFO):
146
+ self._flags.append("--verbose")
147
+ if self._l.isEnabledFor(DEBUG):
148
+ self._flags.append("-vvv")
148
149
  if config.port is not None:
149
150
  self._flags.append(f"--port={config.port}")
150
151
  # Determine host info
@@ -156,7 +157,7 @@ class Rsync(DataTransfer):
156
157
  :param dst: The location to rsync src to
157
158
  """
158
159
  try:
159
- _ = run_cmd(self._rsync, *self._flags, src, dst, verbose=self._verbose, env=self._env, check=True)
160
+ _ = run_cmd(self._rsync, *self._flags, src, dst, env=self._env, check=True)
160
161
  except CalledProcessError as e:
161
162
  raise RuntimeError("Failed to transfer data during speed test") from e
162
163
 
@@ -1,19 +1,23 @@
1
- from subprocess import CalledProcessError, PIPE
1
+ from subprocess import CalledProcessError
2
+ from logging import INFO
2
3
  from sys import platform
3
4
 
4
- from .util import find_exe, run_cmd
5
+ from .util import find_exe, tee_cmd
5
6
 
6
7
 
7
8
  __all__ = ("PingFailed", "ping")
8
9
 
9
10
 
11
+ _LOG = "Ping"
12
+
13
+
10
14
  class PingFailed(RuntimeError):
11
15
  """
12
16
  Raise on ping failure
13
17
  """
14
18
 
15
19
 
16
- def ping(host: str, verbose: bool, n_pings: int = 10, max_wait: int = 3) -> float:
20
+ def ping(host: str, n_pings: int = 10, max_wait: int = 3) -> float:
17
21
  """
18
22
  :param host: The host to ping
19
23
  :return: The number Ping host
@@ -22,16 +26,13 @@ def ping(host: str, verbose: bool, n_pings: int = 10, max_wait: int = 3) -> floa
22
26
  print("Pinging...")
23
27
  exe = find_exe("ping")
24
28
  try:
25
- wait: str = ("-W" if platform == "darwin" else "-w") + str(max_wait)
26
- q = [] if verbose else ["-q"]
27
- p = run_cmd(exe, "-i.1", f"-c{n_pings}", wait, *q, host, verbose=verbose, stdout=PIPE)
29
+ wait: str = f"-W{max_wait*1000}" if platform == "darwin" else f"-w{max_wait}"
30
+ p, stdout = tee_cmd(exe, "-i.1", f"-c{n_pings}", wait, host, level=INFO)
28
31
  if p.returncode:
29
32
  raise PingFailed(f"{exe} exit code: {p.returncode}")
30
- output: str = p.stdout.decode().strip()
33
+ output: str = stdout.strip()
31
34
  except CalledProcessError as e:
32
35
  raise PingFailed("Unkown error") from e
33
- if verbose:
34
- print(output)
35
36
  try:
36
37
  return float(output.split("/")[-3])
37
38
  except IndexError as e:
@@ -1,6 +1,7 @@
1
1
  from collections.abc import Generator
2
2
  from contextlib import contextmanager
3
3
  from typing import TYPE_CHECKING
4
+ from logging import getLogger
4
5
  from pathlib import Path
5
6
  import dataclasses
6
7
 
@@ -16,17 +17,22 @@ if TYPE_CHECKING:
16
17
  __all__ = ("sftp_wrapper",)
17
18
 
18
19
 
20
+ _LOG = "SFTPWrapper"
21
+
22
+
19
23
  def create_paramiko_config(raw: Config) -> dict:
20
24
  """
21
25
  Create a dict for paramiko to use as its config
22
26
  :return: A dict containing info needed for paramiko to ssh
23
27
  """
28
+ log = getLogger(_LOG)
24
29
  config: dict[str, str | int | None] = dataclasses.asdict(raw)
25
30
  config["username"] = config.pop("user", None)
26
31
  config["hostname"] = config.pop("host").lower() # type: ignore
27
32
  # Loads ssh config
28
33
  config_f: Path = Path.home() / ".ssh/config"
29
34
  if config_f.exists():
35
+ log.debug("Loading SSH config from: %s", config_f)
30
36
  ssh_config: SSHConfig = SSHConfig.from_path(str(config_f))
31
37
  global_config: SSHConfigDict = ssh_config.lookup("*")
32
38
  host_config: SSHConfigDict = ssh_config.lookup(raw.host)
@@ -36,6 +42,7 @@ def create_paramiko_config(raw: Config) -> dict:
36
42
  value = host_config.get(name, None)
37
43
  value = global_config.get(name, None) if value is None else value
38
44
  if value is not None:
45
+ log.debug(" %s: %s", cname, value)
39
46
  config[cname] = value
40
47
 
41
48
  load("port", "port")
@@ -43,6 +50,7 @@ def create_paramiko_config(raw: Config) -> dict:
43
50
  load("key_filename", "identityfile")
44
51
  # Install fallbacks
45
52
  if config.get("port", None) is None:
53
+ log.debug("Assuming port 22")
46
54
  config["port"] = 22
47
55
  return config
48
56
 
@@ -51,11 +59,14 @@ def load_keys(client: SSHClient, host: str, port: int) -> None:
51
59
  """
52
60
  Load available ssh keys into the ssh client
53
61
  """
62
+ log = getLogger(_LOG)
54
63
  hosts_f = Path.home() / ".ssh/known_hosts"
55
64
  if hosts_f.exists():
65
+ log.debug("Installing known hosts from: %s", hosts_f)
56
66
  client.load_host_keys(str(hosts_f))
57
67
  keys: HostKeys = client.get_host_keys()
58
68
  # Paramiko seems overly strict about ssh keys in a way ssh is not
69
+ log.debug("Relaxing paramiko's knownhosts configuration to align with ssh's")
59
70
  for i in (host, host.lower()):
60
71
  if i in keys:
61
72
  keys[f"[{i}]:{port}"] = keys[i]
@@ -66,9 +77,12 @@ def sftp_wrapper(raw: Config) -> Generator["SFTPClient", None, None]:
66
77
  """
67
78
  A context manager which yields a conncted SFTPClient
68
79
  """
80
+ log = getLogger(_LOG)
81
+ log.debug("Initializing ssh client")
69
82
  with SSHClient() as client:
70
83
  config: dict[str, str | int | None] = create_paramiko_config(raw)
71
84
  load_keys(client, raw.host, config["port"]) # type: ignore
72
85
  client.connect(**config) # type: ignore
86
+ log.debug("Initializing SFTP client")
73
87
  with client.open_sftp() as sftp:
74
88
  yield sftp
@@ -1,7 +1,9 @@
1
+ from logging import getLogger, basicConfig, WARNING, DEBUG, INFO
1
2
  from tempfile import NamedTemporaryFile as NTF
3
+ from dataclasses import fields
2
4
  from pathlib import Path
3
- from math import log
4
5
  import argparse
6
+ import math
5
7
  import time
6
8
  import sys
7
9
  import os
@@ -14,9 +16,10 @@ from ._version import __version__
14
16
 
15
17
  base_size = 2 * (1024**2)
16
18
  _password_env_name = "SPEEDTEST_SSH_PASSWORD"
19
+ _LOG = "speedtest_ssh"
17
20
 
18
21
 
19
- def _iteration(temp: Path, client: DataTransfer, size: int, verbose: bool) -> tuple[int, int]:
22
+ def _iteration(temp: Path, client: DataTransfer, size: int) -> tuple[int, int]:
20
23
  """
21
24
  Time uploading and downloading a file of size bytes
22
25
  :param temp: A temporary file we have ownership of
@@ -25,8 +28,8 @@ def _iteration(temp: Path, client: DataTransfer, size: int, verbose: bool) -> tu
25
28
  :return: <nanoseconds to upload>, <nanoseconds to download>
26
29
  """
27
30
  # Create the file to upload; sendfile not always supported so we do this the hard way
28
- if verbose:
29
- print(f"Filling {temp} with {size} from /dev/urandom... ", end="", flush=True)
31
+ log = getLogger(_LOG)
32
+ log.debug("Filling %s with %s MiB from /dev/urandom... ", temp, size / 2**20)
30
33
  block: int = min(base_size, 1024**2)
31
34
  with temp.open("ab") as f:
32
35
  done: int = os.fstat(f.fileno()).st_size # .tell() can be odd in append mode
@@ -35,18 +38,19 @@ def _iteration(temp: Path, client: DataTransfer, size: int, verbose: bool) -> tu
35
38
  f.write(ur.read(block))
36
39
  done += block
37
40
  f.truncate(size)
38
- if verbose:
39
- print("Done", flush=True)
40
41
  # Time uploading the file
42
+ log.debug("Starting upload test")
41
43
  client.clean_remote()
42
44
  start: int = time.time_ns()
43
45
  client.put(temp)
44
46
  up: int = time.time_ns() - start
45
47
  # Time downloading the file
48
+ log.debug("Time taken: %s ns\nStarting download test", up)
46
49
  temp.unlink()
47
50
  start = time.time_ns()
48
51
  client.get(temp)
49
52
  down: int = time.time_ns() - start
53
+ log.debug("Time taken: %s ns\n", down)
50
54
  return up, down
51
55
 
52
56
 
@@ -59,40 +63,29 @@ def _print_results(ping_delay: float | None, up_ns: int, down_ns: int, size: int
59
63
  print(f"{prefix}Upload Speed: {fmt(up_ns)}\nDownload Speed: {fmt(down_ns)}")
60
64
 
61
65
 
62
- def speedtest_ssh(
63
- host: str, num_seconds: int, ping: bool, verbose: bool, mode: str, **kwargs: int | str | None
64
- ) -> None:
66
+ def speedtest_ssh(num_seconds: int, ping: bool, mode: str, conf: Config) -> None:
65
67
  """
66
68
  Run speedtest_ssh
67
69
  """
68
- kwargs["password"] = None
69
- if kwargs.pop("password_env"):
70
- kwargs["password"] = os.environ.get(_password_env_name, None)
71
- if kwargs["password"] is None:
72
- raise RuntimeError(f"{_password_env_name} is not set")
73
- ping_delay: float | None = ping_test(host, verbose) if ping else None
70
+ log = getLogger(_LOG)
71
+ ping_delay: float | None = ping_test(conf.host) if ping else None
74
72
  print("Connecting...")
75
- with (Rsync if mode == "rsync" else SFTP)(
76
- Config(host=host, **kwargs), # type: ignore
77
- verbose=verbose,
78
- ) as remote:
73
+ with (Rsync if mode == "rsync" else SFTP)(conf) as remote:
79
74
  with NTF(prefix="speedtest_ssh.", dir="/tmp", delete_on_close=False) as ntf:
80
75
  ntf.close()
81
76
  temp = Path(ntf.name)
82
- if verbose:
83
- print(f"Created temporary file: {temp}")
77
+ log.debug("Created temporary file: %s", temp)
84
78
  print("Testing...")
85
79
  nano_sec: int = 0
86
80
  size: int = base_size
87
81
  remaining: int = int(1e9) * num_seconds
88
82
  while int(1.5 * nano_sec) < remaining - 1: # Try to get close to seconds
89
- size *= 2 ** (0 if not nano_sec else max(1, int(log(remaining / nano_sec, 2))))
83
+ size *= 2 ** (0 if not nano_sec else max(1, int(math.log(remaining / nano_sec, 2))))
90
84
  # ^ Faster than just *=2
91
- up_t, down_t = _iteration(temp, remote, size, verbose)
85
+ up_t, down_t = _iteration(temp, remote, size)
92
86
  nano_sec = up_t + down_t
93
87
  remaining -= nano_sec
94
- if verbose:
95
- print(f"\n{'*'*30} Final Results {'*'*30}")
88
+ log.debug(f"\n{'*'*30} Final Results {'*'*30}")
96
89
  _print_results(ping_delay, up_t, down_t, size)
97
90
 
98
91
 
@@ -106,6 +99,7 @@ def main(argv: list[str]) -> None:
106
99
  parser.add_argument("host", help="The host to speedtest the conection to")
107
100
  parser.add_argument("--ping", action="store_true", help="Ping test the host as well")
108
101
  parser.add_argument("--verbose", action="store_true", help="Be verbose")
102
+ parser.add_argument("--very-verbose", action="store_true", help="Be very verbose")
109
103
  parser.add_argument("-u", "--user", help="The user used for ssh")
110
104
  parser.add_argument(
111
105
  "--password-env", action="store_true", help=f"Read password from {_password_env_name} environment variable"
@@ -117,7 +111,21 @@ def main(argv: list[str]) -> None:
117
111
  parser.add_argument(
118
112
  "-m", "--mode", choices=["rsync", "sftp"], default="rsync", help="The speedtest method. Defaults to rsync"
119
113
  )
120
- return speedtest_ssh(**vars(parser.parse_args(argv[1:])))
114
+ ns = vars(parser.parse_args(argv[1:]))
115
+ basicConfig(level=WARNING, format="%(message)s")
116
+ log = getLogger(_LOG)
117
+ if ns.pop("verbose"):
118
+ getLogger().setLevel(INFO)
119
+ if ns.pop("very_verbose"):
120
+ getLogger().setLevel(DEBUG)
121
+ log.debug("Very verbose logging enabled")
122
+ ns["password"] = None
123
+ if ns.pop("password_env"):
124
+ ns["password"] = os.environ.get(_password_env_name, None)
125
+ if ns["password"] is None:
126
+ raise RuntimeError(f"{_password_env_name} is not set")
127
+ conf = Config(**{i.name: ns.pop(i.name) for i in fields(Config)}) # type: ignore
128
+ return speedtest_ssh(**ns, conf=conf) # type: ignore
121
129
 
122
130
 
123
131
  def cli() -> None:
@@ -0,0 +1,66 @@
1
+ from logging import getLogger, DEBUG
2
+ from pathlib import Path
3
+ from shutil import which
4
+ import subprocess
5
+ import shlex
6
+ import os
7
+
8
+
9
+ __all__ = ("find_exe", "run_cmd")
10
+
11
+
12
+ _LOG = "Util"
13
+
14
+
15
+ def find_exe(name: str) -> Path:
16
+ """
17
+ :param name: The name of the executable to find
18
+ :return: The path to the executable
19
+ """
20
+ where: str | None = which(name)
21
+ if where is None:
22
+ raise RuntimeError(f"Cannot find {name} executable")
23
+ exe: Path = Path(where).resolve()
24
+ if not os.access(exe, os.X_OK):
25
+ raise RuntimeError(f"Cannot find valid {name} executable")
26
+ return exe
27
+
28
+
29
+ def _log_cmd(cmd: Path, args: tuple[str | Path, ...]) -> None:
30
+ log = getLogger(_LOG)
31
+ if log.isEnabledFor(DEBUG):
32
+ full = (cmd, *args)
33
+ msg = f"{'*'*30} Running Command: {cmd.name} {'*'*30}"
34
+ msg = f"{msg}\n{' '.join(shlex.quote(str(i)) for i in full)}\n{'*'*len(msg)}"
35
+ log.debug(msg)
36
+
37
+
38
+ def run_cmd(cmd: Path, *args: str | Path, **kwargs) -> subprocess.CompletedProcess:
39
+ """
40
+ :param cmd: The command to run
41
+ :param args: The arguments of cmd
42
+ :param kwargs: Fowarded to subprocess.Popen
43
+ :return: Completed process
44
+ """
45
+ _log_cmd(cmd, args)
46
+ return subprocess.run((cmd, *args), **kwargs)
47
+
48
+
49
+ def tee_cmd(cmd: Path, *args: str | Path, level: int, **kwargs) -> tuple[subprocess.Popen, str]:
50
+ """
51
+ :param cmd: The command to run (and info log the output of)
52
+ :param args: The arguments of cmd
53
+ :param level: The log level to use
54
+ :param kwargs: Fowarded to subprocess.Popen aside from test and stdout
55
+ :return: Completed process, stdout (do not read stdout from CompletedProcess)
56
+ """
57
+ assert "text" not in kwargs
58
+ assert "stdout" not in kwargs
59
+ log = getLogger(_LOG)
60
+ _log_cmd(cmd, args)
61
+ p = subprocess.Popen((cmd, *args), text=True, stdout=subprocess.PIPE, **kwargs)
62
+ stdout = ""
63
+ for line in p.stdout: # type: ignore
64
+ log.log(level, line[:-1])
65
+ stdout += line
66
+ return p, stdout
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: speedtest_ssh
3
- Version: 2.6.2
3
+ Version: 2.7.1
4
4
  Summary: A tool meant to check inter-device speeds via ssh
5
5
  License: GPL
6
6
  Project-URL: Homepage, https://github.com/zwimer/speedtest_ssh
@@ -1 +0,0 @@
1
- __version__: str = "2.6.2"
@@ -1,30 +0,0 @@
1
- from pathlib import Path
2
- from shutil import which
3
- import subprocess
4
- import shlex
5
- import os
6
-
7
-
8
- __all__ = ("find_exe", "run_cmd")
9
-
10
-
11
- def find_exe(name: str) -> Path:
12
- """
13
- :param name: The name of the executable to find
14
- :return: The path to the executable
15
- """
16
- where: str | None = which(name)
17
- if where is None:
18
- raise RuntimeError(f"Cannot find {name} executable")
19
- exe: Path = Path(where).resolve()
20
- if not os.access(exe, os.X_OK):
21
- raise RuntimeError(f"Cannot find valid {name} executable")
22
- return exe
23
-
24
-
25
- def run_cmd(cmd: Path, *args: str | Path, verbose: bool, **kwargs) -> subprocess.CompletedProcess:
26
- full = (cmd, *args)
27
- if verbose:
28
- msg = f"{'*'*30} Running Command: {cmd.name} {'*'*30}"
29
- print(f"{msg}\n{' '.join(shlex.quote(str(i)) for i in full)}\n{'*'*len(msg)}")
30
- return subprocess.run(full, **kwargs)
File without changes
File without changes
File without changes