pystand 2.16__tar.gz → 2.17__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.
- {pystand-2.16/pystand.egg-info → pystand-2.17}/PKG-INFO +8 -4
- {pystand-2.16 → pystand-2.17}/README.md +5 -2
- {pystand-2.16 → pystand-2.17}/pyproject.toml +2 -1
- {pystand-2.16 → pystand-2.17/pystand.egg-info}/PKG-INFO +8 -4
- {pystand-2.16 → pystand-2.17}/pystand.egg-info/requires.txt +3 -0
- {pystand-2.16 → pystand-2.17}/pystand.py +63 -31
- {pystand-2.16 → pystand-2.17}/.gitignore +0 -0
- {pystand-2.16 → pystand-2.17}/Makefile +0 -0
- {pystand-2.16 → pystand-2.17}/pystand.egg-info/SOURCES.txt +0 -0
- {pystand-2.16 → pystand-2.17}/pystand.egg-info/dependency_links.txt +0 -0
- {pystand-2.16 → pystand-2.17}/pystand.egg-info/entry_points.txt +0 -0
- {pystand-2.16 → pystand-2.17}/pystand.egg-info/top_level.txt +0 -0
- {pystand-2.16 → pystand-2.17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pystand
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.17
|
4
4
|
Summary: Install Python versions from python-build-standalone project
|
5
5
|
Author-email: Mark Blakeney <mark.blakeney@bullet-systems.net>
|
6
6
|
License-Expression: GPL-3.0-or-later
|
@@ -13,7 +13,8 @@ Requires-Dist: argcomplete
|
|
13
13
|
Requires-Dist: packaging
|
14
14
|
Requires-Dist: platformdirs
|
15
15
|
Requires-Dist: pygithub
|
16
|
-
Requires-Dist:
|
16
|
+
Requires-Dist: certifi
|
17
|
+
Requires-Dist: zstandard; python_version < "3.14"
|
17
18
|
|
18
19
|
## PYSTAND - Install Python Versions From The Python-Build-Standalone Project
|
19
20
|
[](https://pypi.org/project/pystand/)
|
@@ -130,7 +131,7 @@ Type `pystand` or `pystand -h` to view the usage summary:
|
|
130
131
|
usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
|
131
132
|
[-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
|
132
133
|
[--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
|
133
|
-
[-V]
|
134
|
+
[--cert {system,certifi,none}] [-V]
|
134
135
|
{install,update,upgrade,remove,uninstall,list,show,path,cache} ...
|
135
136
|
|
136
137
|
Command line tool to download, install, and update pre-built Python versions
|
@@ -162,6 +163,9 @@ options:
|
|
162
163
|
optional Github access token. Can specify to reduce
|
163
164
|
rate limiting.
|
164
165
|
--no-strip do not strip downloaded binaries
|
166
|
+
--cert {system,certifi,none}
|
167
|
+
specify which SSL certificates to use for HTTPS
|
168
|
+
requests. Default="system"
|
165
169
|
-V, --version just show pystand version
|
166
170
|
|
167
171
|
Commands:
|
@@ -598,7 +602,7 @@ either version 3 of the License, or any later version. This program is
|
|
598
602
|
distributed in the hope that it will be useful, but WITHOUT ANY
|
599
603
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
600
604
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License at
|
601
|
-
<
|
605
|
+
<https://en.wikipedia.org/wiki/GNU_General_Public_License> for more details.
|
602
606
|
|
603
607
|
[pystand]: https://github.com/bulletmark/pystand
|
604
608
|
[pbs]: https://github.com/astral-sh/python-build-standalone
|
@@ -113,7 +113,7 @@ Type `pystand` or `pystand -h` to view the usage summary:
|
|
113
113
|
usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
|
114
114
|
[-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
|
115
115
|
[--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
|
116
|
-
[-V]
|
116
|
+
[--cert {system,certifi,none}] [-V]
|
117
117
|
{install,update,upgrade,remove,uninstall,list,show,path,cache} ...
|
118
118
|
|
119
119
|
Command line tool to download, install, and update pre-built Python versions
|
@@ -145,6 +145,9 @@ options:
|
|
145
145
|
optional Github access token. Can specify to reduce
|
146
146
|
rate limiting.
|
147
147
|
--no-strip do not strip downloaded binaries
|
148
|
+
--cert {system,certifi,none}
|
149
|
+
specify which SSL certificates to use for HTTPS
|
150
|
+
requests. Default="system"
|
148
151
|
-V, --version just show pystand version
|
149
152
|
|
150
153
|
Commands:
|
@@ -581,7 +584,7 @@ either version 3 of the License, or any later version. This program is
|
|
581
584
|
distributed in the hope that it will be useful, but WITHOUT ANY
|
582
585
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
583
586
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License at
|
584
|
-
<
|
587
|
+
<https://en.wikipedia.org/wiki/GNU_General_Public_License> for more details.
|
585
588
|
|
586
589
|
[pystand]: https://github.com/bulletmark/pystand
|
587
590
|
[pbs]: https://github.com/astral-sh/python-build-standalone
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pystand
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.17
|
4
4
|
Summary: Install Python versions from python-build-standalone project
|
5
5
|
Author-email: Mark Blakeney <mark.blakeney@bullet-systems.net>
|
6
6
|
License-Expression: GPL-3.0-or-later
|
@@ -13,7 +13,8 @@ Requires-Dist: argcomplete
|
|
13
13
|
Requires-Dist: packaging
|
14
14
|
Requires-Dist: platformdirs
|
15
15
|
Requires-Dist: pygithub
|
16
|
-
Requires-Dist:
|
16
|
+
Requires-Dist: certifi
|
17
|
+
Requires-Dist: zstandard; python_version < "3.14"
|
17
18
|
|
18
19
|
## PYSTAND - Install Python Versions From The Python-Build-Standalone Project
|
19
20
|
[](https://pypi.org/project/pystand/)
|
@@ -130,7 +131,7 @@ Type `pystand` or `pystand -h` to view the usage summary:
|
|
130
131
|
usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
|
131
132
|
[-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
|
132
133
|
[--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
|
133
|
-
[-V]
|
134
|
+
[--cert {system,certifi,none}] [-V]
|
134
135
|
{install,update,upgrade,remove,uninstall,list,show,path,cache} ...
|
135
136
|
|
136
137
|
Command line tool to download, install, and update pre-built Python versions
|
@@ -162,6 +163,9 @@ options:
|
|
162
163
|
optional Github access token. Can specify to reduce
|
163
164
|
rate limiting.
|
164
165
|
--no-strip do not strip downloaded binaries
|
166
|
+
--cert {system,certifi,none}
|
167
|
+
specify which SSL certificates to use for HTTPS
|
168
|
+
requests. Default="system"
|
165
169
|
-V, --version just show pystand version
|
166
170
|
|
167
171
|
Commands:
|
@@ -598,7 +602,7 @@ either version 3 of the License, or any later version. This program is
|
|
598
602
|
distributed in the hope that it will be useful, but WITHOUT ANY
|
599
603
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
600
604
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License at
|
601
|
-
<
|
605
|
+
<https://en.wikipedia.org/wiki/GNU_General_Public_License> for more details.
|
602
606
|
|
603
607
|
[pystand]: https://github.com/bulletmark/pystand
|
604
608
|
[pbs]: https://github.com/astral-sh/python-build-standalone
|
@@ -13,6 +13,7 @@ import platform
|
|
13
13
|
import re
|
14
14
|
import shlex
|
15
15
|
import shutil
|
16
|
+
import ssl
|
16
17
|
import sys
|
17
18
|
import time
|
18
19
|
from argparse import ArgumentParser, Namespace
|
@@ -20,6 +21,7 @@ from collections import defaultdict
|
|
20
21
|
from datetime import date, datetime
|
21
22
|
from pathlib import Path
|
22
23
|
from typing import Any, Iterable, Iterator
|
24
|
+
from urllib.request import urlopen
|
23
25
|
|
24
26
|
import argcomplete
|
25
27
|
import platformdirs
|
@@ -45,10 +47,13 @@ DISTRIBUTIONS = {
|
|
45
47
|
('Linux', 'armv8l'): 'armv7-unknown-linux-gnueabihf-install_only_stripped',
|
46
48
|
('Darwin', 'x86_64'): 'x86_64-apple-darwin-install_only_stripped',
|
47
49
|
('Darwin', 'aarch64'): 'aarch64-apple-darwin-install_only_stripped',
|
50
|
+
('Darwin', 'arm64'): 'aarch64-apple-darwin-install_only_stripped',
|
48
51
|
('Windows', 'x86_64'): 'x86_64-pc-windows-msvc-shared-install_only_stripped',
|
49
52
|
('Windows', 'i686'): 'i686-pc-windows-msvc-shared-install_only_stripped',
|
50
53
|
}
|
51
54
|
|
55
|
+
CERTS = ('system', 'certifi', 'none')
|
56
|
+
|
52
57
|
|
53
58
|
def is_admin() -> bool:
|
54
59
|
"Check if we are running as root"
|
@@ -77,7 +82,7 @@ def fmt(version, release) -> str:
|
|
77
82
|
return f'{version} @ {release}'
|
78
83
|
|
79
84
|
|
80
|
-
def get_json(file: Path) -> dict:
|
85
|
+
def get_json(file: Path) -> dict[str, Any]:
|
81
86
|
from json import load
|
82
87
|
|
83
88
|
'Get JSON data from given file'
|
@@ -90,7 +95,7 @@ def get_json(file: Path) -> dict:
|
|
90
95
|
return {}
|
91
96
|
|
92
97
|
|
93
|
-
def set_json(file: Path, data: dict) -> str | None:
|
98
|
+
def set_json(file: Path, data: dict[str, Any]) -> str | None:
|
94
99
|
"Set JSON data to given file"
|
95
100
|
from json import dump
|
96
101
|
|
@@ -138,23 +143,30 @@ def rm_path(path: Path) -> None:
|
|
138
143
|
path.unlink()
|
139
144
|
|
140
145
|
|
141
|
-
def
|
142
|
-
"
|
143
|
-
|
146
|
+
def register_zst() -> None:
|
147
|
+
"Register custom zstandard unpacker"
|
148
|
+
# Python 3.14+ has built-in support for zstandard so only register custom
|
149
|
+
# handler for earlier versions
|
150
|
+
if sys.version_info < (3, 14):
|
151
|
+
|
152
|
+
def unpack_zst(filename: str, extract_dir: str) -> None:
|
153
|
+
"Unpack a zstandard compressed tar"
|
154
|
+
import tarfile
|
144
155
|
|
145
|
-
|
156
|
+
import zstandard
|
146
157
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
158
|
+
with open(filename, 'rb') as compressed:
|
159
|
+
dctx = zstandard.ZstdDecompressor()
|
160
|
+
with dctx.stream_reader(compressed) as reader:
|
161
|
+
with tarfile.open(fileobj=reader, mode='r|') as tar:
|
162
|
+
tar.extractall(path=extract_dir)
|
163
|
+
|
164
|
+
shutil.register_unpack_format('zst', ['.zst'], unpack_zst)
|
152
165
|
|
153
166
|
|
154
167
|
def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
|
155
168
|
"Fetch and unpack a release file"
|
156
169
|
from urllib.parse import unquote, urlparse
|
157
|
-
from urllib.request import urlretrieve
|
158
170
|
|
159
171
|
error = None
|
160
172
|
tmpdir = tdir.with_name(f'{tdir.name}-tmp')
|
@@ -168,7 +180,8 @@ def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
|
|
168
180
|
|
169
181
|
if not cache_file.exists():
|
170
182
|
try:
|
171
|
-
|
183
|
+
with urlopen(url, context=args._cert) as urlp, cache_file.open('wb') as fp:
|
184
|
+
shutil.copyfileobj(urlp, fp)
|
172
185
|
except Exception as e:
|
173
186
|
error = f'Failed to fetch "{url}": {e}'
|
174
187
|
|
@@ -176,7 +189,7 @@ def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
|
|
176
189
|
rm_path(cache_file)
|
177
190
|
else:
|
178
191
|
if filename.endswith('.zst'):
|
179
|
-
|
192
|
+
register_zst()
|
180
193
|
|
181
194
|
try:
|
182
195
|
shutil.unpack_archive(cache_file, tmpdir)
|
@@ -306,13 +319,12 @@ def check_release_tag(release: str) -> str | None:
|
|
306
319
|
# Note we use a simple direct URL fetch to get the latest tag info
|
307
320
|
# because it is much faster than using the GitHub API, and has no
|
308
321
|
# rate-limits.
|
309
|
-
def fetch_tags() -> Iterator[tuple[str, str]]:
|
322
|
+
def fetch_tags(args: Namespace) -> Iterator[tuple[str, str]]:
|
310
323
|
"Fetch the latest release tags from the GitHub release atom feed"
|
311
324
|
import xml.etree.ElementTree as et
|
312
|
-
from urllib.request import urlopen
|
313
325
|
|
314
326
|
try:
|
315
|
-
with urlopen(LATEST_RELEASES) as url:
|
327
|
+
with urlopen(LATEST_RELEASES, context=args._cert) as url:
|
316
328
|
data = et.parse(url).getroot()
|
317
329
|
except Exception:
|
318
330
|
sys.exit('Failed to fetch latest YYYYMMDD release atom file.')
|
@@ -325,12 +337,10 @@ def fetch_tags() -> Iterator[tuple[str, str]]:
|
|
325
337
|
yield tl, dt
|
326
338
|
|
327
339
|
|
328
|
-
def fetch_tag_latest() -> str:
|
340
|
+
def fetch_tag_latest(args: Namespace) -> str:
|
329
341
|
"Fetch the latest release tag from the GitHub"
|
330
|
-
from urllib.request import urlopen
|
331
|
-
|
332
342
|
try:
|
333
|
-
with urlopen(LATEST_RELEASE_TAG) as url:
|
343
|
+
with urlopen(LATEST_RELEASE_TAG, context=args._cert) as url:
|
334
344
|
data = url.geturl()
|
335
345
|
except Exception:
|
336
346
|
sys.exit('Failed to fetch latest YYYYMMDD release tag.')
|
@@ -351,14 +361,14 @@ def get_release_tag(args: Namespace) -> str:
|
|
351
361
|
if time.time() < (stat.st_mtime + int(args.cache_minutes * 60)):
|
352
362
|
return args._latest_release.read_text().strip()
|
353
363
|
|
354
|
-
if not (tag := fetch_tag_latest()):
|
364
|
+
if not (tag := fetch_tag_latest(args)):
|
355
365
|
sys.exit('Latest YYYYMMDD release tag timestamp file is unavailable.')
|
356
366
|
|
357
367
|
args._latest_release.write_text(tag + '\n')
|
358
368
|
return tag
|
359
369
|
|
360
370
|
|
361
|
-
def add_file(files: dict, tag: str, name: str, url: str) -> None:
|
371
|
+
def add_file(files: dict[str, Any], tag: str, name: str, url: str) -> None:
|
362
372
|
"Extract the implementation, version, and architecture from a filename"
|
363
373
|
if name.endswith('.tar.zst'):
|
364
374
|
name = name[:-8]
|
@@ -386,7 +396,7 @@ def add_file(files: dict, tag: str, name: str, url: str) -> None:
|
|
386
396
|
vers[ver][arch] = url
|
387
397
|
|
388
398
|
|
389
|
-
def get_release_files(args, tag
|
399
|
+
def get_release_files(args, tag) -> dict[str, Any]:
|
390
400
|
"Return the release files for the given tag"
|
391
401
|
# Look for tag data in our release cache
|
392
402
|
jfile = args._releases / tag
|
@@ -414,7 +424,7 @@ def get_release_files(args, tag, implementation: str | None = None) -> dict:
|
|
414
424
|
if error := set_json(jfile, files):
|
415
425
|
sys.exit(f'Failed to write release {tag} file {jfile}: {error}')
|
416
426
|
|
417
|
-
return files.get(
|
427
|
+
return files.get(args._implementation, {})
|
418
428
|
|
419
429
|
|
420
430
|
def update_version_symlinks(args: Namespace) -> None:
|
@@ -499,7 +509,7 @@ def purge_unused_releases(args: Namespace) -> None:
|
|
499
509
|
def show_list(args: Namespace) -> None:
|
500
510
|
"Show a list of available releases"
|
501
511
|
latest = parse_version(get_release_tag(args))
|
502
|
-
releases = {r: d for r, d in fetch_tags()}
|
512
|
+
releases = {r: d for r, d in fetch_tags(args)}
|
503
513
|
cached = set(p.name for p in args._releases.iterdir())
|
504
514
|
for release in sorted(cached.union(releases)):
|
505
515
|
if args.re_match and not re.search(args.re_match, release):
|
@@ -578,7 +588,7 @@ def strip_binaries(vdir: Path, distribution: str) -> bool:
|
|
578
588
|
|
579
589
|
|
580
590
|
def install(
|
581
|
-
args: Namespace, vdir: Path, release: str, distribution: str, files: dict
|
591
|
+
args: Namespace, vdir: Path, release: str, distribution: str, files: dict[str, Any]
|
582
592
|
) -> str | None:
|
583
593
|
"Install a version"
|
584
594
|
version = vdir.name
|
@@ -640,6 +650,21 @@ def show_cache_size(path: Path, args: Namespace) -> None:
|
|
640
650
|
print(f'{size_str}\tTOTAL')
|
641
651
|
|
642
652
|
|
653
|
+
def create_cert(option: str) -> ssl.SSLContext | None:
|
654
|
+
"Create an SSL context for HTTPS connections based on the specified certificate store"
|
655
|
+
|
656
|
+
if not option or option == 'system':
|
657
|
+
return None
|
658
|
+
|
659
|
+
if option == 'certifi':
|
660
|
+
import certifi
|
661
|
+
|
662
|
+
return ssl.create_default_context(cafile=certifi.where())
|
663
|
+
|
664
|
+
assert option == 'none'
|
665
|
+
return ssl._create_unverified_context()
|
666
|
+
|
667
|
+
|
643
668
|
def main() -> str | None:
|
644
669
|
"Main code"
|
645
670
|
distro_default = DISTRIBUTIONS.get((platform.system(), platform.machine()))
|
@@ -700,6 +725,11 @@ def main() -> str | None:
|
|
700
725
|
opt.add_argument(
|
701
726
|
'--no-strip', action='store_true', help='do not strip downloaded binaries'
|
702
727
|
)
|
728
|
+
opt.add_argument(
|
729
|
+
'--cert',
|
730
|
+
choices=CERTS,
|
731
|
+
help=f'specify which SSL certificates to use for HTTPS requests. Default="{CERTS[0]}"',
|
732
|
+
)
|
703
733
|
opt.add_argument(
|
704
734
|
'-V', '--version', action='store_true', help=f'just show {PROG} version'
|
705
735
|
)
|
@@ -765,6 +795,7 @@ def main() -> str | None:
|
|
765
795
|
prefix_dir = Path(args.prefix_dir).expanduser().resolve()
|
766
796
|
cache_dir = Path(args.cache_dir).expanduser().resolve()
|
767
797
|
|
798
|
+
args._implementation = 'cpython' # at the moment, only support CPython
|
768
799
|
args._distribution = distribution
|
769
800
|
args._data = f'{PROG}.json'
|
770
801
|
|
@@ -776,6 +807,7 @@ def main() -> str | None:
|
|
776
807
|
args._releases = cache_dir / 'releases'
|
777
808
|
args._releases.mkdir(parents=True, exist_ok=True)
|
778
809
|
args._latest_release = cache_dir / 'latest_release'
|
810
|
+
args._cert = create_cert(args.cert)
|
779
811
|
|
780
812
|
result = args.func(args)
|
781
813
|
purge_unused_releases(args)
|
@@ -815,7 +847,7 @@ class install_:
|
|
815
847
|
@staticmethod
|
816
848
|
def run(args: Namespace) -> str | None:
|
817
849
|
release = get_release_tag(args)
|
818
|
-
files = get_release_files(args, release
|
850
|
+
files = get_release_files(args, release)
|
819
851
|
if not files:
|
820
852
|
return f'Release "{release}" not found, or has no compatible files.'
|
821
853
|
|
@@ -874,7 +906,7 @@ class update_:
|
|
874
906
|
@staticmethod
|
875
907
|
def run(args: Namespace) -> str | None:
|
876
908
|
release_target = get_release_tag(args)
|
877
|
-
files = get_release_files(args, release_target
|
909
|
+
files = get_release_files(args, release_target)
|
878
910
|
if not files:
|
879
911
|
return f'Release "{release_target}" not found.'
|
880
912
|
|
@@ -991,7 +1023,7 @@ class list_:
|
|
991
1023
|
@staticmethod
|
992
1024
|
def run(args: Namespace) -> str | None:
|
993
1025
|
release_target = get_release_tag(args)
|
994
|
-
files = get_release_files(args, release_target
|
1026
|
+
files = get_release_files(args, release_target)
|
995
1027
|
if not files:
|
996
1028
|
return f'Release "{release_target}" not found.'
|
997
1029
|
|
@@ -1089,7 +1121,7 @@ class show_:
|
|
1089
1121
|
return None
|
1090
1122
|
|
1091
1123
|
release = get_release_tag(args)
|
1092
|
-
files = get_release_files(args, release
|
1124
|
+
files = get_release_files(args, release)
|
1093
1125
|
if not files:
|
1094
1126
|
return f'Error: release "{release}" not found.'
|
1095
1127
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|