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
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/zspeedtest.svg)](https://pypi.org/project/zspeedtest)
38
+ [![PyPI version](https://badge.fury.io/py/zspeedtest.svg)](https://pypi.python.org/pypi/zspeedtest)
39
+ [![Tests](https://github.com/theseriff/zspeedtest/actions/workflows/pr_tests.yaml/badge.svg)](https://github.com/theseriff/zspeedtest/actions/workflows/pr_tests.yaml)
40
+ [![Coverage](https://coverage-badge.samuelcolvin.workers.dev/theseriff/zspeedtest.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/theseriff/zspeedtest)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/zspeedtest.svg)](https://pypi.org/project/zspeedtest)
7
+ [![PyPI version](https://badge.fury.io/py/zspeedtest.svg)](https://pypi.python.org/pypi/zspeedtest)
8
+ [![Tests](https://github.com/theseriff/zspeedtest/actions/workflows/pr_tests.yaml/badge.svg)](https://github.com/theseriff/zspeedtest/actions/workflows/pr_tests.yaml)
9
+ [![Coverage](https://coverage-badge.samuelcolvin.workers.dev/theseriff/zspeedtest.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/theseriff/zspeedtest)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ """Core zspeedtest components for the zspeedtest library."""
2
+
3
+ from importlib.metadata import version as get_version
4
+
5
+ __version__ = get_version("zspeedtest")
@@ -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()