Skip to content

Commit 4a27970

Browse files
committedJun 4, 2024
1 parent ace65f3 commit 4a27970

File tree

4 files changed

+256
-3
lines changed

4 files changed

+256
-3
lines changed
 

‎changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11

22
# Changelogs
33

4+
- 20234.06.04
5+
- add arg `--download-python`: interactive download standalone python interpreter (https://www.github.com/indygreg/python-build-standalone)
6+
- Refactor lazy installation module
7+
- `from zipapps.pip_install import install`
48
- 2023.09.12
59
- add `--download-pip-pyz` to download pip.pyz
610
- install pip module to win32 embeded exe

‎zipapps/__main__.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
==========================================================================='''
5555

5656
PIP_PYZ_URL = 'https://bootstrap.pypa.io/pip/pip.pyz'
57+
DOWNLOAD_PYTHON_URL = 'https://www.github.com/indygreg/python-build-standalone'
5758

5859

5960
def _get_now():
@@ -362,13 +363,19 @@ def main():
362363
default='',
363364
dest='download_pip_pyz',
364365
help=f'Download pip.pyz from "{PIP_PYZ_URL}"')
365-
366+
parser.add_argument('--download-python',
367+
action='store_true',
368+
dest='download_python',
369+
help=f'Download standalone python from "{DOWNLOAD_PYTHON_URL}"')
366370
if len(sys.argv) == 1:
367371
parser.print_help()
368372
handle_win32_embeded()
369373
return
370374
args, pip_args = parser.parse_known_args()
371-
if args.download_pip_pyz:
375+
if args.download_python:
376+
from .download_python import download_python
377+
return download_python()
378+
elif args.download_pip_pyz:
372379
return download_pip_pyz(args.download_pip_pyz)
373380
if args.quite_mode:
374381
ZipApp.LOGGING = False

‎zipapps/download_python.py

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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()

‎zipapps/main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pkgutil import get_data
1616
from zipfile import ZIP_DEFLATED, ZIP_STORED, BadZipFile, ZipFile
1717

18-
__version__ = '2024.04.22'
18+
__version__ = '2024.06.04'
1919

2020

2121
def get_pip_main(ensurepip_root=None):

0 commit comments

Comments
 (0)
Failed to load comments.