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 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
+ [![CI](https://github.com/cumulus13/pywget/actions/workflows/ci.yml/badge.svg)](https://github.com/cumulus13/pywget/actions/workflows/ci.yml)
74
+ [![PyPI](https://img.shields.io/pypi/v/pywget.svg)](https://pypi.org/project/pywget/)
75
+ [![Python](https://img.shields.io/pypi/pyversions/pywget.svg)](https://pypi.org/project/pywget/)
76
+ [![License](https://img.shields.io/badge/license-BSD-blue.svg)](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
+ [![Architecture 1](https://i.ibb.co/mr6hxjYg/pywget-architecture1.jpg)](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
+ [![Architecture 2](https://i.ibb.co/DPgLTD6D/pywget-architecture2.jpg)](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
+ [![Architecture 3](https://i.ibb.co/YBZKFsxG/pywget-architecture3.jpg)](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
+ [![Architecture 4](https://i.ibb.co/nqGmzdpw/pywget-architecture4.jpg)](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
+ [![Architecture 5](https://i.ibb.co/9HYQ3WrT/pywget-architecture5.jpg)](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
+ [![Buy Me a Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/cumulus13)
369
+ [![Donate via Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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
+ [![CI](https://github.com/cumulus13/pywget/actions/workflows/ci.yml/badge.svg)](https://github.com/cumulus13/pywget/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/pywget.svg)](https://pypi.org/project/pywget/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/pywget.svg)](https://pypi.org/project/pywget/)
6
+ [![License](https://img.shields.io/badge/license-BSD-blue.svg)](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
+ [![Architecture 1](https://i.ibb.co/mr6hxjYg/pywget-architecture1.jpg)](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
+ [![Architecture 2](https://i.ibb.co/DPgLTD6D/pywget-architecture2.jpg)](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
+ [![Architecture 3](https://i.ibb.co/YBZKFsxG/pywget-architecture3.jpg)](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
+ [![Architecture 4](https://i.ibb.co/nqGmzdpw/pywget-architecture4.jpg)](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
+ [![Architecture 5](https://i.ibb.co/9HYQ3WrT/pywget-architecture5.jpg)](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
+ [![Buy Me a Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/cumulus13)
299
+ [![Donate via Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/cumulus13)
300
+ [Support me on Patreon](https://www.patreon.com/cumulus13)