zspeedtest 0.1.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.
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zspeedtest
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: zspeedtest
|
|
5
|
+
Keywords: zspeedtest
|
|
6
|
+
Author: Sergey
|
|
7
|
+
Author-email: Sergey <kava.develop@protonmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Typing :: Typed
|
|
10
|
+
Classifier: Framework :: Pytest
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10, <3.15
|
|
25
|
+
Project-URL: Homepage, https://theseriff.github.io/zspeedtest/
|
|
26
|
+
Project-URL: Repository, https://github.com/theseriff/zspeedtest
|
|
27
|
+
Project-URL: Documentation, https://theseriff.github.io/zspeedtest/
|
|
28
|
+
Project-URL: Changelog, https://github.com/theseriff/zspeedtest/blob/main/docs/CHANGELOG.md
|
|
29
|
+
Project-URL: Issues, https://github.com/theseriff/zspeedtest/issues
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
<div align="center">
|
|
33
|
+
|
|
34
|
+
<h1>zspeedtest</h1>
|
|
35
|
+
<p><strong>Simple CLI internet speed tester. Downloads a file and measures throughput.</strong></p>
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/zspeedtest)
|
|
38
|
+
[](https://pypi.python.org/pypi/zspeedtest)
|
|
39
|
+
[](https://github.com/theseriff/zspeedtest/actions/workflows/pr_tests.yaml)
|
|
40
|
+
[](https://coverage-badge.samuelcolvin.workers.dev/redirect/theseriff/zspeedtest)
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv add zspeedtest
|
|
47
|
+
zspeedtest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
zspeedtest [URL] [--requests N] [--timeout N]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `URL` — large file to download (default: 10MB test file from ThinkBroadband)
|
|
57
|
+
- `--requests`/`-n` — number of test requests (default: 10)
|
|
58
|
+
- `--timeout`/`-t` — per-request timeout in seconds (default: 30)
|
|
59
|
+
|
|
60
|
+
### Examples
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
zspeedtest
|
|
64
|
+
zspeedtest http://example.com/file.bin --requests 5
|
|
65
|
+
zspeedtest http://example.com/file.bin -n 3 -t 15
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Output:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
URL: http://ipv4.download.thinkbroadband.com/10MB.zip
|
|
72
|
+
Requests: 1
|
|
73
|
+
----------------------------------------------------
|
|
74
|
+
# Size Time Speed
|
|
75
|
+
----------------------------------------------------
|
|
76
|
+
|
|
77
|
+
1 10.0 MB 29.95s 0.33 MB/s
|
|
78
|
+
====================================================
|
|
79
|
+
Successful requests : 1 / 1
|
|
80
|
+
Total downloaded : 10.0 MB
|
|
81
|
+
Average time : 29.95 s
|
|
82
|
+
Average speed : 0.33 MB/s
|
|
83
|
+
Min / Max : 0.33 / 0.33 MB/s
|
|
84
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<h1>zspeedtest</h1>
|
|
4
|
+
<p><strong>Simple CLI internet speed tester. Downloads a file and measures throughput.</strong></p>
|
|
5
|
+
|
|
6
|
+
[](https://pypi.org/project/zspeedtest)
|
|
7
|
+
[](https://pypi.python.org/pypi/zspeedtest)
|
|
8
|
+
[](https://github.com/theseriff/zspeedtest/actions/workflows/pr_tests.yaml)
|
|
9
|
+
[](https://coverage-badge.samuelcolvin.workers.dev/redirect/theseriff/zspeedtest)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv add zspeedtest
|
|
16
|
+
zspeedtest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
zspeedtest [URL] [--requests N] [--timeout N]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `URL` — large file to download (default: 10MB test file from ThinkBroadband)
|
|
26
|
+
- `--requests`/`-n` — number of test requests (default: 10)
|
|
27
|
+
- `--timeout`/`-t` — per-request timeout in seconds (default: 30)
|
|
28
|
+
|
|
29
|
+
### Examples
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
zspeedtest
|
|
33
|
+
zspeedtest http://example.com/file.bin --requests 5
|
|
34
|
+
zspeedtest http://example.com/file.bin -n 3 -t 15
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Output:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
URL: http://ipv4.download.thinkbroadband.com/10MB.zip
|
|
41
|
+
Requests: 1
|
|
42
|
+
----------------------------------------------------
|
|
43
|
+
# Size Time Speed
|
|
44
|
+
----------------------------------------------------
|
|
45
|
+
|
|
46
|
+
1 10.0 MB 29.95s 0.33 MB/s
|
|
47
|
+
====================================================
|
|
48
|
+
Successful requests : 1 / 1
|
|
49
|
+
Total downloaded : 10.0 MB
|
|
50
|
+
Average time : 29.95 s
|
|
51
|
+
Average speed : 0.33 MB/s
|
|
52
|
+
Min / Max : 0.33 / 0.33 MB/s
|
|
53
|
+
```
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.23,<0.13.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[tool.uv.build-backend]
|
|
6
|
+
module-root = "src"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "zspeedtest"
|
|
10
|
+
version = "0.1.1"
|
|
11
|
+
description = "zspeedtest"
|
|
12
|
+
requires-python = ">=3.10,<3.15"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
authors = [{ name = "Sergey", email = "kava.develop@protonmail.com" }]
|
|
16
|
+
keywords = ["zspeedtest"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
"Framework :: Pytest",
|
|
20
|
+
"Framework :: AsyncIO",
|
|
21
|
+
"Natural Language :: English",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
"Topic :: Utilities",
|
|
33
|
+
]
|
|
34
|
+
dependencies = []
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
zspeedtest = "zspeedtest.main:run"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
lint = [
|
|
41
|
+
"mypy==2.1.0",
|
|
42
|
+
"ruff==0.15.18",
|
|
43
|
+
"bandit==1.9.4",
|
|
44
|
+
"zizmor==1.25.2",
|
|
45
|
+
"semgrep==1.167.0",
|
|
46
|
+
"codespell==2.4.2",
|
|
47
|
+
"ty==0.0.54",
|
|
48
|
+
# mypy extensions
|
|
49
|
+
"mypy-extensions==1.1.0",
|
|
50
|
+
"types-setuptools",
|
|
51
|
+
]
|
|
52
|
+
test = [
|
|
53
|
+
"pytest==9.1.1",
|
|
54
|
+
"pytest-asyncio==1.4.0",
|
|
55
|
+
"pytest-timeout>=2.4.0",
|
|
56
|
+
"pytest-cov>=6.2.1",
|
|
57
|
+
"covdefaults>=2.3.0",
|
|
58
|
+
"coverage[toml]==7.14.1",
|
|
59
|
+
]
|
|
60
|
+
docs = ["zensical==0.0.45"]
|
|
61
|
+
dev = [
|
|
62
|
+
{ include-group = "lint" },
|
|
63
|
+
{ include-group = "test" },
|
|
64
|
+
{ include-group = "docs" },
|
|
65
|
+
"detect-secrets==1.5.0",
|
|
66
|
+
"prek==0.4.5",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[project.urls]
|
|
70
|
+
Homepage = "https://theseriff.github.io/zspeedtest/"
|
|
71
|
+
Repository = "https://github.com/theseriff/zspeedtest"
|
|
72
|
+
Documentation = "https://theseriff.github.io/zspeedtest/"
|
|
73
|
+
Changelog = "https://github.com/theseriff/zspeedtest/blob/main/docs/CHANGELOG.md"
|
|
74
|
+
Issues = "https://github.com/theseriff/zspeedtest/issues"
|
|
75
|
+
|
|
76
|
+
# Tests
|
|
77
|
+
[tool.pytest.ini_options]
|
|
78
|
+
xfail_strict = true
|
|
79
|
+
timeout = 10
|
|
80
|
+
addopts = "--strict-markers --strict-config"
|
|
81
|
+
testpaths = ["tests"]
|
|
82
|
+
asyncio_mode = "auto"
|
|
83
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
84
|
+
filterwarnings = []
|
|
85
|
+
markers = []
|
|
86
|
+
|
|
87
|
+
[tool.coverage.run]
|
|
88
|
+
plugins = ["covdefaults"]
|
|
89
|
+
relative_files = true
|
|
90
|
+
parallel = true
|
|
91
|
+
branch = true
|
|
92
|
+
concurrency = ["thread", "multiprocessing"]
|
|
93
|
+
source = ["src", "tests"]
|
|
94
|
+
omit = ["**/__init__.py"]
|
|
95
|
+
|
|
96
|
+
[tool.coverage.paths]
|
|
97
|
+
source = ["src"]
|
|
98
|
+
tests = ["tests"]
|
|
99
|
+
|
|
100
|
+
[tool.coverage.report]
|
|
101
|
+
fail_under = 100
|
|
102
|
+
skip_empty = true
|
|
103
|
+
show_missing = true
|
|
104
|
+
exclude_also = [
|
|
105
|
+
'if TYPE_CHECKING:',
|
|
106
|
+
"if __name__ == .__main__.:",
|
|
107
|
+
'@()?abstractmethod',
|
|
108
|
+
"raise NotImplementedError",
|
|
109
|
+
"pass",
|
|
110
|
+
'\.\.\.',
|
|
111
|
+
]
|
|
112
|
+
omit = ["*/__init__.py", '*/types.py']
|
|
113
|
+
|
|
114
|
+
# Static Analysis
|
|
115
|
+
[tool.ty.environment]
|
|
116
|
+
python-version = "3.10"
|
|
117
|
+
|
|
118
|
+
[tool.ty.src]
|
|
119
|
+
include = ["src", "tests"]
|
|
120
|
+
|
|
121
|
+
[tool.ty.rules]
|
|
122
|
+
all = "error"
|
|
123
|
+
unresolved-attribute = "ignore"
|
|
124
|
+
|
|
125
|
+
[tool.ty.analysis]
|
|
126
|
+
respect-type-ignore-comments = false
|
|
127
|
+
|
|
128
|
+
[tool.ty.terminal]
|
|
129
|
+
error-on-warning = true
|
|
130
|
+
output-format = "concise"
|
|
131
|
+
|
|
132
|
+
[tool.mypy]
|
|
133
|
+
mypy_path = "src"
|
|
134
|
+
files = ["src", "tests"]
|
|
135
|
+
python_version = "3.10"
|
|
136
|
+
strict = true
|
|
137
|
+
pretty = true
|
|
138
|
+
warn_return_any = true
|
|
139
|
+
warn_unreachable = true
|
|
140
|
+
warn_unused_ignores = true
|
|
141
|
+
warn_redundant_casts = true
|
|
142
|
+
allow_redefinition = true
|
|
143
|
+
allow_untyped_calls = true
|
|
144
|
+
disallow_any_explicit = false
|
|
145
|
+
disallow_any_generics = false
|
|
146
|
+
disallow_subclassing_any = false
|
|
147
|
+
disallow_untyped_decorators = false
|
|
148
|
+
show_error_codes = true
|
|
149
|
+
show_error_context = true
|
|
150
|
+
show_column_numbers = true
|
|
151
|
+
check_untyped_defs = true
|
|
152
|
+
namespace_packages = true
|
|
153
|
+
ignore_missing_imports = true
|
|
154
|
+
|
|
155
|
+
[tool.semgrep]
|
|
156
|
+
paths = ["src"]
|
|
157
|
+
include = ["src"]
|
|
158
|
+
|
|
159
|
+
[tool.bandit]
|
|
160
|
+
targets = "src"
|
|
161
|
+
|
|
162
|
+
# Liter
|
|
163
|
+
[tool.ruff]
|
|
164
|
+
src = ["src"]
|
|
165
|
+
include = ["src/**.py", "tests/**.py"]
|
|
166
|
+
target-version = "py310"
|
|
167
|
+
line-length = 79
|
|
168
|
+
fix = true
|
|
169
|
+
|
|
170
|
+
[tool.ruff.lint]
|
|
171
|
+
select = ["ALL"]
|
|
172
|
+
ignore = [
|
|
173
|
+
"D203", # Conflict: D203 ("one-blank-line-before-class-docstring") is incompatible with D211 (No blank line before class docstring). We follow D211.
|
|
174
|
+
"D213", # Conflict: D213 ("multi-line summary second line") conflicts with D212 (summary on the same line as triple quotes). We follow D212.
|
|
175
|
+
"COM812", # Conflict: Danger! This rule ("trailing-comma-missing") conflicts with the automatic behavior of "ruff format", which manages trailing commas.
|
|
176
|
+
"SLF001",
|
|
177
|
+
"T201", # print
|
|
178
|
+
"RUF001", # cyrillic
|
|
179
|
+
"D",
|
|
180
|
+
"S310",
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
[tool.ruff.lint.per-file-ignores]
|
|
184
|
+
"tests/**" = ["S101", "D", "SLF001", "TID251", "PLR2004"]
|
|
185
|
+
|
|
186
|
+
[tool.ruff.format]
|
|
187
|
+
docstring-code-format = true
|
|
188
|
+
|
|
189
|
+
[tool.ruff.lint.isort]
|
|
190
|
+
known-first-party = ["src", "tests"]
|
|
191
|
+
no-lines-before = ["local-folder"]
|
|
192
|
+
|
|
193
|
+
[tool.codespell]
|
|
194
|
+
skip = "*.pyi,*.pyc,./site"
|
|
195
|
+
|
|
196
|
+
# Python Semantic Release
|
|
197
|
+
[tool.semantic_release]
|
|
198
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
199
|
+
commit_parser = "conventional"
|
|
200
|
+
build_command = """
|
|
201
|
+
uv lock --upgrade-package zspeedtest
|
|
202
|
+
git add uv.lock
|
|
203
|
+
uv build
|
|
204
|
+
"""
|
|
205
|
+
tag_format = "v{version}"
|
|
206
|
+
major_on_zero = false
|
|
207
|
+
allow_zero_version = true
|
|
208
|
+
commit_message = "chore(release): release v{version} [skip ci]"
|
|
209
|
+
|
|
210
|
+
[tool.semantic_release.commit_parser_options]
|
|
211
|
+
major_tags = ["BREAKING CHANGE"]
|
|
212
|
+
minor_tags = ["feat"]
|
|
213
|
+
patch_tags = ["fix", "perf"]
|
|
214
|
+
parse_squash_commits = true
|
|
215
|
+
ignore_merge_commits = true
|
|
216
|
+
|
|
217
|
+
[tool.semantic_release.changelog]
|
|
218
|
+
# Recommended patterns for conventional commits parser that is scope aware
|
|
219
|
+
exclude_commit_patterns = [
|
|
220
|
+
'''Initial [Cc]ommit.*''', # codespell:ignore ommit
|
|
221
|
+
'''refactor(?:\([^)]*?\))?: .+''',
|
|
222
|
+
'''polish(?:\([^)]*?\))?: .+''',
|
|
223
|
+
'''style(?:\([^)]*?\))?: .+''',
|
|
224
|
+
'''chore(?:\([^)]*?\))?: .+''',
|
|
225
|
+
'''build\((?!deps\): .+)''',
|
|
226
|
+
'''test(?:\([^)]*?\))?: .+''',
|
|
227
|
+
'''ci(?:\([^)]*?\))?: .+''',
|
|
228
|
+
'''Merged? .*''',
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
[tool.semantic_release.changelog.default_templates]
|
|
232
|
+
changelog_file = "docs/CHANGELOG.md"
|
|
233
|
+
mask_initial_release = true
|
|
234
|
+
|
|
235
|
+
[tool.semantic_release.publish]
|
|
236
|
+
dist_glob_patterns = ["dist/*"]
|
|
237
|
+
upload_to_vcs_release = true
|
|
238
|
+
|
|
239
|
+
[tool.semantic_release.remote]
|
|
240
|
+
type = "github"
|
|
241
|
+
ignore_token_for_push = true
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Internet speed tester.
|
|
2
|
+
|
|
3
|
+
Usage: zspeedtest <URL> [--requests N]
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
import textwrap
|
|
9
|
+
import time
|
|
10
|
+
from typing import NamedTuple
|
|
11
|
+
from urllib.request import Request, urlopen
|
|
12
|
+
|
|
13
|
+
KB = 1024
|
|
14
|
+
MB = KB**2
|
|
15
|
+
CHUNK_SIZE = KB * 64
|
|
16
|
+
DEFAULT_URL = "http://ipv4.download.thinkbroadband.com/10MB.zip"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SpeedTestArgs(NamedTuple):
|
|
20
|
+
url: str
|
|
21
|
+
requests: int
|
|
22
|
+
timeout: int
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_cli(cls) -> "SpeedTestArgs":
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
description="Internet speed tester",
|
|
28
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"url",
|
|
32
|
+
nargs="?",
|
|
33
|
+
default=DEFAULT_URL,
|
|
34
|
+
help="URL of a large file to download",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--requests",
|
|
38
|
+
"-n",
|
|
39
|
+
type=int,
|
|
40
|
+
default=10,
|
|
41
|
+
metavar="N",
|
|
42
|
+
help="Number of requests",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--timeout",
|
|
46
|
+
"-t",
|
|
47
|
+
type=int,
|
|
48
|
+
default=30,
|
|
49
|
+
metavar="N",
|
|
50
|
+
help="Request timeout",
|
|
51
|
+
)
|
|
52
|
+
namespace = parser.parse_args()
|
|
53
|
+
|
|
54
|
+
if not namespace.url.startswith(("http:", "https:")):
|
|
55
|
+
msg = "URL must start with 'http:' or 'https:'"
|
|
56
|
+
raise ValueError(msg)
|
|
57
|
+
|
|
58
|
+
return cls(**namespace.__dict__)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RequestResult(NamedTuple):
|
|
62
|
+
duration_seconds: float
|
|
63
|
+
bytes_downloaded: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def download_url(req: Request, timeout: int) -> RequestResult:
|
|
67
|
+
start = time.perf_counter()
|
|
68
|
+
total_bytes: int = 0
|
|
69
|
+
|
|
70
|
+
# nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # noqa: E501, ERA001
|
|
71
|
+
with urlopen(req, timeout=timeout) as response: # nosec
|
|
72
|
+
while chunk := response.read(CHUNK_SIZE):
|
|
73
|
+
total_bytes += len(chunk)
|
|
74
|
+
|
|
75
|
+
duration = time.perf_counter() - start
|
|
76
|
+
return RequestResult(
|
|
77
|
+
duration_seconds=duration,
|
|
78
|
+
bytes_downloaded=total_bytes,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def format_size(bytes_count: float) -> str:
|
|
83
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
84
|
+
if bytes_count < KB:
|
|
85
|
+
return f"{bytes_count:.1f} {unit}"
|
|
86
|
+
bytes_count /= KB
|
|
87
|
+
return f"{bytes_count:.1f} TB"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run() -> None:
|
|
91
|
+
args = SpeedTestArgs.from_cli()
|
|
92
|
+
start_info = textwrap.dedent(f"""\
|
|
93
|
+
URL: {args.url}
|
|
94
|
+
Requests: {args.requests}
|
|
95
|
+
{"-" * 52}
|
|
96
|
+
{"#":>3} {"Size":>10} {"Time":>8} {"Speed":>12}
|
|
97
|
+
{"-" * 52}
|
|
98
|
+
""")
|
|
99
|
+
print(start_info)
|
|
100
|
+
|
|
101
|
+
results: list[RequestResult] = []
|
|
102
|
+
|
|
103
|
+
req = Request(args.url)
|
|
104
|
+
req.add_header("User-Agent", "zspeedtest/1.0")
|
|
105
|
+
|
|
106
|
+
for i in range(1, args.requests + 1):
|
|
107
|
+
try:
|
|
108
|
+
r = download_url(req, timeout=args.timeout)
|
|
109
|
+
except Exception as e: # noqa: BLE001
|
|
110
|
+
print(f"{i:>3} ERROR: {e}")
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
results.append(r)
|
|
114
|
+
|
|
115
|
+
speed_mbps = (r.bytes_downloaded / r.duration_seconds) / MB
|
|
116
|
+
print(
|
|
117
|
+
f"{i:>3} {format_size(r.bytes_downloaded):>10}"
|
|
118
|
+
f" {r.duration_seconds:>7.2f}s {speed_mbps:>10.2f} MB/s"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if not results:
|
|
122
|
+
print("\nNo requests completed successfully.")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
print("=" * 52)
|
|
126
|
+
|
|
127
|
+
total_bytes = sum(r.bytes_downloaded for r in results)
|
|
128
|
+
total_time = sum(r.duration_seconds for r in results)
|
|
129
|
+
avg_time = total_time / len(results)
|
|
130
|
+
avg_speed = (total_bytes / total_time) / MB
|
|
131
|
+
min_speed = min(
|
|
132
|
+
(r.bytes_downloaded / r.duration_seconds) / MB for r in results
|
|
133
|
+
)
|
|
134
|
+
max_speed = max(
|
|
135
|
+
(r.bytes_downloaded / r.duration_seconds) / MB for r in results
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
end_info = textwrap.dedent(f"""\
|
|
139
|
+
Successful requests : {len(results)} / {args.requests}
|
|
140
|
+
Total downloaded : {format_size(total_bytes)}
|
|
141
|
+
Average time : {avg_time:.2f} s
|
|
142
|
+
Average speed : {avg_speed:.2f} MB/s
|
|
143
|
+
Min / Max : {min_speed:.2f} / {max_speed:.2f} MB/s
|
|
144
|
+
""")
|
|
145
|
+
print(end_info)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
run()
|