pywget 2.0.2__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # File: pywget/__init__.py
4
+ # Author: Hadi Cahyadi <cumulus13@gmail.com>
5
+ # Date: 2026-10-25
6
+ # Description: pywget - a robust, resumable, multi-connection wget alternative and library.
7
+ # License: MIT
8
+
9
+ """pywget - a robust, resumable, multi-connection wget alternative and library."""
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import Any, Union
15
+
16
+ from .__version__ import __version__
17
+ from .core import Downloader, DownloadResult, Options
18
+ from .errors import (
19
+ ChecksumError,
20
+ DownloadCancelled,
21
+ HTTPStatusError,
22
+ InvalidURLError,
23
+ NetworkError,
24
+ PywgetError,
25
+ StorageError,
26
+ )
27
+ from .progress import ProgressBar, ProgressInfo
28
+
29
+ __all__ = [
30
+ "__version__",
31
+ "download",
32
+ "Downloader",
33
+ "DownloadResult",
34
+ "Options",
35
+ "ProgressBar",
36
+ "ProgressInfo",
37
+ "PywgetError",
38
+ "InvalidURLError",
39
+ "NetworkError",
40
+ "HTTPStatusError",
41
+ "ChecksumError",
42
+ "StorageError",
43
+ "DownloadCancelled",
44
+ ]
45
+
46
+
47
+ def download(url: str, path: Union[str, Path] = ".", *, progress: bool = False, **options: Any) -> Path:
48
+ """Download *url* into directory *path*; return the final file path.
49
+
50
+ Keyword options are those of :class:`Options` (``connections``, ``retries``,
51
+ ``checksum``, ``limit_rate`` ...). ``progress=True`` shows a terminal bar.
52
+ """
53
+ bar = ProgressBar() if progress else None
54
+ try:
55
+ return Downloader(Options(output_dir=str(path), progress=bar, **options)).download(url)
56
+ finally:
57
+ if bar is not None:
58
+ bar.close()
pywget/__main__.py ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # File: pywget/__main__.py
4
+ # Author: Hadi Cahyadi <cumulus13@gmail.com>
5
+ # Date: 2026-10-25
6
+ # Description:
7
+ # License: MIT
8
+
9
+ import sys
10
+
11
+ from .cli import main
12
+
13
+ sys.exit(main())
pywget/__version__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Single source of truth for the package version."""
2
+
3
+ __version__ = "2.0.0"
4
+ version = __version__ # backward compatible alias
pywget/cli.py ADDED
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # File: pywget/cli.py
4
+ # Author: Hadi Cahyadi <cumulus13@gmail.com>
5
+ # Date: 2026-10-25
6
+ # Description: Command line interface.
7
+ # License: MIT
8
+
9
+ """Command line interface."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import getpass
15
+ import json
16
+ import logging
17
+ import os
18
+ import signal
19
+ import sys
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from .__version__ import __version__
23
+ from .core import Downloader, Options
24
+ from .errors import PywgetError
25
+ from .progress import ProgressBar
26
+ from .utils import format_size
27
+
28
+ EPILOG = """\
29
+ examples:
30
+ pywget https://example.com/file.iso
31
+ pywget https://example.com/file.iso -p ~/Downloads -n 8
32
+ pywget -i urls.txt --limit-rate 2M --checksum sha256:<hex>
33
+ pywget URL -H "Authorization: Bearer $TOKEN" -O out.bin
34
+ cat urls.txt | pywget -i - --json
35
+
36
+ exit codes: 0 ok | 2 bad usage/URL | 3 network | 4 HTTP status | 5 checksum
37
+ 6 storage/I-O | 130 interrupted
38
+ """
39
+
40
+
41
+ def build_parser() -> argparse.ArgumentParser:
42
+ p = argparse.ArgumentParser(
43
+ prog="pywget",
44
+ description="Robust, resumable, multi-connection wget alternative.",
45
+ epilog=EPILOG,
46
+ formatter_class=argparse.RawDescriptionHelpFormatter,
47
+ )
48
+ p.add_argument("urls", nargs="*", metavar="URL", help="http(s) URLs to download")
49
+ p.add_argument("-i", "--input-file", metavar="FILE", help="read URLs from FILE ('-' = stdin)")
50
+ p.add_argument("-p", "-P", "--path", "--directory-prefix", dest="path", default=".",
51
+ metavar="DIR", help="download directory (default: .)")
52
+ p.add_argument("-O", "--output", metavar="NAME", help="output file name (single URL only)")
53
+ p.add_argument("-n", "--connections", type=int, default=4, metavar="N",
54
+ help="parallel connections per file, 1-32 (default: 4)")
55
+ p.add_argument("-r", "--retries", type=int, default=5, metavar="N",
56
+ help="retries per request/segment (default: 5)")
57
+ p.add_argument("-T", "--timeout", type=float, default=30.0, metavar="SEC",
58
+ help="network timeout in seconds (default: 30)")
59
+ p.add_argument("--limit-rate", metavar="RATE", help="max total speed, e.g. 500k, 2M")
60
+ p.add_argument("-H", "--header", action="append", default=[], metavar="'K: V'",
61
+ help="extra request header (repeatable)")
62
+ p.add_argument("-U", "--user-agent", metavar="UA", help="custom User-Agent")
63
+ p.add_argument("--user", metavar="NAME", help="HTTP basic-auth user name")
64
+ p.add_argument("--password", metavar="PASS",
65
+ help="HTTP basic-auth password (or env PYWGET_PASSWORD, or prompt)")
66
+ p.add_argument("--proxy", metavar="URL", help="proxy URL (default: environment)")
67
+ p.add_argument("--no-check-certificate", action="store_true", help="skip TLS verification")
68
+ p.add_argument("--ca-cert", metavar="FILE", help="custom CA bundle")
69
+ p.add_argument("--checksum", metavar="ALGO:HEX", help="verify downloaded file, e.g. sha256:ab12..")
70
+ p.add_argument("--overwrite", action="store_true", help="replace existing file, restart download")
71
+ p.add_argument("--no-resume", action="store_true", help="never resume partial downloads")
72
+ p.add_argument("-c", "--continue", dest="cont", action="store_true",
73
+ help="accepted for wget compatibility (resume is on by default)")
74
+ p.add_argument("--no-mtime", action="store_true", help="do not copy Last-Modified to the file")
75
+ p.add_argument("-q", "--quiet", action="store_true", help="no progress or status output")
76
+ p.add_argument("-v", "--verbose", action="store_true", help="debug logging")
77
+ p.add_argument("--json", action="store_true", help="print machine-readable results to stdout")
78
+ p.add_argument("-V", "--version", action="version", version=f"pywget {__version__}")
79
+ return p
80
+
81
+
82
+ def _read_urls(source: str) -> List[str]:
83
+ handle = sys.stdin if source == "-" else open(source, encoding="utf-8")
84
+ try:
85
+ lines = [line.strip() for line in handle]
86
+ finally:
87
+ if handle is not sys.stdin:
88
+ handle.close()
89
+ return [line for line in lines if line and not line.startswith("#")]
90
+
91
+
92
+ def _parse_headers(items: List[str]) -> Dict[str, str]:
93
+ headers: Dict[str, str] = {}
94
+ for item in items:
95
+ name, sep, value = item.partition(":")
96
+ if not sep or not name.strip():
97
+ raise ValueError(f"invalid header (expected 'Name: value'): {item!r}")
98
+ headers[name.strip()] = value.strip()
99
+ return headers
100
+
101
+
102
+ def _build_options(args: argparse.Namespace, progress: Optional[ProgressBar]) -> Options:
103
+ password = args.password or os.environ.get("PYWGET_PASSWORD")
104
+ if args.user and password is None and sys.stdin.isatty():
105
+ password = getpass.getpass(f"Password for {args.user}: ")
106
+ extra: Dict[str, Any] = {}
107
+ if args.user_agent:
108
+ extra["user_agent"] = args.user_agent
109
+ return Options(
110
+ output_dir=args.path,
111
+ connections=args.connections,
112
+ retries=args.retries,
113
+ timeout=args.timeout,
114
+ limit_rate=args.limit_rate or 0,
115
+ resume=not args.no_resume,
116
+ overwrite=args.overwrite,
117
+ preserve_mtime=not args.no_mtime,
118
+ headers=_parse_headers(args.header),
119
+ username=args.user,
120
+ password=password,
121
+ proxy=args.proxy,
122
+ verify_ssl=not args.no_check_certificate,
123
+ ca_file=args.ca_cert,
124
+ checksum=args.checksum,
125
+ progress=progress,
126
+ **extra,
127
+ )
128
+
129
+
130
+ def _on_sigterm(signum: int, frame: Any) -> None:
131
+ raise KeyboardInterrupt
132
+
133
+
134
+ def main(argv: Optional[List[str]] = None) -> int:
135
+ parser = build_parser()
136
+ args = parser.parse_args(argv)
137
+ urls = list(args.urls)
138
+ if args.input_file:
139
+ try:
140
+ urls += _read_urls(args.input_file)
141
+ except OSError as exc:
142
+ parser.error(f"cannot read input file: {exc}")
143
+ if not urls:
144
+ parser.error("no URLs given")
145
+ if args.output and len(urls) > 1:
146
+ parser.error("--output can only be used with a single URL")
147
+
148
+ logging.basicConfig(
149
+ level=logging.DEBUG if args.verbose else logging.WARNING,
150
+ format="pywget: %(levelname)s: %(message)s",
151
+ stream=sys.stderr,
152
+ )
153
+ try:
154
+ signal.signal(signal.SIGTERM, _on_sigterm)
155
+ except (ValueError, OSError): # not in main thread / unsupported platform
156
+ pass
157
+
158
+ bar = ProgressBar(enabled=not args.quiet)
159
+ try:
160
+ options = _build_options(args, bar)
161
+ except ValueError as exc:
162
+ parser.error(str(exc))
163
+ downloader = Downloader(options)
164
+
165
+ results: List[Dict[str, Any]] = []
166
+ exit_code = 0
167
+ for url in urls:
168
+ try:
169
+ result = downloader.fetch(url, filename=args.output)
170
+ except KeyboardInterrupt:
171
+ bar.close()
172
+ print("pywget: interrupted; partial data kept, re-run to resume", file=sys.stderr)
173
+ return 130
174
+ except (PywgetError, OSError) as exc:
175
+ bar.close()
176
+ print(f"pywget: error: {exc}", file=sys.stderr)
177
+ results.append({"ok": False, "url": url, "error": str(exc)})
178
+ exit_code = exit_code or getattr(exc, "exit_code", 6)
179
+ continue
180
+ results.append({"ok": True, **result.to_dict()})
181
+ if not args.quiet:
182
+ extra = ", skipped (already complete)" if result.skipped else ""
183
+ extra += ", resumed" if result.resumed else ""
184
+ print(
185
+ f"OK {result.path} ({format_size(result.size)}, {result.elapsed:.1f}s, "
186
+ f"{format_size(result.speed)}/s, {result.connections} conn{extra})",
187
+ file=sys.stderr,
188
+ )
189
+ if args.json:
190
+ print(json.dumps(results, indent=2))
191
+ return exit_code
192
+
193
+
194
+ if __name__ == "__main__": # pragma: no cover
195
+ sys.exit(main())