speedtest-ssh 2.10.2__tar.gz → 2.12.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.
- {speedtest_ssh-2.10.2/speedtest_ssh.egg-info → speedtest_ssh-2.12.0}/PKG-INFO +4 -4
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/pyproject.toml +11 -9
- speedtest_ssh-2.12.0/speedtest_ssh/_version.py +1 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh/config.py +1 -3
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh/data_transfer.py +17 -26
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh/ping.py +6 -5
- speedtest_ssh-2.12.0/speedtest_ssh/py.typed +1 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh/sftp_wrapper.py +7 -10
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh/speedtest_ssh.py +13 -16
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh/util.py +20 -11
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0/speedtest_ssh.egg-info}/PKG-INFO +4 -4
- speedtest_ssh-2.10.2/speedtest_ssh/_version.py +0 -1
- speedtest_ssh-2.10.2/speedtest_ssh/py.typed +0 -1
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/LICENSE +0 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/README.md +0 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/setup.cfg +0 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh/__init__.py +0 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh.egg-info/SOURCES.txt +0 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh.egg-info/dependency_links.txt +0 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh.egg-info/entry_points.txt +0 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh.egg-info/requires.txt +0 -0
- {speedtest_ssh-2.10.2 → speedtest_ssh-2.12.0}/speedtest_ssh.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: speedtest_ssh
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.0
|
|
4
4
|
Summary: A tool meant to check inter-device speeds via ssh
|
|
5
|
-
License: GPL
|
|
5
|
+
License-Expression: GPL-3.0-only
|
|
6
6
|
Project-URL: Homepage, https://github.com/zwimer/speedtest_ssh
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.
|
|
9
|
-
Requires-Python: >=3.
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Requires-Python: >=3.14
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: argcomplete
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "speedtest_ssh"
|
|
7
7
|
classifiers = [
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
"Programming Language :: Python :: 3",
|
|
9
|
+
"Programming Language :: Python :: 3.14",
|
|
10
10
|
]
|
|
11
|
-
license =
|
|
11
|
+
license = "GPL-3.0-only"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
12
13
|
description = "A tool meant to check inter-device speeds via ssh"
|
|
13
14
|
urls = {Homepage = "https://github.com/zwimer/speedtest_ssh"}
|
|
14
|
-
requires-python = ">= 3.
|
|
15
|
+
requires-python = ">= 3.14"
|
|
15
16
|
dynamic = ["version"]
|
|
16
17
|
dependencies = [
|
|
17
18
|
"argcomplete",
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
"paramiko",
|
|
20
|
+
"tqdm",
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
[project.readme]
|
|
@@ -27,7 +28,6 @@ content-type = "text/markdown"
|
|
|
27
28
|
speedtest-ssh = "speedtest_ssh:cli"
|
|
28
29
|
|
|
29
30
|
[tool.setuptools]
|
|
30
|
-
license-files = ["LICENSE"]
|
|
31
31
|
include-package-data = false
|
|
32
32
|
|
|
33
33
|
[tool.setuptools.packages]
|
|
@@ -49,10 +49,12 @@ disable = [
|
|
|
49
49
|
"line-too-long",
|
|
50
50
|
"invalid-name",
|
|
51
51
|
]
|
|
52
|
+
[tool.pylint.main]
|
|
53
|
+
jobs = 0
|
|
52
54
|
|
|
53
55
|
[tool.black]
|
|
54
56
|
line-length = 120
|
|
55
|
-
target-version = ["
|
|
57
|
+
target-version = ["py314", "py315"]
|
|
56
58
|
|
|
57
59
|
[tool.ruff]
|
|
58
60
|
line-length = 120
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__: str = "2.12.0"
|
|
@@ -3,9 +3,7 @@ from dataclasses import dataclass
|
|
|
3
3
|
|
|
4
4
|
@dataclass(kw_only=True)
|
|
5
5
|
class Config:
|
|
6
|
-
"""
|
|
7
|
-
A simple config dataclass that holds information used to access a remote client
|
|
8
|
-
"""
|
|
6
|
+
"""A simple config dataclass that holds information used to access a remote client"""
|
|
9
7
|
|
|
10
8
|
host: str
|
|
11
9
|
user: str | None
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
from subprocess import CalledProcessError, PIPE
|
|
3
2
|
from logging import getLogger, DEBUG, INFO
|
|
4
3
|
from typing import TYPE_CHECKING, Self
|
|
@@ -30,17 +29,16 @@ def _rand_str(size: int) -> str:
|
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
class ProgressBar(tqdm):
|
|
33
|
-
"""
|
|
34
|
-
A small tqdm wrapper for paramiko's SFTP callbacks to use
|
|
35
|
-
"""
|
|
32
|
+
"""A small tqdm wrapper for paramiko's SFTP callbacks to use"""
|
|
36
33
|
|
|
37
34
|
def __init__(self, desc: str):
|
|
38
35
|
super().__init__(desc=desc, unit="B", unit_scale=True, unit_divisor=1024, dynamic_ncols=True)
|
|
39
36
|
|
|
40
37
|
def __call__(self, bytes_done: int, bytes_remaining: int):
|
|
41
38
|
"""
|
|
42
|
-
:
|
|
43
|
-
|
|
39
|
+
Args:
|
|
40
|
+
bytes_done: The number of bytes transferred
|
|
41
|
+
bytes_remaining: The number of bytes yet to be transferred
|
|
44
42
|
"""
|
|
45
43
|
self.total: int | None
|
|
46
44
|
if self.total is None:
|
|
@@ -49,8 +47,7 @@ class ProgressBar(tqdm):
|
|
|
49
47
|
|
|
50
48
|
|
|
51
49
|
class DataTransfer:
|
|
52
|
-
"""
|
|
53
|
-
A context manager used to send and receive data to and from the remote client
|
|
50
|
+
"""A context manager used to send and receive data to and from the remote client
|
|
54
51
|
This class is a context manager; on exit will remove the remote file on deletion
|
|
55
52
|
"""
|
|
56
53
|
|
|
@@ -58,7 +55,8 @@ class DataTransfer:
|
|
|
58
55
|
|
|
59
56
|
def __init__(self, config: Config):
|
|
60
57
|
"""
|
|
61
|
-
:
|
|
58
|
+
Args:
|
|
59
|
+
config: The Config object the DataTransfer instance should use
|
|
62
60
|
"""
|
|
63
61
|
self._l = getLogger(self._LOG)
|
|
64
62
|
self._remote_f: str = f"/tmp/speedtest-ssh_{datetime.now(tz=UTC)}_{_rand_str(8)}.tmp"
|
|
@@ -79,21 +77,15 @@ class DataTransfer:
|
|
|
79
77
|
self._sftp_cm.__exit__(*args, **kwargs)
|
|
80
78
|
|
|
81
79
|
def put(self, local: Path) -> None:
|
|
82
|
-
"""
|
|
83
|
-
Send the local file to be saved at the remote path
|
|
84
|
-
"""
|
|
80
|
+
"""Send the local file to be saved at the remote path"""
|
|
85
81
|
raise NotImplementedError
|
|
86
82
|
|
|
87
83
|
def get(self, local: Path) -> None:
|
|
88
|
-
"""
|
|
89
|
-
Get the remote file to and save it at the local path
|
|
90
|
-
"""
|
|
84
|
+
"""Get the remote file to and save it at the local path"""
|
|
91
85
|
raise NotImplementedError
|
|
92
86
|
|
|
93
87
|
def clean_remote(self) -> None:
|
|
94
|
-
"""
|
|
95
|
-
Removes the remote file
|
|
96
|
-
"""
|
|
88
|
+
"""Removes the remote file"""
|
|
97
89
|
try:
|
|
98
90
|
self._l.debug("Removing remote file (if it exists): %s", self._remote_f)
|
|
99
91
|
self._sftp.remove(self._remote_f)
|
|
@@ -102,9 +94,7 @@ class DataTransfer:
|
|
|
102
94
|
|
|
103
95
|
|
|
104
96
|
class SFTP(DataTransfer):
|
|
105
|
-
"""
|
|
106
|
-
A DataTransfer classes that uses SFTP
|
|
107
|
-
"""
|
|
97
|
+
"""A DataTransfer classes that uses SFTP"""
|
|
108
98
|
|
|
109
99
|
def put(self, local: Path) -> None:
|
|
110
100
|
with ProgressBar("Upload ") as p:
|
|
@@ -116,14 +106,14 @@ class SFTP(DataTransfer):
|
|
|
116
106
|
|
|
117
107
|
|
|
118
108
|
class Rsync(DataTransfer):
|
|
119
|
-
"""
|
|
120
|
-
A DataTransfer that uses rsync
|
|
109
|
+
"""A DataTransfer that uses rsync
|
|
121
110
|
Requires rsync be installed
|
|
122
111
|
"""
|
|
123
112
|
|
|
124
113
|
def __init__(self, config: Config):
|
|
125
114
|
"""
|
|
126
|
-
:
|
|
115
|
+
Args:
|
|
116
|
+
host: The hostname ssh uses for the remote client
|
|
127
117
|
"""
|
|
128
118
|
super().__init__(config)
|
|
129
119
|
self._remote_f = re.sub(r"[^a-zA-Z\d_/-]", "_", self._remote_f) # Old rsync args suck
|
|
@@ -159,8 +149,9 @@ class Rsync(DataTransfer):
|
|
|
159
149
|
|
|
160
150
|
def _transfer(self, src: str | Path, dst: str | Path) -> None:
|
|
161
151
|
"""
|
|
162
|
-
:
|
|
163
|
-
|
|
152
|
+
Args:
|
|
153
|
+
src: The file to rsync to dst
|
|
154
|
+
dst: The location to rsync src to
|
|
164
155
|
"""
|
|
165
156
|
try:
|
|
166
157
|
_ = run_cmd(self._rsync, *self._flags, src, dst, env=self._env, check=True)
|
|
@@ -11,15 +11,16 @@ _LOG = "Ping"
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class PingFailed(RuntimeError):
|
|
14
|
-
"""
|
|
15
|
-
Raise on ping failure
|
|
16
|
-
"""
|
|
14
|
+
"""Raise on ping failure"""
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
def ping(host: str, n_pings: int = 10, max_wait: int = 3) -> float:
|
|
20
18
|
"""
|
|
21
|
-
:
|
|
22
|
-
|
|
19
|
+
Args:
|
|
20
|
+
host: The host to ping
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The number Ping host
|
|
23
24
|
"""
|
|
24
25
|
# We use subprocess b/c ping is suid and we don't want to require root
|
|
25
26
|
print("Pinging...")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -21,9 +21,10 @@ _LOG = "SFTPWrapper"
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def create_paramiko_config(raw: Config) -> dict:
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
:
|
|
24
|
+
"""Create a dict for paramiko to use as its config
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A dict containing info needed for paramiko to ssh
|
|
27
28
|
"""
|
|
28
29
|
log = getLogger(_LOG)
|
|
29
30
|
config: dict[str, str | int | None] = dataclasses.asdict(raw)
|
|
@@ -56,9 +57,7 @@ def create_paramiko_config(raw: Config) -> dict:
|
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
def load_keys(client: SSHClient, host: str, port: int) -> None:
|
|
59
|
-
"""
|
|
60
|
-
Load available ssh keys into the ssh client
|
|
61
|
-
"""
|
|
60
|
+
"""Load available ssh keys into the ssh client"""
|
|
62
61
|
log = getLogger(_LOG)
|
|
63
62
|
hosts_f = Path.home() / ".ssh/known_hosts"
|
|
64
63
|
if hosts_f.exists():
|
|
@@ -73,10 +72,8 @@ def load_keys(client: SSHClient, host: str, port: int) -> None:
|
|
|
73
72
|
|
|
74
73
|
|
|
75
74
|
@contextmanager
|
|
76
|
-
def sftp_wrapper(raw: Config) -> Generator[
|
|
77
|
-
"""
|
|
78
|
-
A context manager which yields a conncted SFTPClient
|
|
79
|
-
"""
|
|
75
|
+
def sftp_wrapper(raw: Config) -> Generator[SFTPClient]:
|
|
76
|
+
"""A context manager which yields a conncted SFTPClient"""
|
|
80
77
|
log = getLogger(_LOG)
|
|
81
78
|
log.debug("Initializing ssh client")
|
|
82
79
|
with SSHClient() as client:
|
|
@@ -21,12 +21,15 @@ _LOG = "speedtest_ssh"
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _iteration(temp: Path, client: DataTransfer, size: int) -> tuple[int, int]:
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
"""Time uploading and downloading a file of size bytes
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
temp: A temporary file we have ownership of
|
|
28
|
+
client: A client used to transfer the data
|
|
29
|
+
size: how much data to transfer
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
<nanoseconds to upload>, <nanoseconds to download>
|
|
30
33
|
"""
|
|
31
34
|
# Create the file to upload; sendfile not always supported so we do this the hard way
|
|
32
35
|
log = getLogger(_LOG)
|
|
@@ -56,18 +59,14 @@ def _iteration(temp: Path, client: DataTransfer, size: int) -> tuple[int, int]:
|
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
def _print_results(ping_delay: float | None, up_ns: int, down_ns: int, size: int) -> None:
|
|
59
|
-
"""
|
|
60
|
-
Print the speed test results
|
|
61
|
-
"""
|
|
62
|
+
"""Print the speed test results"""
|
|
62
63
|
prefix = "\n" + ("" if ping_delay is None else f"Ping: {ping_delay} ms\n")
|
|
63
64
|
fmt = lambda s: f"{8 * (size/1024**2) / (s/1000**3):.2f} Mbit/s"
|
|
64
65
|
print(f"{prefix}Upload Speed: {fmt(up_ns)}\nDownload Speed: {fmt(down_ns)}")
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
def speedtest_ssh(seconds: int, ping: bool, mode: str, conf: Config) -> None:
|
|
68
|
-
"""
|
|
69
|
-
Run speedtest_ssh
|
|
70
|
-
"""
|
|
69
|
+
"""Run speedtest_ssh"""
|
|
71
70
|
log = getLogger(_LOG)
|
|
72
71
|
ping_delay: float | None = ping_test(conf.host) if ping else None
|
|
73
72
|
print("Connecting...")
|
|
@@ -93,9 +92,7 @@ def speedtest_ssh(seconds: int, ping: bool, mode: str, conf: Config) -> None:
|
|
|
93
92
|
|
|
94
93
|
|
|
95
94
|
def cli() -> None:
|
|
96
|
-
"""
|
|
97
|
-
speedtest_ssh main
|
|
98
|
-
"""
|
|
95
|
+
"""speedtest_ssh main"""
|
|
99
96
|
parser = argparse.ArgumentParser()
|
|
100
97
|
parser.add_argument("-V", "--version", action="version", version=f"{parser.prog} {__version__}")
|
|
101
98
|
remote = parser.add_argument_group("SSH Options")
|
|
@@ -128,7 +125,7 @@ def cli() -> None:
|
|
|
128
125
|
# Configure logging
|
|
129
126
|
lvl = ns.pop("verbose")
|
|
130
127
|
lvl = _LOG_VERBOSITY[max(i for i in _LOG_VERBOSITY if i <= lvl)]
|
|
131
|
-
basicConfig(level=lvl, format="%(levelname)-
|
|
128
|
+
basicConfig(level=lvl, format="%(levelname)-7s - %(message)s")
|
|
132
129
|
getLogger("main").debug("Log level set to %d", lvl)
|
|
133
130
|
# Call speedtest
|
|
134
131
|
ns["password"] = None
|
|
@@ -13,8 +13,11 @@ _LOG = "Util"
|
|
|
13
13
|
|
|
14
14
|
def find_exe(name: str) -> Path:
|
|
15
15
|
"""
|
|
16
|
-
:
|
|
17
|
-
|
|
16
|
+
Args:
|
|
17
|
+
name: The name of the executable to find
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
The path to the executable
|
|
18
21
|
"""
|
|
19
22
|
where: str | None = which(name)
|
|
20
23
|
if where is None:
|
|
@@ -36,10 +39,13 @@ def _log_cmd(cmd: Path, args: tuple[str | Path, ...]) -> None:
|
|
|
36
39
|
|
|
37
40
|
def run_cmd(cmd: Path, *args: str | Path, **kwargs) -> subprocess.CompletedProcess:
|
|
38
41
|
"""
|
|
39
|
-
:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
Args:
|
|
43
|
+
cmd: The command to run
|
|
44
|
+
*args: The arguments of cmd
|
|
45
|
+
**kwargs: Fowarded to subprocess.Popen
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Completed process
|
|
43
49
|
"""
|
|
44
50
|
_log_cmd(cmd, args)
|
|
45
51
|
return subprocess.run((cmd, *args), **kwargs) # nosec B603 # noqa: PLW1510
|
|
@@ -47,11 +53,14 @@ def run_cmd(cmd: Path, *args: str | Path, **kwargs) -> subprocess.CompletedProce
|
|
|
47
53
|
|
|
48
54
|
def tee_cmd(cmd: Path, *args: str | Path, level: int, **kwargs) -> tuple[subprocess.Popen, str]:
|
|
49
55
|
"""
|
|
50
|
-
:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
Args:
|
|
57
|
+
cmd: The command to run (and info log the output of)
|
|
58
|
+
*args: The arguments of cmd
|
|
59
|
+
level: The log level to use
|
|
60
|
+
**kwargs: Fowarded to subprocess.Popen aside from test and stdout
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Completed process, stdout (do not read stdout from CompletedProcess)
|
|
55
64
|
"""
|
|
56
65
|
assert "text" not in kwargs # nosec B101
|
|
57
66
|
assert "stdout" not in kwargs # nosec B101
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: speedtest_ssh
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.0
|
|
4
4
|
Summary: A tool meant to check inter-device speeds via ssh
|
|
5
|
-
License: GPL
|
|
5
|
+
License-Expression: GPL-3.0-only
|
|
6
6
|
Project-URL: Homepage, https://github.com/zwimer/speedtest_ssh
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.
|
|
9
|
-
Requires-Python: >=3.
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Requires-Python: >=3.14
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: argcomplete
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__: str = "2.10.2"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
PARTIAL
|
|
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
|