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.
- {speedtest_ssh-2.6.2/speedtest_ssh.egg-info → speedtest_ssh-2.7.1}/PKG-INFO +1 -1
- speedtest_ssh-2.7.1/speedtest_ssh/_version.py +1 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/data_transfer.py +20 -19
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/ping.py +10 -9
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/sftp_wrapper.py +14 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/speedtest_ssh.py +34 -26
- speedtest_ssh-2.7.1/speedtest_ssh/util.py +66 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1/speedtest_ssh.egg-info}/PKG-INFO +1 -1
- speedtest_ssh-2.6.2/speedtest_ssh/_version.py +0 -1
- speedtest_ssh-2.6.2/speedtest_ssh/util.py +0 -30
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/LICENSE +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/README.md +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/pyproject.toml +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/setup.cfg +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/__init__.py +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/config.py +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh/py.typed +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/SOURCES.txt +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/dependency_links.txt +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/entry_points.txt +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/requires.txt +0 -0
- {speedtest_ssh-2.6.2 → speedtest_ssh-2.7.1}/speedtest_ssh.egg-info/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
126
|
-
self._remote_f = re.sub("[^a-zA-Z
|
|
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",
|
|
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.
|
|
147
|
-
self._flags.
|
|
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,
|
|
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
|
|
1
|
+
from subprocess import CalledProcessError
|
|
2
|
+
from logging import INFO
|
|
2
3
|
from sys import platform
|
|
3
4
|
|
|
4
|
-
from .util import find_exe,
|
|
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,
|
|
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 =
|
|
26
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
85
|
+
up_t, down_t = _iteration(temp, remote, size)
|
|
92
86
|
nano_sec = up_t + down_t
|
|
93
87
|
remaining -= nano_sec
|
|
94
|
-
|
|
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
|
-
|
|
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 +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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|