|
| 1 | +import http.client |
| 2 | +import json |
| 3 | +import time |
| 4 | +import traceback |
| 5 | +import urllib.request |
| 6 | +from collections import deque |
| 7 | +from dataclasses import dataclass |
| 8 | +from pathlib import Path |
| 9 | + |
| 10 | + |
| 11 | +@dataclass |
| 12 | +class Asset(object): |
| 13 | + name: str |
| 14 | + keywords: str |
| 15 | + url: str |
| 16 | + size: int |
| 17 | + |
| 18 | + |
| 19 | +def get_assets(): |
| 20 | + api = ( |
| 21 | + "https://api.github.com/repos/indygreg/python-build-standalone/releases/latest" |
| 22 | + ) |
| 23 | + req = urllib.request.Request(api, headers={"User-Agent": "Chrome"}) |
| 24 | + with urllib.request.urlopen(url=req, timeout=10) as resp: |
| 25 | + data = json.loads(resp.read()) |
| 26 | + # print(data) |
| 27 | + urls = [ |
| 28 | + Asset( |
| 29 | + i["name"], |
| 30 | + [k.replace(f"+{data['name']}", "") for k in i["name"].split("-", 6)], |
| 31 | + i["browser_download_url"], |
| 32 | + i["size"], |
| 33 | + ) |
| 34 | + for i in data["assets"] |
| 35 | + if not i["name"].endswith(".sha256") and i["name"].count("-") >= 6 |
| 36 | + ] |
| 37 | + return urls |
| 38 | + |
| 39 | + |
| 40 | +def get_time(): |
| 41 | + return time.strftime("%H:%M:%S") |
| 42 | + |
| 43 | + |
| 44 | +def download_python(): |
| 45 | + """Download python portable interpreter from https://github.com/indygreg/python-build-standalone/releases. `python -m morebuiltins.download_python` |
| 46 | +
|
| 47 | + λ python -m morebuiltins.download_python |
| 48 | + [10:56:17] Checking https://api.github.com/repos/indygreg/python-build-standalone/releases/latest |
| 49 | + [10:56:19] View the rules: |
| 50 | + https://gregoryszorc.com/docs/python-build-standalone/main/running.html#obtaining-distributions |
| 51 | +
|
| 52 | + [10:56:19] Got 290 urls from github. |
| 53 | +
|
| 54 | + [290] Enter keywords (can be int index or partial match, defaults to 0): |
| 55 | + 0. windows |
| 56 | + 1. linux |
| 57 | + 2. darwin |
| 58 | + 0 |
| 59 | + [10:56:24] Filt with keyword: "windows". 290 => 40 |
| 60 | +
|
| 61 | + [40] Enter keywords (can be int index or partial match, defaults to 0): |
| 62 | + 0. 3.12.3 |
| 63 | + 1. 3.11.9 |
| 64 | + 2. 3.10.14 |
| 65 | + 3. 3.9.19 |
| 66 | + 4. 3.8.19 |
| 67 | +
|
| 68 | + [10:56:25] Filt with keyword: "3.12.3". 40 => 8 |
| 69 | +
|
| 70 | + [8] Enter keywords (can be int index or partial match, defaults to 0): |
| 71 | + 0. x86_64 |
| 72 | + 1. i686 |
| 73 | +
|
| 74 | + [10:56:28] Filt with keyword: "x86_64". 8 => 4 |
| 75 | +
|
| 76 | + [4] Enter keywords (can be int index or partial match, defaults to 0): |
| 77 | + 0. shared-pgo-full.tar.zst |
| 78 | + 1. shared-install_only.tar.gz |
| 79 | + 2. pgo-full.tar.zst |
| 80 | + 3. install_only.tar.gz |
| 81 | + 3 |
| 82 | + [10:56:33] Filt with keyword: "install_only.tar.gz". 4 => 1 |
| 83 | + [10:56:33] Download URL: 39.1 MB |
| 84 | + https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-x86_64-pc-windows-msvc-install_only.tar.gz |
| 85 | + File path to save(defaults to `./cpython-3.12.3+20240415-x86_64-pc-windows-msvc-install_only.tar.gz`)? |
| 86 | + or `q` to exit. |
| 87 | +
|
| 88 | + [10:56:38] Start downloading... |
| 89 | + https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-x86_64-pc-windows-msvc-install_only.tar.gz |
| 90 | + D:\github\morebuiltins\morebuiltins\download_python\cpython-3.12.3+20240415-x86_64-pc-windows-msvc-install_only.tar.gz |
| 91 | + [10:56:44] Downloading: 39.12 / 39.12 MB | 100.00% | 11.3 MB/s | 0s |
| 92 | + [10:56:44] Download complete. |
| 93 | +""" |
| 94 | + print( |
| 95 | + f"[{get_time()}] Checking https://api.github.com/repos/indygreg/python-build-standalone/releases/latest", |
| 96 | + flush=True, |
| 97 | + ) |
| 98 | + assets = get_assets() |
| 99 | + print( |
| 100 | + f"[{get_time()}] View the rules:\nhttps://gregoryszorc.com/docs/python-build-standalone/main/running.html#obtaining-distributions\n", |
| 101 | + flush=True, |
| 102 | + ) |
| 103 | + print(f"[{get_time()}] Got {len(assets)} urls from github.") |
| 104 | + |
| 105 | + def sort_key(s): |
| 106 | + try: |
| 107 | + return tuple(map(int, s.split("."))) |
| 108 | + except ValueError: |
| 109 | + return s |
| 110 | + |
| 111 | + indexs = [4, 1, 5, 2, 3, 0, 6] |
| 112 | + for index in indexs: |
| 113 | + try: |
| 114 | + to_filt = sorted( |
| 115 | + {i.keywords[index] for i in assets}, key=sort_key, reverse=True |
| 116 | + ) |
| 117 | + except IndexError: |
| 118 | + continue |
| 119 | + if len(to_filt) == 1: |
| 120 | + continue |
| 121 | + choices = "\n".join((f"{idx}. {ii}" for idx, ii in enumerate(to_filt, 0))) |
| 122 | + arg = input( |
| 123 | + f"\n[{len(assets)}] Enter keywords (can be int index or partial match, defaults to 0):\n{choices}\n" |
| 124 | + ) |
| 125 | + if not arg: |
| 126 | + arg = to_filt[0] |
| 127 | + elif arg.isdigit(): |
| 128 | + arg = to_filt[int(arg)] |
| 129 | + old = len(assets) |
| 130 | + temp = [i for i in assets if arg == i.keywords[index]] |
| 131 | + if temp: |
| 132 | + assets = temp |
| 133 | + else: |
| 134 | + assets = [i for i in assets if arg in i.keywords[index]] |
| 135 | + print( |
| 136 | + f'[{get_time()}] Filt with keyword: "{arg}".', |
| 137 | + old, |
| 138 | + "=>", |
| 139 | + len(assets), |
| 140 | + flush=True, |
| 141 | + ) |
| 142 | + while len(assets) > 1: |
| 143 | + names = "\n".join(i.name for i in assets) |
| 144 | + arg = input(f"Enter keyword to reduce the list (partial match):\n{names}\n") |
| 145 | + assets = [i for i in assets if arg in i.name] |
| 146 | + if not assets: |
| 147 | + input("No match, press enter to exit.") |
| 148 | + return |
| 149 | + asset = assets[0] |
| 150 | + download_url = asset.url |
| 151 | + total_size = asset.size |
| 152 | + print( |
| 153 | + f"[{get_time()}] Download URL:", |
| 154 | + round(total_size / 1024**2, 1), |
| 155 | + "MB", |
| 156 | + ) |
| 157 | + print(download_url, flush=True) |
| 158 | + target = ( |
| 159 | + input( |
| 160 | + f"File path to save(defaults to `./{asset.name}`)?\nor `q` to exit.\n" |
| 161 | + ).lower() |
| 162 | + or asset.name |
| 163 | + ) |
| 164 | + if target == "q": |
| 165 | + return |
| 166 | + target_path = Path(target) |
| 167 | + target_path.unlink(missing_ok=True) |
| 168 | + print(f"[{get_time()}] Start downloading...") |
| 169 | + print(download_url) |
| 170 | + print(target_path.absolute(), flush=True) |
| 171 | + records = deque(maxlen=1000) |
| 172 | + |
| 173 | + def reporthook(blocknum, blocksize, totalsize): |
| 174 | + if totalsize < 0: |
| 175 | + totalsize = total_size |
| 176 | + _done = blocknum * blocksize |
| 177 | + if not _done: |
| 178 | + return |
| 179 | + percent = 100.0 * _done / totalsize |
| 180 | + total = totalsize / 1024 / 1024 |
| 181 | + done = _done / 1024 / 1024 or total |
| 182 | + if percent > 100: |
| 183 | + percent = 100 |
| 184 | + now = time.time() |
| 185 | + record = (now, _done) |
| 186 | + records.appendleft(record) |
| 187 | + timeleft = "-" |
| 188 | + _speed = 0 |
| 189 | + if len(records) >= 2: |
| 190 | + for record in records: |
| 191 | + if now - record[0] > 1: |
| 192 | + break |
| 193 | + time_diff = now - record[0] |
| 194 | + if time_diff: |
| 195 | + _speed = round((_done - record[1]) / time_diff, 1) |
| 196 | + secs = (totalsize - _done) / _speed |
| 197 | + if secs > 60: |
| 198 | + timeleft = f"{int(secs / 60)}:{int(secs % 60):02}" |
| 199 | + else: |
| 200 | + timeleft = f"{int(secs)}s" |
| 201 | + if _speed > 1024**2: |
| 202 | + speed = f"{round(_speed / 1024**2, 1)} MB/s" |
| 203 | + elif _speed > 1024: |
| 204 | + speed = f"{round(_speed / 1024, 1)} KB/s" |
| 205 | + else: |
| 206 | + speed = f"{round(_speed, 1)} B/s" |
| 207 | + print( |
| 208 | + f"[{get_time()}] Downloading: {done:.2f} / {total:.2f} MB | {percent:.2f}% | {speed} | {timeleft} {' ' * 10}", |
| 209 | + end="\r", |
| 210 | + flush=True, |
| 211 | + ) |
| 212 | + |
| 213 | + temp_path = target_path.with_suffix(".tmp") |
| 214 | + for _ in range(3): |
| 215 | + try: |
| 216 | + urllib.request.urlretrieve( |
| 217 | + download_url, temp_path.absolute().as_posix(), reporthook=reporthook |
| 218 | + ) |
| 219 | + temp_path.rename(target_path) |
| 220 | + print( |
| 221 | + f"\n[{get_time()}] Download complete.", |
| 222 | + flush=True, |
| 223 | + ) |
| 224 | + break |
| 225 | + except http.client.RemoteDisconnected: |
| 226 | + continue |
| 227 | + except KeyboardInterrupt: |
| 228 | + temp_path.unlink(missing_ok=True) |
| 229 | + print() |
| 230 | + print(f"\n[{get_time()}] Download canceled.", flush=True) |
| 231 | + return |
| 232 | + except Exception: |
| 233 | + print() |
| 234 | + traceback.print_exc() |
| 235 | + temp_path.unlink(missing_ok=True) |
| 236 | + break |
| 237 | + print("Press enter to exit.", flush=True) |
| 238 | + input() |
| 239 | + |
| 240 | + |
| 241 | +if __name__ == "__main__": |
| 242 | + download_python() |
0 commit comments