speedtest-cli-ng 0.1.1__tar.gz → 0.2.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_cli_ng-0.1.1/speedtest_cli_ng.egg-info → speedtest_cli_ng-0.2.0}/PKG-INFO +9 -16
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/README.md +8 -15
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/extras/man/speedtest-cli.1 +2 -27
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/pyproject.toml +1 -1
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest/__main__.py +1 -1
- speedtest_cli_ng-0.2.0/speedtest/cli/commands.py +26 -0
- speedtest_cli_ng-0.2.0/speedtest/cli/main.py +103 -0
- speedtest_cli_ng-0.2.0/speedtest/cli/output.py +62 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest/cli/parser.py +6 -38
- speedtest_cli_ng-0.2.0/speedtest/client.py +117 -0
- speedtest_cli_ng-0.2.0/speedtest/engine/config.py +50 -0
- speedtest_cli_ng-0.2.0/speedtest/engine/network.py +170 -0
- speedtest_cli_ng-0.2.0/speedtest/engine/servers.py +77 -0
- speedtest_cli_ng-0.2.0/speedtest/engine/transfer.py +139 -0
- speedtest_cli_ng-0.2.0/speedtest/exceptions.py +35 -0
- speedtest_cli_ng-0.2.0/speedtest/models/__init__.py +12 -0
- speedtest_cli_ng-0.2.0/speedtest/models/config.py +27 -0
- speedtest_cli_ng-0.2.0/speedtest/models/context.py +54 -0
- speedtest_cli_ng-0.2.0/speedtest/models/result.py +58 -0
- speedtest_cli_ng-0.2.0/speedtest/models/server.py +62 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest/utils/logger.py +17 -8
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0/speedtest_cli_ng.egg-info}/PKG-INFO +9 -16
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest_cli_ng.egg-info/SOURCES.txt +6 -8
- speedtest_cli_ng-0.1.1/speedtest/cli/commands.py +0 -120
- speedtest_cli_ng-0.1.1/speedtest/cli/main.py +0 -108
- speedtest_cli_ng-0.1.1/speedtest/cli/output.py +0 -46
- speedtest_cli_ng-0.1.1/speedtest/client.py +0 -144
- speedtest_cli_ng-0.1.1/speedtest/engine/config.py +0 -120
- speedtest_cli_ng-0.1.1/speedtest/engine/results.py +0 -195
- speedtest_cli_ng-0.1.1/speedtest/engine/servers.py +0 -189
- speedtest_cli_ng-0.1.1/speedtest/engine/transfer.py +0 -157
- speedtest_cli_ng-0.1.1/speedtest/exceptions.py +0 -78
- speedtest_cli_ng-0.1.1/speedtest/http/connections.py +0 -36
- speedtest_cli_ng-0.1.1/speedtest/http/errors.py +0 -21
- speedtest_cli_ng-0.1.1/speedtest/http/handlers.py +0 -105
- speedtest_cli_ng-0.1.1/speedtest/http/request.py +0 -80
- speedtest_cli_ng-0.1.1/speedtest/http/response.py +0 -23
- speedtest_cli_ng-0.1.1/speedtest/http/workers.py +0 -128
- speedtest_cli_ng-0.1.1/speedtest/utils/__init__.py +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/LICENSE +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/NOTICE +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/setup.cfg +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest/__init__.py +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest/cli/__init__.py +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest/engine/__init__.py +0 -0
- {speedtest_cli_ng-0.1.1/speedtest/http → speedtest_cli_ng-0.2.0/speedtest/utils}/__init__.py +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest/utils/status.py +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest_cli_ng.egg-info/dependency_links.txt +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest_cli_ng.egg-info/entry_points.txt +0 -0
- {speedtest_cli_ng-0.1.1 → speedtest_cli_ng-0.2.0}/speedtest_cli_ng.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: speedtest-cli-ng
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Next generation CLI for testing internet bandwidth using speedtest.net
|
|
5
5
|
Author-email: Michał Korczak <git@omikor.in>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,8 +29,8 @@ Dynamic: license-file
|
|
|
29
29
|
[](https://github.com/Omikorin/speedtest-cli)
|
|
30
30
|
[](https://github.com/Omikorin/speedtest-cli)
|
|
31
31
|
[](https://github.com/Omikorin/speedtest-cli)
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
[](https://github.com/Omikorin/speedtest-cli/actions/workflows/ci.yml)
|
|
33
|
+
[](https://sonarcloud.io/summary/new_code?id=Omikorin_speedtest-cli)
|
|
34
34
|
|
|
35
35
|
Next generation CLI for testing internet bandwidth using speedtest.net
|
|
36
36
|
|
|
@@ -75,15 +75,14 @@ speedtest-cli --help
|
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
```text
|
|
78
|
-
usage: speedtest-cli [-h] [-l] [-s SERVER] [--no-download] [--no-upload]
|
|
79
|
-
[--
|
|
80
|
-
[--
|
|
78
|
+
usage: speedtest-cli [-h] [-l] [-s SERVER] [--no-download] [--no-upload]
|
|
79
|
+
[-t THREADS | --single] [--share] [--bytes] [--json]
|
|
80
|
+
[--debug] [--version]
|
|
81
81
|
|
|
82
82
|
Next generation CLI for testing internet bandwidth using speedtest.net.
|
|
83
83
|
|
|
84
84
|
options:
|
|
85
85
|
-h, --help show this help message and exit
|
|
86
|
-
--version show program's version number and exit
|
|
87
86
|
|
|
88
87
|
Core Options:
|
|
89
88
|
-l, --list Show available speedtest.net servers sorted by distance. (default: False)
|
|
@@ -99,17 +98,12 @@ Transfer Modifiers:
|
|
|
99
98
|
|
|
100
99
|
Output Options:
|
|
101
100
|
--share Generate and provide a URL to the speedtest.net share results image. (default: False)
|
|
102
|
-
--bytes Display values in bytes instead of bits. Does not affect image generation or JSON
|
|
103
|
-
--csv Suppress verbose output, only show basic information in CSV format. Speeds listed in bit/s. (default: False)
|
|
101
|
+
--bytes Display values in bytes instead of bits. Does not affect image generation or JSON output. (default: ('bit', 1))
|
|
104
102
|
--json Suppress verbose output, only show basic information in JSON format. Speeds listed in bit/s. (default: False)
|
|
105
|
-
--csv-delimiter CSV_DELIMITER
|
|
106
|
-
Single character delimiter to use in CSV output. (default: ,)
|
|
107
|
-
--csv-header Print CSV headers and exit. (default: False)
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
--source SOURCE Bind a source IP address to use for connections. (default: None)
|
|
111
|
-
--timeout TIMEOUT HTTP timeout in seconds. (default: 10.0)
|
|
104
|
+
Miscellaneous Options:
|
|
112
105
|
--debug Show verbose debugging output. (default: False)
|
|
106
|
+
--version show program's version number and exit
|
|
113
107
|
```
|
|
114
108
|
|
|
115
109
|
## Development
|
|
@@ -153,7 +147,6 @@ uv run build
|
|
|
153
147
|
|
|
154
148
|
The output will be in the `dist/` directory.
|
|
155
149
|
|
|
156
|
-
|
|
157
150
|
## Acknowledgments
|
|
158
151
|
|
|
159
152
|
This project is a heavily refactored and modernized fork of the original [speedtest-cli](https://github.com/sivel/speedtest-cli) created by Matt Martz. While the codebase has been completely rewritten to support modern Python standards, concurrent thread pools, and memory-safe streaming, it was built upon the foundational concepts of the original tool.
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
[](https://github.com/Omikorin/speedtest-cli)
|
|
4
4
|
[](https://github.com/Omikorin/speedtest-cli)
|
|
5
5
|
[](https://github.com/Omikorin/speedtest-cli)
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
[](https://github.com/Omikorin/speedtest-cli/actions/workflows/ci.yml)
|
|
7
|
+
[](https://sonarcloud.io/summary/new_code?id=Omikorin_speedtest-cli)
|
|
8
8
|
|
|
9
9
|
Next generation CLI for testing internet bandwidth using speedtest.net
|
|
10
10
|
|
|
@@ -49,15 +49,14 @@ speedtest-cli --help
|
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
```text
|
|
52
|
-
usage: speedtest-cli [-h] [-l] [-s SERVER] [--no-download] [--no-upload]
|
|
53
|
-
[--
|
|
54
|
-
[--
|
|
52
|
+
usage: speedtest-cli [-h] [-l] [-s SERVER] [--no-download] [--no-upload]
|
|
53
|
+
[-t THREADS | --single] [--share] [--bytes] [--json]
|
|
54
|
+
[--debug] [--version]
|
|
55
55
|
|
|
56
56
|
Next generation CLI for testing internet bandwidth using speedtest.net.
|
|
57
57
|
|
|
58
58
|
options:
|
|
59
59
|
-h, --help show this help message and exit
|
|
60
|
-
--version show program's version number and exit
|
|
61
60
|
|
|
62
61
|
Core Options:
|
|
63
62
|
-l, --list Show available speedtest.net servers sorted by distance. (default: False)
|
|
@@ -73,17 +72,12 @@ Transfer Modifiers:
|
|
|
73
72
|
|
|
74
73
|
Output Options:
|
|
75
74
|
--share Generate and provide a URL to the speedtest.net share results image. (default: False)
|
|
76
|
-
--bytes Display values in bytes instead of bits. Does not affect image generation or JSON
|
|
77
|
-
--csv Suppress verbose output, only show basic information in CSV format. Speeds listed in bit/s. (default: False)
|
|
75
|
+
--bytes Display values in bytes instead of bits. Does not affect image generation or JSON output. (default: ('bit', 1))
|
|
78
76
|
--json Suppress verbose output, only show basic information in JSON format. Speeds listed in bit/s. (default: False)
|
|
79
|
-
--csv-delimiter CSV_DELIMITER
|
|
80
|
-
Single character delimiter to use in CSV output. (default: ,)
|
|
81
|
-
--csv-header Print CSV headers and exit. (default: False)
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
--source SOURCE Bind a source IP address to use for connections. (default: None)
|
|
85
|
-
--timeout TIMEOUT HTTP timeout in seconds. (default: 10.0)
|
|
78
|
+
Miscellaneous Options:
|
|
86
79
|
--debug Show verbose debugging output. (default: False)
|
|
80
|
+
--version show program's version number and exit
|
|
87
81
|
```
|
|
88
82
|
|
|
89
83
|
## Development
|
|
@@ -127,7 +121,6 @@ uv run build
|
|
|
127
121
|
|
|
128
122
|
The output will be in the `dist/` directory.
|
|
129
123
|
|
|
130
|
-
|
|
131
124
|
## Acknowledgments
|
|
132
125
|
|
|
133
126
|
This project is a heavily refactored and modernized fork of the original [speedtest-cli](https://github.com/sivel/speedtest-cli) created by Matt Martz. While the codebase has been completely rewritten to support modern Python standards, concurrent thread pools, and memory-safe streaming, it was built upon the foundational concepts of the original tool.
|
|
@@ -62,12 +62,7 @@ Generate and provide a URL to the speedtest.net share results image.
|
|
|
62
62
|
|
|
63
63
|
\fB\-\-bytes\fR
|
|
64
64
|
.RS
|
|
65
|
-
Display values in bytes instead of bits. Does not affect image generation or JSON
|
|
66
|
-
.RE
|
|
67
|
-
|
|
68
|
-
\fB\-\-csv\fR
|
|
69
|
-
.RS
|
|
70
|
-
Suppress verbose output, only show basic information in CSV format. Speeds listed in bit/s.
|
|
65
|
+
Display values in bytes instead of bits. Does not affect image generation or JSON output.
|
|
71
66
|
.RE
|
|
72
67
|
|
|
73
68
|
\fB\-\-json\fR
|
|
@@ -75,27 +70,7 @@ Suppress verbose output, only show basic information in CSV format. Speeds liste
|
|
|
75
70
|
Suppress verbose output, only show basic information in JSON format. Speeds listed in bit/s.
|
|
76
71
|
.RE
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
.RS
|
|
80
|
-
Single character delimiter to use in CSV output. Default ","
|
|
81
|
-
.RE
|
|
82
|
-
|
|
83
|
-
\fB\-\-csv\-header\fR
|
|
84
|
-
.RS
|
|
85
|
-
Print CSV headers and exit.
|
|
86
|
-
.RE
|
|
87
|
-
|
|
88
|
-
.B Connection Options
|
|
89
|
-
|
|
90
|
-
\fB\-\-source SOURCE\fR
|
|
91
|
-
.RS
|
|
92
|
-
Bind a source IP address to use for connections.
|
|
93
|
-
.RE
|
|
94
|
-
|
|
95
|
-
\fB\-\-timeout TIMEOUT\fR
|
|
96
|
-
.RS
|
|
97
|
-
HTTP timeout in seconds. Default 10.0
|
|
98
|
-
.RE
|
|
73
|
+
.B Miscellaneous Options
|
|
99
74
|
|
|
100
75
|
\fB\-\-debug\fR
|
|
101
76
|
.RS
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "speedtest-cli-ng"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{name = "Michał Korczak", email = "git@omikor.in"}
|
|
10
10
|
# TODO: I'm not sure whether I should put the old author because of possible spam for him unknowingly etc
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wraps the core Speedtest logic into CLI actions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from speedtest.models import Server
|
|
6
|
+
from speedtest.utils.status import ExitStatus
|
|
7
|
+
|
|
8
|
+
__all__ = ["handle_server_list"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def handle_server_list(servers: list[Server]) -> int:
|
|
12
|
+
"""Handle the --list argument by printing nearby servers and exiting."""
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
for server in servers:
|
|
16
|
+
line = (
|
|
17
|
+
f"[{server.id:>5}]\t{server.distance} km\t"
|
|
18
|
+
f"{server.name} ({server.country}) "
|
|
19
|
+
f"by {server.sponsor}"
|
|
20
|
+
)
|
|
21
|
+
print(line)
|
|
22
|
+
except BrokenPipeError:
|
|
23
|
+
# Prevents messy tracebacks if the user pipes output to `head` or `less`
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
return ExitStatus.SUCCESS.value
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The main entry point of the CLI shell.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import signal
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from speedtest.cli.commands import handle_server_list
|
|
11
|
+
from speedtest.cli.output import format_json, format_text
|
|
12
|
+
from speedtest.cli.parser import parse_args
|
|
13
|
+
from speedtest.client import SpeedtestClient
|
|
14
|
+
from speedtest.engine.config import get_config
|
|
15
|
+
from speedtest.exceptions import SpeedtestCLIError
|
|
16
|
+
from speedtest.models import RunContext, TestResult
|
|
17
|
+
from speedtest.utils.logger import logger, setup_logging
|
|
18
|
+
from speedtest.utils.status import ExitStatus
|
|
19
|
+
|
|
20
|
+
__all__ = ["shell"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _register_shutdown_handler() -> threading.Event:
|
|
24
|
+
"""Register a SIGINT handler and return the associated shutdown event."""
|
|
25
|
+
|
|
26
|
+
shutdown_event = threading.Event()
|
|
27
|
+
|
|
28
|
+
def _handler(signum: int, frame: Any) -> None:
|
|
29
|
+
shutdown_event.set()
|
|
30
|
+
logger.warning("Stopping speedtest-cli...")
|
|
31
|
+
raise KeyboardInterrupt
|
|
32
|
+
|
|
33
|
+
signal.signal(signal.SIGINT, _handler)
|
|
34
|
+
return shutdown_event
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def shell() -> int:
|
|
38
|
+
"""Run the full speedtest.net test orchestrator."""
|
|
39
|
+
|
|
40
|
+
raw_args = parse_args()
|
|
41
|
+
ctx = RunContext.from_args(raw_args)
|
|
42
|
+
results = TestResult()
|
|
43
|
+
|
|
44
|
+
setup_logging(debug=ctx.debug_mode, quiet=ctx.is_quiet)
|
|
45
|
+
|
|
46
|
+
ctx.shutdown_event = _register_shutdown_handler()
|
|
47
|
+
client = SpeedtestClient()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
logger.info("[INFO] Retrieving speedtest.net configuration...")
|
|
51
|
+
ctx.api_config = get_config()
|
|
52
|
+
|
|
53
|
+
logger.info(
|
|
54
|
+
f"ISP: {ctx.api_config.ip_address} ({ctx.api_config.isp_name}) "
|
|
55
|
+
f"[{ctx.api_config.location.latitude:.4f}, {ctx.api_config.location.longitude:.4f}]"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
target_servers = client.get_target_servers(
|
|
59
|
+
config=ctx.api_config, target_id=ctx.target_server_id
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if ctx.list_servers_only:
|
|
63
|
+
return handle_server_list(target_servers)
|
|
64
|
+
|
|
65
|
+
# Latency test
|
|
66
|
+
results.server, results.ping_ms = client.select_best_server(target_servers)
|
|
67
|
+
|
|
68
|
+
logger.info(
|
|
69
|
+
f"Test server: [{results.server.id}] {results.server.distance} km "
|
|
70
|
+
f"{results.server.name} ({results.server.country}) "
|
|
71
|
+
f"by {results.server.sponsor}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
logger.info(f"Latency: {results.ping_ms:.5f} ms")
|
|
75
|
+
|
|
76
|
+
# Transfer tests
|
|
77
|
+
if not ctx.no_download:
|
|
78
|
+
results.download_bytes, results.download_bps = client.download(
|
|
79
|
+
server=results.server, ctx=ctx
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if not ctx.no_upload:
|
|
83
|
+
results.upload_bytes, results.upload_bps = client.upload(server=results.server, ctx=ctx)
|
|
84
|
+
|
|
85
|
+
if ctx.share:
|
|
86
|
+
logger.info("[INFO] Generating share link...")
|
|
87
|
+
results.share_url = client.generate_share_link(results)
|
|
88
|
+
|
|
89
|
+
logger.debug(f"Results:\n{dataclasses.asdict(results)!r}")
|
|
90
|
+
|
|
91
|
+
if ctx.json_output:
|
|
92
|
+
final_output = format_json(results, ctx.api_config)
|
|
93
|
+
else:
|
|
94
|
+
final_output = format_text(results, ctx.units)
|
|
95
|
+
|
|
96
|
+
print(final_output)
|
|
97
|
+
|
|
98
|
+
return ExitStatus.SUCCESS.value
|
|
99
|
+
|
|
100
|
+
except SpeedtestCLIError as e:
|
|
101
|
+
logger.error(f"Test failed: {e}")
|
|
102
|
+
|
|
103
|
+
return ExitStatus.ERROR.value
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Handles formatting of test results into strings.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
|
|
10
|
+
from speedtest.models import SpeedtestConfig, TestResult
|
|
11
|
+
|
|
12
|
+
__all__ = ["format_json", "format_text"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_json(results: TestResult, client_config: SpeedtestConfig | None = None) -> str:
|
|
16
|
+
"""Construct the machine-readable JSON representation as a string."""
|
|
17
|
+
|
|
18
|
+
json_data = {
|
|
19
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
20
|
+
"ping": results.ping_ms,
|
|
21
|
+
"download": results.download_bps,
|
|
22
|
+
"upload": results.upload_bps,
|
|
23
|
+
"bytes_received": results.download_bytes,
|
|
24
|
+
"bytes_sent": results.upload_bytes,
|
|
25
|
+
"share": results.share_url,
|
|
26
|
+
"server": dataclasses.asdict(results.server) if results.server else None,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if client_config:
|
|
30
|
+
json_data["client"] = {
|
|
31
|
+
"ip": client_config.ip_address,
|
|
32
|
+
"isp": client_config.isp_name,
|
|
33
|
+
"isp_id": client_config.isp_id,
|
|
34
|
+
"location": dataclasses.asdict(client_config.location),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
indent = 2 if sys.stdout.isatty() else None
|
|
38
|
+
return json.dumps(json_data, indent=indent)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_text(results: TestResult, units: tuple[str, int]) -> str:
|
|
42
|
+
"""Format the human-readable text representation as a string."""
|
|
43
|
+
|
|
44
|
+
unit_name, unit_divisor = units
|
|
45
|
+
lines = []
|
|
46
|
+
|
|
47
|
+
dl_speed = results.get_download_speed(unit_divisor)
|
|
48
|
+
dl_bytes = results.get_downloaded_megabytes()
|
|
49
|
+
|
|
50
|
+
if dl_speed is not None and dl_bytes is not None:
|
|
51
|
+
lines.append(f"Download: {dl_speed:.2f} M{unit_name}/s ({dl_bytes:.2f} MB)")
|
|
52
|
+
|
|
53
|
+
ul_speed = results.get_upload_speed(unit_divisor)
|
|
54
|
+
ul_bytes = results.get_uploaded_megabytes()
|
|
55
|
+
|
|
56
|
+
if ul_speed is not None and ul_bytes is not None:
|
|
57
|
+
lines.append(f"Upload: {ul_speed:.2f} M{unit_name}/s ({ul_bytes:.2f} MB)")
|
|
58
|
+
|
|
59
|
+
if results.share_url is not None:
|
|
60
|
+
lines.append(f"Share results: {results.share_url}")
|
|
61
|
+
|
|
62
|
+
return "\n".join(lines)
|
|
@@ -9,16 +9,6 @@ from speedtest import __version__
|
|
|
9
9
|
__all__ = ["parse_args"]
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def _single_char_delimiter(value: str) -> str:
|
|
13
|
-
"""Argparse type validator to ensure a delimiter is exactly one character."""
|
|
14
|
-
|
|
15
|
-
if len(value) != 1:
|
|
16
|
-
raise argparse.ArgumentTypeError(
|
|
17
|
-
f"CSV delimiter must be a single character, got: {value!r}"
|
|
18
|
-
)
|
|
19
|
-
return value
|
|
20
|
-
|
|
21
|
-
|
|
22
12
|
def parse_args() -> argparse.Namespace:
|
|
23
13
|
"""Function to handle building and parsing of command line arguments."""
|
|
24
14
|
|
|
@@ -83,23 +73,15 @@ def parse_args() -> argparse.Namespace:
|
|
|
83
73
|
"--bytes",
|
|
84
74
|
dest="units",
|
|
85
75
|
action="store_const",
|
|
86
|
-
const=("
|
|
87
|
-
default=("
|
|
76
|
+
const=("B", 8),
|
|
77
|
+
default=("b", 1),
|
|
88
78
|
help=(
|
|
89
79
|
"Display values in bytes instead of bits. "
|
|
90
|
-
"Does not affect image generation or JSON
|
|
80
|
+
"Does not affect image generation or JSON output."
|
|
91
81
|
),
|
|
92
82
|
)
|
|
93
83
|
|
|
94
84
|
format_group = output_group.add_mutually_exclusive_group()
|
|
95
|
-
format_group.add_argument(
|
|
96
|
-
"--csv",
|
|
97
|
-
action="store_true",
|
|
98
|
-
help=(
|
|
99
|
-
"Suppress verbose output, only show basic information "
|
|
100
|
-
"in CSV format. Speeds listed in bit/s."
|
|
101
|
-
),
|
|
102
|
-
)
|
|
103
85
|
format_group.add_argument(
|
|
104
86
|
"--json",
|
|
105
87
|
action="store_true",
|
|
@@ -109,22 +91,8 @@ def parse_args() -> argparse.Namespace:
|
|
|
109
91
|
),
|
|
110
92
|
)
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
type=_single_char_delimiter,
|
|
116
|
-
help="Single character delimiter to use in CSV output.",
|
|
117
|
-
)
|
|
118
|
-
output_group.add_argument(
|
|
119
|
-
"--csv-header", action="store_true", help="Print CSV headers and exit."
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
conn_group = parser.add_argument_group("Connection Options")
|
|
123
|
-
conn_group.add_argument(
|
|
124
|
-
"--source", type=str, help="Bind a source IP address to use for connections."
|
|
125
|
-
)
|
|
126
|
-
conn_group.add_argument("--timeout", default=10.0, type=float, help="HTTP timeout in seconds.")
|
|
127
|
-
conn_group.add_argument("--debug", action="store_true", help="Show verbose debugging output.")
|
|
128
|
-
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
94
|
+
misc_group = parser.add_argument_group("Miscellaneous Options")
|
|
95
|
+
misc_group.add_argument("--debug", action="store_true", help="Show verbose debugging output.")
|
|
96
|
+
misc_group.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
129
97
|
|
|
130
98
|
return parser.parse_args()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import urllib.error
|
|
2
|
+
import urllib.parse
|
|
3
|
+
import urllib.request
|
|
4
|
+
from hashlib import md5
|
|
5
|
+
from operator import attrgetter
|
|
6
|
+
|
|
7
|
+
from speedtest.engine.servers import get_best_server
|
|
8
|
+
from speedtest.engine.transfer import run_download_test, run_upload_test
|
|
9
|
+
from speedtest.exceptions import NoMatchedServer, SpeedtestCLIError
|
|
10
|
+
from speedtest.models import Server, SpeedtestConfig, TestResult
|
|
11
|
+
from speedtest.models.context import RunContext
|
|
12
|
+
|
|
13
|
+
__all__ = ["SpeedtestClient"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SpeedtestClient:
|
|
17
|
+
"""Orchestrates high-level speedtest actions."""
|
|
18
|
+
|
|
19
|
+
def get_target_servers(
|
|
20
|
+
self, config: SpeedtestConfig, target_id: int | None = None, limit: int = 5
|
|
21
|
+
) -> list[Server]:
|
|
22
|
+
"""Sorts available servers by distance and filters by ID if requested."""
|
|
23
|
+
|
|
24
|
+
servers = sorted(config.servers, key=attrgetter("distance"))
|
|
25
|
+
|
|
26
|
+
if target_id is not None:
|
|
27
|
+
servers = [srv for srv in servers if srv.id == target_id]
|
|
28
|
+
if not servers:
|
|
29
|
+
raise NoMatchedServer(f"No server matched the ID: {target_id}")
|
|
30
|
+
|
|
31
|
+
return servers[:limit]
|
|
32
|
+
|
|
33
|
+
def select_best_server(self, servers: list[Server]) -> tuple[Server, float]:
|
|
34
|
+
"""Pings a list of servers and returns the fastest one alongside its latency."""
|
|
35
|
+
|
|
36
|
+
if not servers:
|
|
37
|
+
raise SpeedtestCLIError("No servers provided to test against.")
|
|
38
|
+
|
|
39
|
+
best_server, latency = get_best_server(servers)
|
|
40
|
+
|
|
41
|
+
if not best_server:
|
|
42
|
+
raise SpeedtestCLIError("Failed to identify a valid best server.")
|
|
43
|
+
|
|
44
|
+
return best_server, latency
|
|
45
|
+
|
|
46
|
+
def download(self, server: Server, ctx: RunContext) -> tuple[int, float]:
|
|
47
|
+
"""Executes the download test. Returns (bytes_received, download_speed)."""
|
|
48
|
+
|
|
49
|
+
if not server.url:
|
|
50
|
+
raise SpeedtestCLIError("The target server is missing a valid URL.")
|
|
51
|
+
|
|
52
|
+
return run_download_test(best_server_url=server.url, ctx=ctx)
|
|
53
|
+
|
|
54
|
+
def upload(self, server: Server, ctx: RunContext) -> tuple[int, float]:
|
|
55
|
+
"""Executes the upload test. Returns (bytes_sent, upload_speed)."""
|
|
56
|
+
|
|
57
|
+
if not server.url:
|
|
58
|
+
raise SpeedtestCLIError("The target server is missing a valid URL.")
|
|
59
|
+
|
|
60
|
+
return run_upload_test(best_server_url=server.url, ctx=ctx)
|
|
61
|
+
|
|
62
|
+
def generate_share_link(self, results: TestResult) -> str:
|
|
63
|
+
"""POST data to the speedtest.net API to obtain a share results link."""
|
|
64
|
+
|
|
65
|
+
if not results.is_complete or not results.server:
|
|
66
|
+
raise SpeedtestCLIError("Cannot generate share link: missing test results.")
|
|
67
|
+
|
|
68
|
+
# The legacy Ookla API expects speeds in kilobits per second (kbps)
|
|
69
|
+
download_kbps = round((results.download_bps or 0) / 1000.0)
|
|
70
|
+
upload_kbps = round((results.upload_bps or 0) / 1000.0)
|
|
71
|
+
ping = round(results.ping_ms or 0)
|
|
72
|
+
|
|
73
|
+
hash_str = f"{ping}-{upload_kbps}-{download_kbps}-297aae72"
|
|
74
|
+
hash_val = md5(hash_str.encode()).hexdigest()
|
|
75
|
+
|
|
76
|
+
api_parameters = {
|
|
77
|
+
"recommendedserverid": results.server.id,
|
|
78
|
+
"ping": ping,
|
|
79
|
+
"screenresolution": "",
|
|
80
|
+
"promo": "",
|
|
81
|
+
"download": download_kbps,
|
|
82
|
+
"screendpi": "",
|
|
83
|
+
"upload": upload_kbps,
|
|
84
|
+
"testmethod": "http",
|
|
85
|
+
"hash": hash_val,
|
|
86
|
+
"touchscreen": "none",
|
|
87
|
+
"startmode": "pingselect",
|
|
88
|
+
"accuracy": 1,
|
|
89
|
+
"bytesreceived": results.download_bytes or 0,
|
|
90
|
+
"bytessent": results.upload_bytes or 0,
|
|
91
|
+
"serverid": results.server.id,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
api_data = urllib.parse.urlencode(api_parameters).encode()
|
|
95
|
+
headers = {"Referer": "https://c.speedtest.net/flash/speedtest.swf"}
|
|
96
|
+
|
|
97
|
+
req = urllib.request.Request(
|
|
98
|
+
"https://www.speedtest.net/api/api.php", data=api_data, headers=headers, method="POST"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
with urllib.request.urlopen(req) as response:
|
|
103
|
+
if response.status != 200:
|
|
104
|
+
raise SpeedtestCLIError(f"Could not submit results. HTTP {response.status}")
|
|
105
|
+
|
|
106
|
+
body = response.read().decode(errors="ignore")
|
|
107
|
+
|
|
108
|
+
except urllib.error.URLError as e:
|
|
109
|
+
raise SpeedtestCLIError(f"Failed to connect to Share API: {e}") from e
|
|
110
|
+
|
|
111
|
+
qsargs = urllib.parse.parse_qs(body)
|
|
112
|
+
resultid = qsargs.get("resultid")
|
|
113
|
+
|
|
114
|
+
if not resultid or len(resultid) != 1:
|
|
115
|
+
raise SpeedtestCLIError("Could not parse result ID from API response.")
|
|
116
|
+
|
|
117
|
+
return f"https://www.speedtest.net/result/{resultid[0]}.png"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retrieves and parses the core configuration JSON from speedtest.net.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from json import JSONDecodeError
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.error import URLError
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
from speedtest.engine.network import build_user_agent
|
|
12
|
+
from speedtest.models import SpeedtestConfig
|
|
13
|
+
|
|
14
|
+
CONFIG_URL = "https://www.speedtest.net/api/js/config-sdk"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigFetchError(Exception):
|
|
18
|
+
"""Raised when the configuration cannot be retrieved or parsed."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fetch_raw_config(url: str = CONFIG_URL) -> dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
Fetches the JSON configuration from the Speedtest API.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
user_agent = build_user_agent()
|
|
29
|
+
|
|
30
|
+
req = Request(url, headers={"User-Agent": user_agent, "Accept": "application/json"})
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
with urlopen(req, timeout=10.0) as response:
|
|
34
|
+
if response.status != 200:
|
|
35
|
+
raise ConfigFetchError(f"HTTP {response.status}: Failed to fetch config.")
|
|
36
|
+
|
|
37
|
+
body = response.read().decode("utf-8")
|
|
38
|
+
return json.loads(body)
|
|
39
|
+
|
|
40
|
+
except (URLError, JSONDecodeError) as e:
|
|
41
|
+
raise ConfigFetchError(f"Failed to retrieve or parse config: {e}") from e
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_config() -> SpeedtestConfig:
|
|
45
|
+
"""
|
|
46
|
+
Orchestrates fetching the raw data and building the strict dataclass models.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
raw_data = fetch_raw_config()
|
|
50
|
+
return SpeedtestConfig.from_dict(raw_data)
|