pywget 0.26__tar.gz → 2.0.2__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.
- pywget-2.0.2/LICENSE +29 -0
- pywget-2.0.2/PKG-INFO +370 -0
- pywget-2.0.2/README.md +300 -0
- pywget-2.0.2/pyproject.toml +115 -0
- pywget-2.0.2/pywget/__init__.py +58 -0
- pywget-2.0.2/pywget/__main__.py +13 -0
- pywget-2.0.2/pywget/__version__.py +4 -0
- pywget-2.0.2/pywget/cli.py +195 -0
- pywget-2.0.2/pywget/core.py +702 -0
- pywget-2.0.2/pywget/errors.py +65 -0
- pywget-2.0.2/pywget/progress.py +158 -0
- pywget-2.0.2/pywget/py.typed +0 -0
- pywget-2.0.2/pywget/utils.py +180 -0
- pywget-2.0.2/pywget/wget.py +41 -0
- pywget-2.0.2/pywget.egg-info/PKG-INFO +370 -0
- pywget-2.0.2/pywget.egg-info/SOURCES.txt +21 -0
- pywget-2.0.2/pywget.egg-info/entry_points.txt +2 -0
- pywget-2.0.2/pywget.egg-info/requires.txt +8 -0
- pywget-2.0.2/setup.cfg +4 -0
- pywget-2.0.2/tests/test_download.py +150 -0
- pywget-2.0.2/tests/test_utils.py +91 -0
- pywget-0.26/LICENSE.rst +0 -39
- pywget-0.26/PKG-INFO +0 -78
- pywget-0.26/README.rst +0 -51
- pywget-0.26/__version__.py +0 -1
- pywget-0.26/pywget/__init__.py +0 -8
- pywget-0.26/pywget/__main__.py +0 -2
- pywget-0.26/pywget/__version__.py +0 -1
- pywget-0.26/pywget/wget.py +0 -639
- pywget-0.26/pywget.egg-info/PKG-INFO +0 -78
- pywget-0.26/pywget.egg-info/SOURCES.txt +0 -14
- pywget-0.26/pywget.egg-info/entry_points.txt +0 -3
- pywget-0.26/setup.cfg +0 -14
- pywget-0.26/setup.py +0 -60
- {pywget-0.26 → pywget-2.0.2}/pywget.egg-info/dependency_links.txt +0 -0
- {pywget-0.26 → pywget-2.0.2}/pywget.egg-info/top_level.txt +0 -0
pywget-2.0.2/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-2026, Hadi Cahyadi
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
pywget-2.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pywget
|
|
3
|
+
Version: 2.0.2
|
|
4
|
+
Summary: Robust, resumable, multi-connection wget alternative - CLI and Python library, zero dependencies
|
|
5
|
+
Author-email: Hadi Cahyadi <cumulus13@gmail.com>
|
|
6
|
+
Maintainer-email: cumulus13 <cumulus13@gmail.com>
|
|
7
|
+
License: BSD 3-Clause License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2021-2026, Hadi Cahyadi
|
|
10
|
+
All rights reserved.
|
|
11
|
+
|
|
12
|
+
Redistribution and use in source and binary forms, with or without
|
|
13
|
+
modification, are permitted provided that the following conditions are met:
|
|
14
|
+
|
|
15
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
16
|
+
list of conditions and the following disclaimer.
|
|
17
|
+
|
|
18
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
19
|
+
this list of conditions and the following disclaimer in the documentation
|
|
20
|
+
and/or other materials provided with the distribution.
|
|
21
|
+
|
|
22
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
23
|
+
contributors may be used to endorse or promote products derived from
|
|
24
|
+
this software without specific prior written permission.
|
|
25
|
+
|
|
26
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
27
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
28
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
29
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
30
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
31
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
32
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
33
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
34
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
35
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
|
+
|
|
37
|
+
Project-URL: Homepage, https://github.com/cumulus13/pywget
|
|
38
|
+
Project-URL: Documentation, https://github.com/cumulus13/pywget#readme
|
|
39
|
+
Project-URL: Source, https://github.com/cumulus13/pywget
|
|
40
|
+
Project-URL: Issues, https://github.com/cumulus13/pywget/issues
|
|
41
|
+
Project-URL: Changelog, https://github.com/cumulus13/pywget/blob/master/CHANGELOG.md
|
|
42
|
+
Keywords: wget,download,downloader,resume,multi-connection,cli,http
|
|
43
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
44
|
+
Classifier: Environment :: Console
|
|
45
|
+
Classifier: Intended Audience :: Developers
|
|
46
|
+
Classifier: Intended Audience :: System Administrators
|
|
47
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
48
|
+
Classifier: Operating System :: OS Independent
|
|
49
|
+
Classifier: Programming Language :: Python :: 3
|
|
50
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
51
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
52
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
53
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
54
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
55
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
56
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
57
|
+
Classifier: Topic :: System :: Networking
|
|
58
|
+
Classifier: Typing :: Typed
|
|
59
|
+
Requires-Python: >=3.8
|
|
60
|
+
Description-Content-Type: text/markdown
|
|
61
|
+
License-File: LICENSE
|
|
62
|
+
Provides-Extra: dev
|
|
63
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
64
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
65
|
+
Requires-Dist: ruff; extra == "dev"
|
|
66
|
+
Requires-Dist: mypy; extra == "dev"
|
|
67
|
+
Requires-Dist: build; extra == "dev"
|
|
68
|
+
Requires-Dist: twine; extra == "dev"
|
|
69
|
+
Dynamic: license-file
|
|
70
|
+
|
|
71
|
+
# pywget
|
|
72
|
+
|
|
73
|
+
[](https://github.com/cumulus13/pywget/actions/workflows/ci.yml)
|
|
74
|
+
[](https://pypi.org/project/pywget/)
|
|
75
|
+
[](https://pypi.org/project/pywget/)
|
|
76
|
+
[](LICENSE)
|
|
77
|
+
|
|
78
|
+
A robust, resumable, **multi-connection** command-line downloader and Python library.
|
|
79
|
+
A drop-in-style `wget` alternative with zero third-party dependencies.
|
|
80
|
+
|
|
81
|
+
## Features
|
|
82
|
+
|
|
83
|
+
| Area | What you get |
|
|
84
|
+
|------|--------------|
|
|
85
|
+
| Speed | Parallel segmented download (`-n 1..32`), automatic single-stream fallback |
|
|
86
|
+
| Reliability | Crash-safe resume, retries with exponential backoff + jitter, `Retry-After`, atomic rename |
|
|
87
|
+
| Integrity | `--checksum sha256:...`, size verification, disk-space pre-check |
|
|
88
|
+
| Security | http/https only, credentials stripped on cross-origin redirects, path-traversal-proof filenames, TLS verification on by default |
|
|
89
|
+
| Control | Bandwidth limit, custom headers, basic auth, proxy, custom CA, timeouts |
|
|
90
|
+
| Automation | Batch input (`-i`), `--json` output, stable exit codes, SIGINT/SIGTERM-safe |
|
|
91
|
+
| Library | Typed API, progress callbacks, cancel support, legacy `wget.download()` kept |
|
|
92
|
+
| Portability | Python 3.8+, Windows / Linux / macOS, no dependencies |
|
|
93
|
+
|
|
94
|
+
## Installation
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install pywget
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> **v2 note:** Python 2 is no longer supported. Python 3.8+ is required. The CLI and
|
|
101
|
+
> `wget.download(url, dir)` keep working as before.
|
|
102
|
+
|
|
103
|
+
## Quick start
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# simple download into a directory (same as before)
|
|
107
|
+
pywget "https://example.com/file.iso" -p ~/Downloads
|
|
108
|
+
|
|
109
|
+
# 8 parallel connections, 2 MiB/s cap, verify checksum
|
|
110
|
+
pywget https://example.com/file.iso -n 8 --limit-rate 2M --checksum sha256:<hex>
|
|
111
|
+
|
|
112
|
+
# many URLs from a file (or stdin with -i -), JSON report
|
|
113
|
+
pywget -i urls.txt -p downloads --json
|
|
114
|
+
|
|
115
|
+
# auth + custom header
|
|
116
|
+
pywget https://api.example.com/export.zip --user me -H "Accept: application/zip"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Interrupted (Ctrl+C, crash, network loss)? Run the same command again - it resumes.
|
|
120
|
+
|
|
121
|
+
## CLI reference
|
|
122
|
+
|
|
123
|
+
| Option | Description |
|
|
124
|
+
|--------|-------------|
|
|
125
|
+
| `URL ...` | One or more http(s) URLs |
|
|
126
|
+
| `-i, --input-file FILE` | Read URLs from file (`-` = stdin); `#` comments allowed |
|
|
127
|
+
| `-p, -P, --path DIR` | Download directory (default `.`) |
|
|
128
|
+
| `-O, --output NAME` | Output file name (single URL) |
|
|
129
|
+
| `-n, --connections N` | Parallel connections per file, 1-32 (default 4) |
|
|
130
|
+
| `-r, --retries N` | Retries per request/segment (default 5) |
|
|
131
|
+
| `-T, --timeout SEC` | Network timeout (default 30) |
|
|
132
|
+
| `--limit-rate RATE` | Total speed cap, e.g. `500k`, `2M` |
|
|
133
|
+
| `-H, --header 'K: V'` | Extra header (repeatable) |
|
|
134
|
+
| `-U, --user-agent UA` | Custom User-Agent |
|
|
135
|
+
| `--user`, `--password` | Basic auth (password also from `PYWGET_PASSWORD` or prompt) |
|
|
136
|
+
| `--proxy URL` | Proxy (default: environment) |
|
|
137
|
+
| `--no-check-certificate` | Disable TLS verification (not recommended) |
|
|
138
|
+
| `--ca-cert FILE` | Custom CA bundle |
|
|
139
|
+
| `--checksum ALGO:HEX` | Verify result (md5, sha1, sha256, sha512, ...) |
|
|
140
|
+
| `--overwrite` | Replace existing file and restart |
|
|
141
|
+
| `--no-resume` | Never resume partial files |
|
|
142
|
+
| `--no-mtime` | Do not copy `Last-Modified` to the file |
|
|
143
|
+
| `-q`, `-v` | Quiet / debug logging |
|
|
144
|
+
| `--json` | Machine-readable results on stdout |
|
|
145
|
+
| `-c` | Accepted for wget compatibility (resume is default) |
|
|
146
|
+
|
|
147
|
+
### Exit codes
|
|
148
|
+
|
|
149
|
+
| Code | Meaning |
|
|
150
|
+
|------|---------|
|
|
151
|
+
| 0 | Success |
|
|
152
|
+
| 1 | Unexpected error |
|
|
153
|
+
| 2 | Bad usage / invalid URL |
|
|
154
|
+
| 3 | Network error |
|
|
155
|
+
| 4 | HTTP error status (404, 403, ...) |
|
|
156
|
+
| 5 | Checksum mismatch |
|
|
157
|
+
| 6 | Storage / I/O error (disk full, permissions) |
|
|
158
|
+
| 130 | Interrupted (partial data kept for resume) |
|
|
159
|
+
|
|
160
|
+
## Python API
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import pywget
|
|
164
|
+
|
|
165
|
+
path = pywget.download(
|
|
166
|
+
"https://example.com/file.iso",
|
|
167
|
+
"/data/downloads",
|
|
168
|
+
progress=True, # terminal bar
|
|
169
|
+
connections=8,
|
|
170
|
+
limit_rate="5M",
|
|
171
|
+
checksum="sha256:ab12...",
|
|
172
|
+
)
|
|
173
|
+
print(path)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Full control:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from pywget import Downloader, Options, ProgressInfo
|
|
180
|
+
|
|
181
|
+
def on_progress(p: ProgressInfo) -> None:
|
|
182
|
+
print(f"{p.filename}: {p.percent or 0:.1f}% {p.speed/1e6:.1f} MB/s ETA {p.eta}")
|
|
183
|
+
|
|
184
|
+
dl = Downloader(Options(output_dir="out", retries=8, progress=on_progress,
|
|
185
|
+
headers={"Authorization": "Bearer TOKEN"}))
|
|
186
|
+
result = dl.fetch("https://example.com/big.bin")
|
|
187
|
+
print(result.path, result.size, result.resumed, result.connections)
|
|
188
|
+
|
|
189
|
+
# from another thread: dl.cancel() -> partial state is kept for resume
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Legacy API (unchanged):
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from pywget import wget
|
|
196
|
+
|
|
197
|
+
wget.download("http://www.test.com/test.tar.gz", "/home/root/Downloads")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Errors derive from `pywget.PywgetError` (`InvalidURLError`, `NetworkError`,
|
|
201
|
+
`HTTPStatusError`, `ChecksumError`, `StorageError`, `DownloadCancelled`).
|
|
202
|
+
A `Downloader` runs one download at a time - use one instance per thread.
|
|
203
|
+
|
|
204
|
+
## How it works
|
|
205
|
+
|
|
206
|
+
### Architecture
|
|
207
|
+
|
|
208
|
+
```mermaid
|
|
209
|
+
flowchart LR
|
|
210
|
+
CLI["CLI<br/>pywget.cli"] --> DL
|
|
211
|
+
API["Python API<br/>pywget.download()"] --> DL
|
|
212
|
+
LEG["Legacy shim<br/>pywget.wget"] --> DL
|
|
213
|
+
|
|
214
|
+
subgraph Engine["pywget.core"]
|
|
215
|
+
DL["Downloader"] --> PROBE["Probe<br/>Range: bytes=0-0"]
|
|
216
|
+
PROBE --> PLAN{"Size known and<br/>ranges supported?"}
|
|
217
|
+
PLAN -- yes --> SEG["Segment workers<br/>1..N threads"]
|
|
218
|
+
PLAN -- no --> STR["Single stream<br/>worker"]
|
|
219
|
+
SEG --> MON["Monitor loop<br/>progress + state save"]
|
|
220
|
+
STR --> MON
|
|
221
|
+
MON --> VER["Verify<br/>size + checksum"]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
SEG -. throttled by .-> RL["RateLimiter"]
|
|
225
|
+
STR -. throttled by .-> RL
|
|
226
|
+
MON --> TR["Tracker / ProgressBar"]
|
|
227
|
+
SEG <--> ST[("state file<br/>.pywget")]
|
|
228
|
+
VER --> OUT[("final file<br/>atomic rename")]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
[](https://i.ibb.co/mr6hxjYg/pywget-architecture1.jpg)
|
|
232
|
+
|
|
233
|
+
### Download sequence
|
|
234
|
+
|
|
235
|
+
```mermaid
|
|
236
|
+
sequenceDiagram
|
|
237
|
+
autonumber
|
|
238
|
+
actor U as User
|
|
239
|
+
participant D as Downloader
|
|
240
|
+
participant S as Server
|
|
241
|
+
participant F as Disk
|
|
242
|
+
|
|
243
|
+
U->>D: fetch(url)
|
|
244
|
+
D->>S: GET Range: bytes=0-0
|
|
245
|
+
S-->>D: 206 + Content-Range (size, ETag)
|
|
246
|
+
D->>F: load .pywget state (if any)
|
|
247
|
+
alt state matches size / ETag / mtime
|
|
248
|
+
D->>D: resume remaining segments
|
|
249
|
+
else fresh start
|
|
250
|
+
D->>F: preallocate .part, split into N segments
|
|
251
|
+
end
|
|
252
|
+
par segment 1..N
|
|
253
|
+
D->>S: GET Range: bytes=a-b
|
|
254
|
+
S-->>D: 206 data
|
|
255
|
+
D->>F: write at offset
|
|
256
|
+
end
|
|
257
|
+
loop every second
|
|
258
|
+
D->>F: save state
|
|
259
|
+
D-->>U: progress callback
|
|
260
|
+
end
|
|
261
|
+
D->>F: verify size and checksum
|
|
262
|
+
D->>F: rename .part to final name
|
|
263
|
+
D-->>U: DownloadResult
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
[](https://i.ibb.co/DPgLTD6D/pywget-architecture2.jpg)
|
|
267
|
+
|
|
268
|
+
### Segment lifecycle and retries
|
|
269
|
+
|
|
270
|
+
```mermaid
|
|
271
|
+
stateDiagram-v2
|
|
272
|
+
[*] --> Probing
|
|
273
|
+
Probing --> Planning: size and ranges known
|
|
274
|
+
Probing --> Streaming: no range support
|
|
275
|
+
Planning --> Downloading
|
|
276
|
+
Streaming --> Verifying
|
|
277
|
+
Downloading --> Retrying: timeout / reset / 5xx / 429
|
|
278
|
+
Retrying --> Downloading: backoff + jitter, resume at offset
|
|
279
|
+
Retrying --> Failed: retries exhausted
|
|
280
|
+
Downloading --> Interrupted: Ctrl+C / SIGTERM / cancel()
|
|
281
|
+
Interrupted --> Planning: re-run, state validated
|
|
282
|
+
Downloading --> Verifying: all segments complete
|
|
283
|
+
Verifying --> Completed: size and checksum OK
|
|
284
|
+
Verifying --> Failed: mismatch, partial files removed
|
|
285
|
+
Completed --> [*]
|
|
286
|
+
Failed --> [*]
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
[](https://i.ibb.co/YBZKFsxG/pywget-architecture3.jpg)
|
|
290
|
+
|
|
291
|
+
### Resume decision
|
|
292
|
+
|
|
293
|
+
```mermaid
|
|
294
|
+
flowchart TD
|
|
295
|
+
A["Start"] --> B{".pywget state<br/>and .part exist?"}
|
|
296
|
+
B -- no --> N["Fresh download"]
|
|
297
|
+
B -- yes --> C{"URL, size, ETag,<br/>Last-Modified equal?"}
|
|
298
|
+
C -- no --> N
|
|
299
|
+
C -- yes --> D{"Segments valid and<br/>.part size correct?"}
|
|
300
|
+
D -- no --> N
|
|
301
|
+
D -- yes --> R["Resume each segment<br/>from its saved offset"]
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
[](https://i.ibb.co/nqGmzdpw/pywget-architecture4.jpg)
|
|
305
|
+
|
|
306
|
+
### Main types
|
|
307
|
+
|
|
308
|
+
```mermaid
|
|
309
|
+
classDiagram
|
|
310
|
+
class Downloader {
|
|
311
|
+
+fetch(url) DownloadResult
|
|
312
|
+
+download(url) Path
|
|
313
|
+
+cancel()
|
|
314
|
+
}
|
|
315
|
+
class Options {
|
|
316
|
+
connections
|
|
317
|
+
retries
|
|
318
|
+
limit_rate
|
|
319
|
+
checksum
|
|
320
|
+
progress
|
|
321
|
+
}
|
|
322
|
+
class DownloadResult {
|
|
323
|
+
path
|
|
324
|
+
size
|
|
325
|
+
resumed
|
|
326
|
+
connections
|
|
327
|
+
checksum
|
|
328
|
+
}
|
|
329
|
+
class Tracker
|
|
330
|
+
class ProgressBar
|
|
331
|
+
class RateLimiter
|
|
332
|
+
Downloader o-- Options
|
|
333
|
+
Downloader --> DownloadResult
|
|
334
|
+
Downloader --> Tracker
|
|
335
|
+
Downloader --> RateLimiter
|
|
336
|
+
ProgressBar ..> Tracker : renders snapshots
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
[](https://i.ibb.co/9HYQ3WrT/pywget-architecture5.jpg)
|
|
340
|
+
|
|
341
|
+
## Behaviour details
|
|
342
|
+
|
|
343
|
+
- **Probing:** one `Range: bytes=0-0` request yields size, range support, ETag, `Last-Modified` and `Content-Disposition`.
|
|
344
|
+
- **Segments:** a file is split into `min(connections, size / 2 MiB)` parts; small files use one connection.
|
|
345
|
+
- **Existing file:** same size means skipped; different size is saved as `name.1` (wget style) unless `--overwrite`.
|
|
346
|
+
- **Partial files:** stored as `name.part` + `name.pywget`; removed on success. Changed remote file (size/ETag/mtime) restarts cleanly.
|
|
347
|
+
- **Redirects:** only http/https; `Authorization`/`Cookie` are dropped when the origin changes.
|
|
348
|
+
- **Not supported (by design):** FTP, recursive mirroring.
|
|
349
|
+
|
|
350
|
+
## Development
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
git clone https://github.com/cumulus13/pywget && cd pywget
|
|
354
|
+
pip install -e ".[dev]"
|
|
355
|
+
pytest --cov=pywget
|
|
356
|
+
ruff check pywget tests && mypy pywget
|
|
357
|
+
python -m build
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## License
|
|
361
|
+
|
|
362
|
+
[BSD](LICENSE)
|
|
363
|
+
|
|
364
|
+
## Author
|
|
365
|
+
|
|
366
|
+
[Hadi Cahyadi](https://github.com/cumulus13)
|
|
367
|
+
|
|
368
|
+
[](https://www.buymeacoffee.com/cumulus13)
|
|
369
|
+
[](https://ko-fi.com/cumulus13)
|
|
370
|
+
[Support me on Patreon](https://www.patreon.com/cumulus13)
|
pywget-2.0.2/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# pywget
|
|
2
|
+
|
|
3
|
+
[](https://github.com/cumulus13/pywget/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/pywget/)
|
|
5
|
+
[](https://pypi.org/project/pywget/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A robust, resumable, **multi-connection** command-line downloader and Python library.
|
|
9
|
+
A drop-in-style `wget` alternative with zero third-party dependencies.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
| Area | What you get |
|
|
14
|
+
|------|--------------|
|
|
15
|
+
| Speed | Parallel segmented download (`-n 1..32`), automatic single-stream fallback |
|
|
16
|
+
| Reliability | Crash-safe resume, retries with exponential backoff + jitter, `Retry-After`, atomic rename |
|
|
17
|
+
| Integrity | `--checksum sha256:...`, size verification, disk-space pre-check |
|
|
18
|
+
| Security | http/https only, credentials stripped on cross-origin redirects, path-traversal-proof filenames, TLS verification on by default |
|
|
19
|
+
| Control | Bandwidth limit, custom headers, basic auth, proxy, custom CA, timeouts |
|
|
20
|
+
| Automation | Batch input (`-i`), `--json` output, stable exit codes, SIGINT/SIGTERM-safe |
|
|
21
|
+
| Library | Typed API, progress callbacks, cancel support, legacy `wget.download()` kept |
|
|
22
|
+
| Portability | Python 3.8+, Windows / Linux / macOS, no dependencies |
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install pywget
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> **v2 note:** Python 2 is no longer supported. Python 3.8+ is required. The CLI and
|
|
31
|
+
> `wget.download(url, dir)` keep working as before.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# simple download into a directory (same as before)
|
|
37
|
+
pywget "https://example.com/file.iso" -p ~/Downloads
|
|
38
|
+
|
|
39
|
+
# 8 parallel connections, 2 MiB/s cap, verify checksum
|
|
40
|
+
pywget https://example.com/file.iso -n 8 --limit-rate 2M --checksum sha256:<hex>
|
|
41
|
+
|
|
42
|
+
# many URLs from a file (or stdin with -i -), JSON report
|
|
43
|
+
pywget -i urls.txt -p downloads --json
|
|
44
|
+
|
|
45
|
+
# auth + custom header
|
|
46
|
+
pywget https://api.example.com/export.zip --user me -H "Accept: application/zip"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Interrupted (Ctrl+C, crash, network loss)? Run the same command again - it resumes.
|
|
50
|
+
|
|
51
|
+
## CLI reference
|
|
52
|
+
|
|
53
|
+
| Option | Description |
|
|
54
|
+
|--------|-------------|
|
|
55
|
+
| `URL ...` | One or more http(s) URLs |
|
|
56
|
+
| `-i, --input-file FILE` | Read URLs from file (`-` = stdin); `#` comments allowed |
|
|
57
|
+
| `-p, -P, --path DIR` | Download directory (default `.`) |
|
|
58
|
+
| `-O, --output NAME` | Output file name (single URL) |
|
|
59
|
+
| `-n, --connections N` | Parallel connections per file, 1-32 (default 4) |
|
|
60
|
+
| `-r, --retries N` | Retries per request/segment (default 5) |
|
|
61
|
+
| `-T, --timeout SEC` | Network timeout (default 30) |
|
|
62
|
+
| `--limit-rate RATE` | Total speed cap, e.g. `500k`, `2M` |
|
|
63
|
+
| `-H, --header 'K: V'` | Extra header (repeatable) |
|
|
64
|
+
| `-U, --user-agent UA` | Custom User-Agent |
|
|
65
|
+
| `--user`, `--password` | Basic auth (password also from `PYWGET_PASSWORD` or prompt) |
|
|
66
|
+
| `--proxy URL` | Proxy (default: environment) |
|
|
67
|
+
| `--no-check-certificate` | Disable TLS verification (not recommended) |
|
|
68
|
+
| `--ca-cert FILE` | Custom CA bundle |
|
|
69
|
+
| `--checksum ALGO:HEX` | Verify result (md5, sha1, sha256, sha512, ...) |
|
|
70
|
+
| `--overwrite` | Replace existing file and restart |
|
|
71
|
+
| `--no-resume` | Never resume partial files |
|
|
72
|
+
| `--no-mtime` | Do not copy `Last-Modified` to the file |
|
|
73
|
+
| `-q`, `-v` | Quiet / debug logging |
|
|
74
|
+
| `--json` | Machine-readable results on stdout |
|
|
75
|
+
| `-c` | Accepted for wget compatibility (resume is default) |
|
|
76
|
+
|
|
77
|
+
### Exit codes
|
|
78
|
+
|
|
79
|
+
| Code | Meaning |
|
|
80
|
+
|------|---------|
|
|
81
|
+
| 0 | Success |
|
|
82
|
+
| 1 | Unexpected error |
|
|
83
|
+
| 2 | Bad usage / invalid URL |
|
|
84
|
+
| 3 | Network error |
|
|
85
|
+
| 4 | HTTP error status (404, 403, ...) |
|
|
86
|
+
| 5 | Checksum mismatch |
|
|
87
|
+
| 6 | Storage / I/O error (disk full, permissions) |
|
|
88
|
+
| 130 | Interrupted (partial data kept for resume) |
|
|
89
|
+
|
|
90
|
+
## Python API
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import pywget
|
|
94
|
+
|
|
95
|
+
path = pywget.download(
|
|
96
|
+
"https://example.com/file.iso",
|
|
97
|
+
"/data/downloads",
|
|
98
|
+
progress=True, # terminal bar
|
|
99
|
+
connections=8,
|
|
100
|
+
limit_rate="5M",
|
|
101
|
+
checksum="sha256:ab12...",
|
|
102
|
+
)
|
|
103
|
+
print(path)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Full control:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from pywget import Downloader, Options, ProgressInfo
|
|
110
|
+
|
|
111
|
+
def on_progress(p: ProgressInfo) -> None:
|
|
112
|
+
print(f"{p.filename}: {p.percent or 0:.1f}% {p.speed/1e6:.1f} MB/s ETA {p.eta}")
|
|
113
|
+
|
|
114
|
+
dl = Downloader(Options(output_dir="out", retries=8, progress=on_progress,
|
|
115
|
+
headers={"Authorization": "Bearer TOKEN"}))
|
|
116
|
+
result = dl.fetch("https://example.com/big.bin")
|
|
117
|
+
print(result.path, result.size, result.resumed, result.connections)
|
|
118
|
+
|
|
119
|
+
# from another thread: dl.cancel() -> partial state is kept for resume
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Legacy API (unchanged):
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from pywget import wget
|
|
126
|
+
|
|
127
|
+
wget.download("http://www.test.com/test.tar.gz", "/home/root/Downloads")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Errors derive from `pywget.PywgetError` (`InvalidURLError`, `NetworkError`,
|
|
131
|
+
`HTTPStatusError`, `ChecksumError`, `StorageError`, `DownloadCancelled`).
|
|
132
|
+
A `Downloader` runs one download at a time - use one instance per thread.
|
|
133
|
+
|
|
134
|
+
## How it works
|
|
135
|
+
|
|
136
|
+
### Architecture
|
|
137
|
+
|
|
138
|
+
```mermaid
|
|
139
|
+
flowchart LR
|
|
140
|
+
CLI["CLI<br/>pywget.cli"] --> DL
|
|
141
|
+
API["Python API<br/>pywget.download()"] --> DL
|
|
142
|
+
LEG["Legacy shim<br/>pywget.wget"] --> DL
|
|
143
|
+
|
|
144
|
+
subgraph Engine["pywget.core"]
|
|
145
|
+
DL["Downloader"] --> PROBE["Probe<br/>Range: bytes=0-0"]
|
|
146
|
+
PROBE --> PLAN{"Size known and<br/>ranges supported?"}
|
|
147
|
+
PLAN -- yes --> SEG["Segment workers<br/>1..N threads"]
|
|
148
|
+
PLAN -- no --> STR["Single stream<br/>worker"]
|
|
149
|
+
SEG --> MON["Monitor loop<br/>progress + state save"]
|
|
150
|
+
STR --> MON
|
|
151
|
+
MON --> VER["Verify<br/>size + checksum"]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
SEG -. throttled by .-> RL["RateLimiter"]
|
|
155
|
+
STR -. throttled by .-> RL
|
|
156
|
+
MON --> TR["Tracker / ProgressBar"]
|
|
157
|
+
SEG <--> ST[("state file<br/>.pywget")]
|
|
158
|
+
VER --> OUT[("final file<br/>atomic rename")]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
[](https://i.ibb.co/mr6hxjYg/pywget-architecture1.jpg)
|
|
162
|
+
|
|
163
|
+
### Download sequence
|
|
164
|
+
|
|
165
|
+
```mermaid
|
|
166
|
+
sequenceDiagram
|
|
167
|
+
autonumber
|
|
168
|
+
actor U as User
|
|
169
|
+
participant D as Downloader
|
|
170
|
+
participant S as Server
|
|
171
|
+
participant F as Disk
|
|
172
|
+
|
|
173
|
+
U->>D: fetch(url)
|
|
174
|
+
D->>S: GET Range: bytes=0-0
|
|
175
|
+
S-->>D: 206 + Content-Range (size, ETag)
|
|
176
|
+
D->>F: load .pywget state (if any)
|
|
177
|
+
alt state matches size / ETag / mtime
|
|
178
|
+
D->>D: resume remaining segments
|
|
179
|
+
else fresh start
|
|
180
|
+
D->>F: preallocate .part, split into N segments
|
|
181
|
+
end
|
|
182
|
+
par segment 1..N
|
|
183
|
+
D->>S: GET Range: bytes=a-b
|
|
184
|
+
S-->>D: 206 data
|
|
185
|
+
D->>F: write at offset
|
|
186
|
+
end
|
|
187
|
+
loop every second
|
|
188
|
+
D->>F: save state
|
|
189
|
+
D-->>U: progress callback
|
|
190
|
+
end
|
|
191
|
+
D->>F: verify size and checksum
|
|
192
|
+
D->>F: rename .part to final name
|
|
193
|
+
D-->>U: DownloadResult
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
[](https://i.ibb.co/DPgLTD6D/pywget-architecture2.jpg)
|
|
197
|
+
|
|
198
|
+
### Segment lifecycle and retries
|
|
199
|
+
|
|
200
|
+
```mermaid
|
|
201
|
+
stateDiagram-v2
|
|
202
|
+
[*] --> Probing
|
|
203
|
+
Probing --> Planning: size and ranges known
|
|
204
|
+
Probing --> Streaming: no range support
|
|
205
|
+
Planning --> Downloading
|
|
206
|
+
Streaming --> Verifying
|
|
207
|
+
Downloading --> Retrying: timeout / reset / 5xx / 429
|
|
208
|
+
Retrying --> Downloading: backoff + jitter, resume at offset
|
|
209
|
+
Retrying --> Failed: retries exhausted
|
|
210
|
+
Downloading --> Interrupted: Ctrl+C / SIGTERM / cancel()
|
|
211
|
+
Interrupted --> Planning: re-run, state validated
|
|
212
|
+
Downloading --> Verifying: all segments complete
|
|
213
|
+
Verifying --> Completed: size and checksum OK
|
|
214
|
+
Verifying --> Failed: mismatch, partial files removed
|
|
215
|
+
Completed --> [*]
|
|
216
|
+
Failed --> [*]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
[](https://i.ibb.co/YBZKFsxG/pywget-architecture3.jpg)
|
|
220
|
+
|
|
221
|
+
### Resume decision
|
|
222
|
+
|
|
223
|
+
```mermaid
|
|
224
|
+
flowchart TD
|
|
225
|
+
A["Start"] --> B{".pywget state<br/>and .part exist?"}
|
|
226
|
+
B -- no --> N["Fresh download"]
|
|
227
|
+
B -- yes --> C{"URL, size, ETag,<br/>Last-Modified equal?"}
|
|
228
|
+
C -- no --> N
|
|
229
|
+
C -- yes --> D{"Segments valid and<br/>.part size correct?"}
|
|
230
|
+
D -- no --> N
|
|
231
|
+
D -- yes --> R["Resume each segment<br/>from its saved offset"]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
[](https://i.ibb.co/nqGmzdpw/pywget-architecture4.jpg)
|
|
235
|
+
|
|
236
|
+
### Main types
|
|
237
|
+
|
|
238
|
+
```mermaid
|
|
239
|
+
classDiagram
|
|
240
|
+
class Downloader {
|
|
241
|
+
+fetch(url) DownloadResult
|
|
242
|
+
+download(url) Path
|
|
243
|
+
+cancel()
|
|
244
|
+
}
|
|
245
|
+
class Options {
|
|
246
|
+
connections
|
|
247
|
+
retries
|
|
248
|
+
limit_rate
|
|
249
|
+
checksum
|
|
250
|
+
progress
|
|
251
|
+
}
|
|
252
|
+
class DownloadResult {
|
|
253
|
+
path
|
|
254
|
+
size
|
|
255
|
+
resumed
|
|
256
|
+
connections
|
|
257
|
+
checksum
|
|
258
|
+
}
|
|
259
|
+
class Tracker
|
|
260
|
+
class ProgressBar
|
|
261
|
+
class RateLimiter
|
|
262
|
+
Downloader o-- Options
|
|
263
|
+
Downloader --> DownloadResult
|
|
264
|
+
Downloader --> Tracker
|
|
265
|
+
Downloader --> RateLimiter
|
|
266
|
+
ProgressBar ..> Tracker : renders snapshots
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
[](https://i.ibb.co/9HYQ3WrT/pywget-architecture5.jpg)
|
|
270
|
+
|
|
271
|
+
## Behaviour details
|
|
272
|
+
|
|
273
|
+
- **Probing:** one `Range: bytes=0-0` request yields size, range support, ETag, `Last-Modified` and `Content-Disposition`.
|
|
274
|
+
- **Segments:** a file is split into `min(connections, size / 2 MiB)` parts; small files use one connection.
|
|
275
|
+
- **Existing file:** same size means skipped; different size is saved as `name.1` (wget style) unless `--overwrite`.
|
|
276
|
+
- **Partial files:** stored as `name.part` + `name.pywget`; removed on success. Changed remote file (size/ETag/mtime) restarts cleanly.
|
|
277
|
+
- **Redirects:** only http/https; `Authorization`/`Cookie` are dropped when the origin changes.
|
|
278
|
+
- **Not supported (by design):** FTP, recursive mirroring.
|
|
279
|
+
|
|
280
|
+
## Development
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
git clone https://github.com/cumulus13/pywget && cd pywget
|
|
284
|
+
pip install -e ".[dev]"
|
|
285
|
+
pytest --cov=pywget
|
|
286
|
+
ruff check pywget tests && mypy pywget
|
|
287
|
+
python -m build
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
[BSD](LICENSE)
|
|
293
|
+
|
|
294
|
+
## Author
|
|
295
|
+
|
|
296
|
+
[Hadi Cahyadi](https://github.com/cumulus13)
|
|
297
|
+
|
|
298
|
+
[](https://www.buymeacoffee.com/cumulus13)
|
|
299
|
+
[](https://ko-fi.com/cumulus13)
|
|
300
|
+
[Support me on Patreon](https://www.patreon.com/cumulus13)
|