python-urlopen 0.0.1__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.
LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ChenyangGao <https://github.com/ChenyangGao>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ChenyangGao <https://github.com/ChenyangGao>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-urlopen
3
+ Version: 0.0.1
4
+ Summary: Python urlopen wrapper.
5
+ Home-page: https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-urlopen
6
+ License: MIT
7
+ Keywords: urlopen
8
+ Author: ChenyangGao
9
+ Author-email: wosiwujm@gmail.com
10
+ Requires-Python: >=3.10,<4.0
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Dist: http_response
25
+ Requires-Dist: python-filewrap
26
+ Project-URL: Repository, https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-urlopen
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Python urlopen wrapper.
30
+
31
+ ## Installation
32
+
33
+ You can install via [pypi](https://pypi.org/project/python-urlopen/)
34
+
35
+ ```console
36
+ pip install -U python-urlopen
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from urlopen import urlopen
43
+ ```
44
+
45
+
@@ -0,0 +1,8 @@
1
+ LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
2
+ urlopen/__init__.py,sha256=LE2hLt-vs6UTcfmShqEYDh22952XwobE7OfLfMBBCe0,7231
3
+ urlopen/__main__.py,sha256=Wj5QLDW2LccpOwBD16pKPOaSj-9r4njgyRIqrlD4aho,2768
4
+ urlopen/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ python_urlopen-0.0.1.dist-info/LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
6
+ python_urlopen-0.0.1.dist-info/METADATA,sha256=_lOD6lD-C0-qrfTSItH8WoMYprOSZ8bxsKaERDYLjjg,1395
7
+ python_urlopen-0.0.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
8
+ python_urlopen-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.8.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
urlopen/__init__.py ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+
4
+ __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
+ __version__ = (0, 0, 1)
6
+ __all__ = ["urlopen", "download"]
7
+
8
+ import errno
9
+
10
+ from collections.abc import Callable, Generator, Mapping, Sequence
11
+ from copy import copy
12
+ from http.client import HTTPResponse
13
+ from inspect import isgenerator
14
+ from json import dumps
15
+ from os import fsdecode, fstat, makedirs, PathLike
16
+ from os.path import abspath, dirname, isdir, join as joinpath
17
+ from shutil import COPY_BUFSIZE # type: ignore
18
+ from ssl import SSLContext, _create_unverified_context
19
+ from typing import cast, Any, Optional
20
+ from urllib.parse import urlencode, urlsplit
21
+ from urllib.request import build_opener, HTTPSHandler, OpenerDirector, Request
22
+
23
+ from filewrap import bio_skip_bytes, SupportsRead, SupportsWrite
24
+ from http_response import get_filename, get_length, is_chunked, is_range_request
25
+
26
+
27
+ if "__del__" not in HTTPResponse.__dict__:
28
+ setattr(HTTPResponse, "__del__", HTTPResponse.close)
29
+
30
+
31
+ def urlopen(
32
+ url: str | Request,
33
+ params: Optional[str | Mapping | Sequence[tuple[Any, Any]]] = None,
34
+ data: Optional[bytes | str | Mapping | Sequence[tuple[Any, Any]]] = None,
35
+ json: Any = None,
36
+ headers: dict[str, str] = {"User-agent": ""},
37
+ method: str = "GET",
38
+ timeout: Optional[int | float] = None,
39
+ proxy: Optional[tuple[str, str]] = None,
40
+ opener: OpenerDirector = build_opener(HTTPSHandler(context=_create_unverified_context())),
41
+ context: Optional[SSLContext] = None,
42
+ origin: Optional[str] = None,
43
+ ) -> HTTPResponse:
44
+ if isinstance(url, str) and not urlsplit(url).scheme:
45
+ if origin:
46
+ if not url.startswith("/"):
47
+ url = "/" + url
48
+ url = origin + url
49
+ if params:
50
+ if not isinstance(params, str):
51
+ params = urlencode(params)
52
+ params = cast(Optional[str], params)
53
+ if json is not None:
54
+ if isinstance(json, bytes):
55
+ data = json
56
+ else:
57
+ data = dumps(json).encode("utf-8")
58
+ headers = {**headers, "Content-type": "application/json"}
59
+ elif data is not None:
60
+ if isinstance(data, bytes):
61
+ pass
62
+ elif isinstance(data, str):
63
+ data = data.encode("utf-8")
64
+ else:
65
+ data = urlencode(data).encode("latin-1")
66
+ data = cast(Optional[bytes], data)
67
+ if isinstance(url, Request):
68
+ req = url
69
+ if params:
70
+ req.full_url += "?&"["?" in req.full_url] + params
71
+ if headers:
72
+ for key, val in headers.items():
73
+ req.add_header(key, val)
74
+ if data is not None:
75
+ req.data = data
76
+ req.method = method.upper()
77
+ else:
78
+ if params:
79
+ url += "?&"["?" in url] + params
80
+ req = Request(url, data, headers=headers, method=method.upper())
81
+ if proxy:
82
+ req.set_proxy(*proxy)
83
+ if context:
84
+ opener = build_opener(HTTPSHandler(context=context))
85
+ if timeout is None:
86
+ return opener.open(req)
87
+ else:
88
+ return opener.open(req, timeout=timeout)
89
+
90
+
91
+ def download(
92
+ url: str,
93
+ file: bytes | str | PathLike | SupportsWrite[bytes] = "",
94
+ resume: bool = False,
95
+ chunksize: int = COPY_BUFSIZE,
96
+ headers: Optional[dict[str, str]] = None,
97
+ make_reporthook: Optional[Callable[[Optional[int]], Callable[[int], Any] | Generator[int, Any, Any]]] = None,
98
+ **urlopen_kwargs,
99
+ ) -> str | SupportsWrite[bytes]:
100
+ """Download a URL into a file.
101
+
102
+ Example::
103
+
104
+ 1. use `make_reporthook` to show progress:
105
+
106
+ You can use the following function to show progress for the download task
107
+
108
+ .. code: python
109
+
110
+ from time import perf_counter
111
+
112
+ def progress(total=None):
113
+ read_num = 0
114
+ start_t = perf_counter()
115
+ while True:
116
+ read_num += yield
117
+ speed = read_num / 1024 / 1024 / (perf_counter() - start_t)
118
+ print(f"\r\x1b[K{read_num} / {total} | {speed:.2f} MB/s", end="", flush=True)
119
+
120
+ Or use the following function for more real-time speed
121
+
122
+ .. code: python
123
+
124
+ from collections import deque
125
+ from time import perf_counter
126
+
127
+ def progress(total=None):
128
+ dq = deque(maxlen=64)
129
+ read_num = 0
130
+ dq.append((read_num, perf_counter()))
131
+ while True:
132
+ read_num += yield
133
+ cur_t = perf_counter()
134
+ speed = (read_num - dq[0][0]) / 1024 / 1024 / (cur_t - dq[0][1])
135
+ print(f"\r\x1b[K{read_num} / {total} | {speed:.2f} MB/s", end="", flush=True)
136
+ dq.append((read_num, cur_t))
137
+ """
138
+ if headers:
139
+ headers = {**headers, "Accept-encoding": "identity"}
140
+ else:
141
+ headers = {"Accept-encoding": "identity"}
142
+
143
+ if chunksize <= 0:
144
+ chunksize = COPY_BUFSIZE
145
+
146
+ resp: HTTPResponse = urlopen(url, headers=headers, **urlopen_kwargs)
147
+ length = get_length(resp)
148
+ if length == 0 and is_chunked(resp):
149
+ length = None
150
+
151
+ fdst: SupportsWrite[bytes]
152
+ if hasattr(file, "write"):
153
+ file = fdst = cast(SupportsWrite[bytes], file)
154
+ else:
155
+ file = abspath(fsdecode(file))
156
+ if isdir(file):
157
+ file = joinpath(file, get_filename(resp, "download"))
158
+ try:
159
+ fdst = open(file, "ab" if resume else "wb")
160
+ except FileNotFoundError:
161
+ makedirs(dirname(file), exist_ok=True)
162
+ fdst = open(file, "ab" if resume else "wb")
163
+
164
+ filesize = 0
165
+ if resume:
166
+ try:
167
+ filesize = fstat(fdst.fileno()).st_size # type: ignore
168
+ except (AttributeError, OSError):
169
+ pass
170
+ else:
171
+ if filesize == length:
172
+ return file
173
+ if filesize and is_range_request(resp):
174
+ if filesize == length:
175
+ return file
176
+ elif length is not None and filesize > length:
177
+ raise OSError(errno.EIO, f"file {file!r} is larger than url {url!r}: {filesize} > {length} (in bytes)")
178
+
179
+ if make_reporthook:
180
+ reporthook = make_reporthook(length)
181
+ if isgenerator(reporthook):
182
+ next(reporthook)
183
+ reporthook = reporthook.send
184
+ reporthook = cast(Callable[[int], Any], reporthook)
185
+ else:
186
+ reporthook = None
187
+
188
+ try:
189
+ if filesize:
190
+ if is_range_request(resp):
191
+ resp.close()
192
+ resp = urlopen(url, headers={**headers, "Range": "bytes=%d-" % filesize}, **urlopen_kwargs)
193
+ if not is_range_request(resp):
194
+ raise OSError(errno.EIO, f"range request failed: {url!r}")
195
+ if reporthook:
196
+ reporthook(filesize)
197
+ elif resume:
198
+ bio_skip_bytes(resp, filesize, callback=reporthook)
199
+
200
+ fsrc_read = resp.read
201
+ fdst_write = fdst.write
202
+ while (chunk := fsrc_read(chunksize)):
203
+ fdst_write(chunk)
204
+ if reporthook:
205
+ reporthook(len(chunk))
206
+ finally:
207
+ resp.close()
208
+
209
+ return file
210
+
urlopen/__main__.py ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+
4
+ __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
+ __doc__ = "python url downloader"
6
+
7
+ from argparse import ArgumentParser, RawTextHelpFormatter
8
+
9
+ parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)
10
+ parser.add_argument("urls", nargs="*", metavar="url", help="URL(s) to be downloaded (one URL per line), if omitted, read from stdin")
11
+ parser.add_argument("-d", "--savedir", default="", help="directory to the downloading files")
12
+ parser.add_argument("-r", "--resume", action="store_true", help="skip downloaded data")
13
+ parser.add_argument("-hs", "--headers", help="dictionary of HTTP Headers to send with")
14
+ parser.add_argument("-v", "--version", action="store_true", help="print the current version")
15
+ args = parser.parse_args()
16
+ if args.version:
17
+ from urlopen import __version__
18
+ print(".".join(map(str, __version__)))
19
+ raise SystemExit(0)
20
+
21
+ from collections import deque
22
+ from os import makedirs
23
+ from time import perf_counter
24
+
25
+ from urlopen import download
26
+
27
+ def headers_str_to_dict(headers: str, /) -> dict[str, str]:
28
+ return dict(
29
+ header.split(": ", 1)
30
+ for header in headers.strip("\n").split("\n")
31
+ )
32
+
33
+ def progress(total=None):
34
+ dq: deque[tuple[int, float]] = deque(maxlen=64)
35
+ read_num = 0
36
+ dq.append((read_num, perf_counter()))
37
+ while True:
38
+ read_num += yield
39
+ cur_t = perf_counter()
40
+ speed = (read_num - dq[0][0]) / 1024 / 1024 / (cur_t - dq[0][1])
41
+ if total:
42
+ percentage = read_num / total * 100
43
+ print(f"\r\x1b[K{read_num} / {total} | {speed:.2f} MB/s | {percentage:.2f} %", end="", flush=True)
44
+ else:
45
+ print(f"\r\x1b[K{read_num} | {speed:.2f} MB/s", end="", flush=True)
46
+ dq.append((read_num, cur_t))
47
+
48
+ urls = args.urls
49
+ if not urls:
50
+ from sys import stdin
51
+ urls = (l.removesuffix("\n") for l in stdin)
52
+ savedir = args.savedir
53
+ if savedir:
54
+ makedirs(savedir, exist_ok=True)
55
+
56
+ try:
57
+ headers = args.headers
58
+ if headers is not None:
59
+ headers = headers_str_to_dict(headers)
60
+ for url in urls:
61
+ if not url:
62
+ continue
63
+ try:
64
+ file = download(
65
+ url,
66
+ savedir,
67
+ resume=args.resume,
68
+ make_reporthook=progress,
69
+ headers=headers,
70
+ )
71
+ print(f"\r\x1b[K\x1b[1;32mDOWNLOADED\x1b[0m \x1b[4;34m{url!r}\x1b[0m\n |_ ⏬ \x1b[4;34m{file!r}\x1b[0m")
72
+ except BaseException as e:
73
+ print(f"\r\x1b[K\x1b[1;31mERROR\x1b[0m \x1b[4;34m{url!r}\x1b[0m\n |_ 🙅 \x1b[1;31m{type(e).__qualname__}\x1b[0m: {e}")
74
+ except (EOFError, KeyboardInterrupt):
75
+ pass
76
+ except BrokenPipeError:
77
+ from sys import stderr
78
+ stderr.close()
79
+
urlopen/py.typed ADDED
File without changes