pystand 2.12__py3-none-any.whl → 2.13.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.
- {pystand-2.12.dist-info → pystand-2.13.1.dist-info}/METADATA +20 -19
- pystand-2.13.1.dist-info/RECORD +6 -0
- {pystand-2.12.dist-info → pystand-2.13.1.dist-info}/WHEEL +1 -1
- pystand.py +362 -220
- pystand-2.12.dist-info/RECORD +0 -6
- {pystand-2.12.dist-info → pystand-2.13.1.dist-info}/entry_points.txt +0 -0
- {pystand-2.12.dist-info → pystand-2.13.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: pystand
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.13.1
|
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: GPLv3
|
@@ -99,9 +99,9 @@ $ uv venv -p $(pystand path 3.12) myenv
|
|
99
99
|
# installed python 3.12:
|
100
100
|
$ $(pystand path -p 3.12) -m venv myenv
|
101
101
|
|
102
|
-
# Use
|
102
|
+
# Use uv tool to install a package to be run with pystand installed python
|
103
103
|
# specific version 3.11.1:
|
104
|
-
$
|
104
|
+
$ uv tool install -p $(pystand path 3.11.1) cowsay
|
105
105
|
```
|
106
106
|
|
107
107
|
See detailed usage information in the [Usage](#usage) section that
|
@@ -139,17 +139,17 @@ sh/python-build-standalone.
|
|
139
139
|
|
140
140
|
options:
|
141
141
|
-h, --help show this help message and exit
|
142
|
-
-D
|
142
|
+
-D, --distribution DISTRIBUTION
|
143
143
|
python-build-standalone distribution. Default is
|
144
144
|
"x86_64_v3-unknown-linux-gnu-install_only_stripped for
|
145
145
|
this host
|
146
|
-
-P
|
146
|
+
-P, --prefix-dir PREFIX_DIR
|
147
147
|
specify prefix dir for storing versions. Default is
|
148
148
|
"$HOME/.local/share/pystand"
|
149
|
-
-C
|
149
|
+
-C, --cache-dir CACHE_DIR
|
150
150
|
specify cache dir for downloads. Default is
|
151
151
|
"$HOME/.cache/pystand"
|
152
|
-
-M
|
152
|
+
-M, --cache-minutes CACHE_MINUTES
|
153
153
|
cache latest YYYYMMDD release tag fetch for this many
|
154
154
|
minutes, before rechecking for latest. Default is 60
|
155
155
|
minutes
|
@@ -174,7 +174,7 @@ Commands:
|
|
174
174
|
show Show versions available from a release.
|
175
175
|
path Show path prefix to installed version base directory.
|
176
176
|
|
177
|
-
Some commands offer aliases as shown in
|
177
|
+
Some commands offer aliases as shown in parentheses above. Note you can set
|
178
178
|
default starting global options in $HOME/.config/pystand-flags.conf.
|
179
179
|
```
|
180
180
|
|
@@ -193,7 +193,7 @@ positional arguments:
|
|
193
193
|
|
194
194
|
options:
|
195
195
|
-h, --help show this help message and exit
|
196
|
-
-r
|
196
|
+
-r, --release RELEASE
|
197
197
|
install from specified python-build-standalone
|
198
198
|
YYYYMMDD release (e.g. 20240415), default is latest
|
199
199
|
release
|
@@ -214,7 +214,7 @@ positional arguments:
|
|
214
214
|
|
215
215
|
options:
|
216
216
|
-h, --help show this help message and exit
|
217
|
-
-r
|
217
|
+
-r, --release RELEASE
|
218
218
|
update to specified YYYMMDD release (e.g. 20240415),
|
219
219
|
default is latest release
|
220
220
|
-a, --all update ALL versions
|
@@ -241,7 +241,7 @@ options:
|
|
241
241
|
-a, --all remove ALL versions
|
242
242
|
--skip skip the specified versions when removing all (only
|
243
243
|
can be specified with --all)
|
244
|
-
-r
|
244
|
+
-r, --release RELEASE
|
245
245
|
only remove versions if from specified YYYMMDD release
|
246
246
|
(e.g. 20240415)
|
247
247
|
|
@@ -262,7 +262,7 @@ options:
|
|
262
262
|
-h, --help show this help message and exit
|
263
263
|
-v, --verbose explicitly report why a version is not eligible for
|
264
264
|
update
|
265
|
-
-r
|
265
|
+
-r, --release RELEASE
|
266
266
|
use specified YYYYMMDD release (e.g. 20240415) for
|
267
267
|
verbose compare, default is latest release
|
268
268
|
```
|
@@ -283,7 +283,7 @@ positional arguments:
|
|
283
283
|
options:
|
284
284
|
-h, --help show this help message and exit
|
285
285
|
-l, --list just list recent releases
|
286
|
-
-r
|
286
|
+
-r, --release RELEASE
|
287
287
|
python-build-standalone YYYYMMDD release to show (e.g.
|
288
288
|
20240415), default is latest release
|
289
289
|
-a, --all show all available distributions for each version from
|
@@ -313,23 +313,24 @@ Python 3.8 or later is required. Arch Linux users can install [`pystand`
|
|
313
313
|
from the AUR](https://aur.archlinux.org/packages/pystand) and skip this
|
314
314
|
section.
|
315
315
|
|
316
|
-
|
317
|
-
|
316
|
+
Note [pystand is on PyPI](https://pypi.org/project/pystand/) so the
|
317
|
+
easiest way to install it is to use [`uv tool`][uvtool] (or
|
318
|
+
[`pipx`][pipx] or [`pipxu`][pipxu]).
|
318
319
|
|
319
320
|
```sh
|
320
|
-
$
|
321
|
+
$ uv tool install pystand
|
321
322
|
```
|
322
323
|
|
323
324
|
To upgrade:
|
324
325
|
|
325
326
|
```sh
|
326
|
-
$
|
327
|
+
$ uv tool upgrade pystand
|
327
328
|
```
|
328
329
|
|
329
330
|
To uninstall:
|
330
331
|
|
331
332
|
```sh
|
332
|
-
$
|
333
|
+
$ uv tool uninstall pystand
|
333
334
|
```
|
334
335
|
|
335
336
|
## Extrapolation of Python Versions
|
@@ -0,0 +1,6 @@
|
|
1
|
+
pystand.py,sha256=I7VERu60r7H6iCUy5SJvFv5_fT36MeNrIEZUzsoq-RM,36843
|
2
|
+
pystand-2.13.1.dist-info/METADATA,sha256=LsY47B2HgxjzssPumImNJmjcex6-uq4q5Z_6y022KNo,24811
|
3
|
+
pystand-2.13.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
4
|
+
pystand-2.13.1.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
+
pystand-2.13.1.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
+
pystand-2.13.1.dist-info/RECORD,,
|
pystand.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
#!/usr/bin/python3
|
2
2
|
# PYTHON_ARGCOMPLETE_OK
|
3
|
-
|
3
|
+
"""
|
4
4
|
Command line tool to download, install, and update pre-built Python
|
5
5
|
versions from the python-build-standalone project at
|
6
6
|
https://github.com/astral-sh/python-build-standalone.
|
7
|
-
|
7
|
+
"""
|
8
|
+
|
8
9
|
from __future__ import annotations
|
9
10
|
|
10
11
|
import os
|
@@ -44,22 +45,25 @@ DISTRIBUTIONS = {
|
|
44
45
|
('Linux', 'armv8l'): 'armv7-unknown-linux-gnueabihf-install_only_stripped',
|
45
46
|
('Darwin', 'x86_64'): 'x86_64-apple-darwin-install_only_stripped',
|
46
47
|
('Darwin', 'aarch64'): 'aarch64-apple-darwin-install_only_stripped',
|
47
|
-
('Windows', 'x86_64'):
|
48
|
-
'x86_64-pc-windows-msvc-shared-install_only_stripped',
|
48
|
+
('Windows', 'x86_64'): 'x86_64-pc-windows-msvc-shared-install_only_stripped',
|
49
49
|
('Windows', 'i686'): 'i686-pc-windows-msvc-shared-install_only_stripped',
|
50
50
|
}
|
51
51
|
|
52
|
+
|
52
53
|
def is_admin() -> bool:
|
53
|
-
|
54
|
+
"Check if we are running as root"
|
54
55
|
if platform.system() == 'Windows':
|
55
56
|
import ctypes
|
57
|
+
|
56
58
|
return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore
|
57
59
|
|
58
60
|
return os.geteuid() == 0
|
59
61
|
|
62
|
+
|
60
63
|
def get_version() -> str:
|
61
|
-
|
64
|
+
"Return the version of this package"
|
62
65
|
from importlib.metadata import version
|
66
|
+
|
63
67
|
try:
|
64
68
|
ver = version(PROG)
|
65
69
|
except Exception:
|
@@ -67,12 +71,15 @@ def get_version() -> str:
|
|
67
71
|
|
68
72
|
return ver
|
69
73
|
|
74
|
+
|
70
75
|
def fmt(version, release) -> str:
|
71
|
-
|
76
|
+
"Return a formatted release version string"
|
72
77
|
return f'{version} @ {release}'
|
73
78
|
|
79
|
+
|
74
80
|
def get_json(file: Path) -> dict:
|
75
81
|
from json import load
|
82
|
+
|
76
83
|
'Get JSON data from given file'
|
77
84
|
try:
|
78
85
|
with file.open() as fp:
|
@@ -82,9 +89,11 @@ def get_json(file: Path) -> dict:
|
|
82
89
|
|
83
90
|
return {}
|
84
91
|
|
92
|
+
|
85
93
|
def set_json(file: Path, data: dict) -> str | None:
|
86
|
-
|
94
|
+
"Set JSON data to given file"
|
87
95
|
from json import dump
|
96
|
+
|
88
97
|
try:
|
89
98
|
with file.open('w') as fp:
|
90
99
|
dump(data, fp, indent=2)
|
@@ -93,11 +102,13 @@ def set_json(file: Path, data: dict) -> str | None:
|
|
93
102
|
|
94
103
|
return None
|
95
104
|
|
105
|
+
|
96
106
|
# The gh handle is an opaque github instance handle
|
97
107
|
get_gh_handle = None
|
98
108
|
|
109
|
+
|
99
110
|
def get_gh(args: Namespace) -> Any:
|
100
|
-
|
111
|
+
"Return a GitHub handle"
|
101
112
|
# The gh handle is a global to lazily create it only if/when needed
|
102
113
|
global get_gh_handle
|
103
114
|
if get_gh_handle:
|
@@ -105,17 +116,20 @@ def get_gh(args: Namespace) -> Any:
|
|
105
116
|
|
106
117
|
if args.github_access_token:
|
107
118
|
from github.Auth import Token
|
119
|
+
|
108
120
|
auth = Token(args.github_access_token)
|
109
121
|
else:
|
110
122
|
auth = None
|
111
123
|
|
112
124
|
# Save this handle globally for future use
|
113
125
|
from github import Github
|
126
|
+
|
114
127
|
get_gh_handle = Github(auth=auth) # type: ignore
|
115
128
|
return get_gh_handle
|
116
129
|
|
130
|
+
|
117
131
|
def rm_path(path: Path) -> None:
|
118
|
-
|
132
|
+
"Remove the given path"
|
119
133
|
if path.is_symlink():
|
120
134
|
path.unlink()
|
121
135
|
elif path.is_dir():
|
@@ -123,21 +137,25 @@ def rm_path(path: Path) -> None:
|
|
123
137
|
elif path.exists():
|
124
138
|
path.unlink()
|
125
139
|
|
140
|
+
|
126
141
|
def unpack_zst(filename: str, extract_dir: str) -> None:
|
127
|
-
|
142
|
+
"Unpack a zstandard compressed tar"
|
128
143
|
import tarfile
|
129
144
|
|
130
145
|
import zstandard
|
146
|
+
|
131
147
|
with open(filename, 'rb') as compressed:
|
132
148
|
dctx = zstandard.ZstdDecompressor()
|
133
149
|
with dctx.stream_reader(compressed) as reader:
|
134
150
|
with tarfile.open(fileobj=reader, mode='r|') as tar:
|
135
151
|
tar.extractall(path=extract_dir)
|
136
152
|
|
153
|
+
|
137
154
|
def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
|
138
|
-
|
155
|
+
"Fetch and unpack a release file"
|
139
156
|
from urllib.parse import unquote, urlparse
|
140
157
|
from urllib.request import urlretrieve
|
158
|
+
|
141
159
|
error = None
|
142
160
|
tmpdir = tdir.with_name(f'{tdir.name}-tmp')
|
143
161
|
rm_path(tmpdir)
|
@@ -184,18 +202,20 @@ def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
|
|
184
202
|
rm_path(tmpdir)
|
185
203
|
return error
|
186
204
|
|
205
|
+
|
187
206
|
def is_release_version(version: str) -> bool:
|
188
|
-
|
207
|
+
"Check if a string is a formal Python release tag"
|
189
208
|
return version.replace('.', '').isdigit()
|
190
209
|
|
210
|
+
|
191
211
|
class VersionMatcher:
|
192
|
-
|
212
|
+
"Match a version string to a list of versions"
|
213
|
+
|
193
214
|
def __init__(self, seq: Iterable[str]) -> None:
|
194
215
|
self.seq = sorted(seq, key=parse_version, reverse=True)
|
195
216
|
|
196
|
-
def match(self, version: str | None, *,
|
197
|
-
|
198
|
-
'Return full version string given a [possibly] part version prefix'
|
217
|
+
def match(self, version: str | None, *, upgrade: bool = False) -> str | None:
|
218
|
+
"Return full version string given a [possibly] part version prefix"
|
199
219
|
|
200
220
|
# If no version specified, return the latest release version
|
201
221
|
if not version:
|
@@ -222,25 +242,31 @@ class VersionMatcher:
|
|
222
242
|
# release.
|
223
243
|
for full_version in self.seq:
|
224
244
|
if full_version.startswith(version):
|
225
|
-
if not upgrade or not is_release
|
226
|
-
or is_release_version(full_version):
|
245
|
+
if not upgrade or not is_release or is_release_version(full_version):
|
227
246
|
return full_version
|
228
247
|
|
229
248
|
return None
|
230
249
|
|
250
|
+
|
231
251
|
def iter_versions(args: Namespace) -> Iterator[Path]:
|
232
|
-
|
252
|
+
"Iterate over all version dirs"
|
233
253
|
for f in args._versions.iterdir():
|
234
|
-
if
|
235
|
-
|
254
|
+
if (
|
255
|
+
f.is_dir()
|
256
|
+
and not f.is_symlink()
|
257
|
+
and f.name[0] != '.'
|
258
|
+
and f.name[0].isdigit()
|
259
|
+
):
|
236
260
|
yield f
|
237
261
|
|
262
|
+
|
238
263
|
def get_version_names(args: Namespace) -> list[str]:
|
239
|
-
|
264
|
+
"Return a list of validated version names based on command line args"
|
240
265
|
if args.all:
|
241
266
|
if not args.skip and args.version:
|
242
|
-
args.parser.error(
|
243
|
-
|
267
|
+
args.parser.error(
|
268
|
+
'Can not specify versions with --all unless also specifying --skip.'
|
269
|
+
)
|
244
270
|
else:
|
245
271
|
if args.skip:
|
246
272
|
args.parser.error('--skip can only be specified with --all.')
|
@@ -256,16 +282,16 @@ def get_version_names(args: Namespace) -> list[str]:
|
|
256
282
|
|
257
283
|
given = set(versions)
|
258
284
|
|
259
|
-
if
|
285
|
+
if unknown := given - all_names:
|
260
286
|
s = 's' if len(unknown) > 1 else ''
|
261
287
|
unknowns = [f'"{u}"' for u in unknown]
|
262
288
|
sys.exit(f'Error: version{s} {", ".join(unknowns)} not found.')
|
263
289
|
|
264
|
-
return sorted(all_names - given, key=parse_version)
|
265
|
-
|
290
|
+
return sorted(all_names - given, key=parse_version) if args.all else versions
|
291
|
+
|
266
292
|
|
267
293
|
def check_release_tag(release: str) -> str | None:
|
268
|
-
|
294
|
+
"Check the specified release tag is valid"
|
269
295
|
if not release.isdigit() or len(release) != len(SAMPL_RELEASE):
|
270
296
|
return 'Release must be a YYYYMMDD string.'
|
271
297
|
|
@@ -276,13 +302,15 @@ def check_release_tag(release: str) -> str | None:
|
|
276
302
|
|
277
303
|
return None
|
278
304
|
|
305
|
+
|
279
306
|
# Note we use a simple direct URL fetch to get the latest tag info
|
280
307
|
# because it is much faster than using the GitHub API, and has no
|
281
308
|
# rate-limits.
|
282
309
|
def fetch_tags() -> Iterator[tuple[str, str]]:
|
283
|
-
|
310
|
+
"Fetch the latest release tags from the GitHub release atom feed"
|
284
311
|
import xml.etree.ElementTree as et
|
285
312
|
from urllib.request import urlopen
|
313
|
+
|
286
314
|
try:
|
287
315
|
with urlopen(LATEST_RELEASES) as url:
|
288
316
|
data = et.parse(url).getroot()
|
@@ -296,9 +324,11 @@ def fetch_tags() -> Iterator[tuple[str, str]]:
|
|
296
324
|
if tl and dt:
|
297
325
|
yield tl, dt
|
298
326
|
|
327
|
+
|
299
328
|
def fetch_tag_latest() -> str:
|
300
|
-
|
329
|
+
"Fetch the latest release tag from the GitHub"
|
301
330
|
from urllib.request import urlopen
|
331
|
+
|
302
332
|
try:
|
303
333
|
with urlopen(LATEST_RELEASE_TAG) as url:
|
304
334
|
data = url.geturl()
|
@@ -307,8 +337,9 @@ def fetch_tag_latest() -> str:
|
|
307
337
|
|
308
338
|
return data.split('/')[-1]
|
309
339
|
|
340
|
+
|
310
341
|
def get_release_tag(args: Namespace) -> str:
|
311
|
-
|
342
|
+
"Return the release tag, or latest if not specified"
|
312
343
|
if release := args.release:
|
313
344
|
if err := check_release_tag(release):
|
314
345
|
sys.exit(err)
|
@@ -326,8 +357,9 @@ def get_release_tag(args: Namespace) -> str:
|
|
326
357
|
args._latest_release.write_text(tag + '\n')
|
327
358
|
return tag
|
328
359
|
|
360
|
+
|
329
361
|
def add_file(files: dict, tag: str, name: str, url: str) -> None:
|
330
|
-
|
362
|
+
"Extract the implementation, version, and architecture from a filename"
|
331
363
|
if name.endswith('.tar.zst'):
|
332
364
|
name = name[:-8]
|
333
365
|
elif name.endswith('.tar.gz'):
|
@@ -353,18 +385,19 @@ def add_file(files: dict, tag: str, name: str, url: str) -> None:
|
|
353
385
|
|
354
386
|
vers[ver][arch] = url
|
355
387
|
|
388
|
+
|
356
389
|
def get_release_files(args, tag, implementation: str | None = None) -> dict:
|
357
|
-
|
390
|
+
"Return the release files for the given tag"
|
358
391
|
# Look for tag data in our release cache
|
359
392
|
jfile = args._releases / tag
|
360
393
|
if not (files := get_json(jfile)):
|
361
|
-
|
362
394
|
# May have read this release before but it has no assets
|
363
395
|
if jfile.exists():
|
364
396
|
return {}
|
365
397
|
|
366
398
|
# Not in cache so fetch it (and also store in cache)
|
367
399
|
from github.GithubException import UnknownObjectException
|
400
|
+
|
368
401
|
gh = get_gh(args)
|
369
402
|
try:
|
370
403
|
release = gh.get_repo(GITHUB_REPO).get_release(tag)
|
@@ -384,8 +417,9 @@ def get_release_files(args, tag, implementation: str | None = None) -> dict:
|
|
384
417
|
|
385
418
|
return files.get(implementation, {}) if implementation else files
|
386
419
|
|
420
|
+
|
387
421
|
def update_version_symlinks(args: Namespace) -> None:
|
388
|
-
|
422
|
+
"Create/update symlinks pointing to latest version"
|
389
423
|
base = args._versions
|
390
424
|
if not base.exists():
|
391
425
|
return
|
@@ -435,11 +469,13 @@ def update_version_symlinks(args: Namespace) -> None:
|
|
435
469
|
if not old_tgt or old_tgt != tgt:
|
436
470
|
(base / name).symlink_to(tgt, target_is_directory=True)
|
437
471
|
|
472
|
+
|
438
473
|
def purge_unused_releases(args: Namespace) -> None:
|
439
|
-
|
474
|
+
"Purge old releases that are no longer needed and have expired"
|
440
475
|
# Want to keep releases for versions that we currently have installed
|
441
|
-
keep = {
|
442
|
-
|
476
|
+
keep = {
|
477
|
+
r for v in iter_versions(args) if (r := get_json(v / args._data).get('release'))
|
478
|
+
}
|
443
479
|
|
444
480
|
# Add current release to keep list (even if not currently installed)
|
445
481
|
if args._latest_release.exists():
|
@@ -460,8 +496,10 @@ def purge_unused_releases(args: Namespace) -> None:
|
|
460
496
|
if path.name not in keep:
|
461
497
|
rm_path(path)
|
462
498
|
|
499
|
+
|
463
500
|
def show_list(args: Namespace) -> None:
|
464
|
-
|
501
|
+
"Show a list of available releases"
|
502
|
+
latest = parse_version(get_release_tag(args))
|
465
503
|
releases = {r: d for r, d in fetch_tags()}
|
466
504
|
cached = set(p.name for p in args._releases.iterdir())
|
467
505
|
for release in sorted(cached.union(releases)):
|
@@ -469,43 +507,40 @@ def show_list(args: Namespace) -> None:
|
|
469
507
|
continue
|
470
508
|
|
471
509
|
if dt_str := releases.get(release):
|
472
|
-
dts =
|
473
|
-
|
510
|
+
dts = (
|
511
|
+
datetime.fromisoformat(dt_str)
|
512
|
+
.astimezone()
|
513
|
+
.isoformat(sep='_', timespec='minutes')
|
514
|
+
)
|
474
515
|
else:
|
475
516
|
dts = '......................'
|
476
517
|
|
477
518
|
if release in cached:
|
478
519
|
ddir = args._downloads / release
|
479
520
|
count = len(list(ddir.iterdir())) if ddir.exists() else 0
|
480
|
-
app = f' cached + {count} downloaded files'
|
481
|
-
if count > 0 else ' cached'
|
521
|
+
app = f' cached + {count} downloaded files' if count > 0 else ' cached'
|
482
522
|
else:
|
483
523
|
app = ''
|
484
524
|
|
485
|
-
|
525
|
+
pre = ' pre-release' if parse_version(release) > latest else ''
|
486
526
|
|
487
|
-
|
488
|
-
'Base class for all commands'
|
489
|
-
commands = []
|
527
|
+
print(f'{release} {dts}{app}{pre}')
|
490
528
|
|
491
|
-
@classmethod
|
492
|
-
def add(cls, parent) -> None:
|
493
|
-
'Append parent command to internal list'
|
494
|
-
cls.commands.append(parent)
|
495
529
|
|
496
530
|
def get_title(desc: str) -> str:
|
497
|
-
|
531
|
+
"Return single title line from description"
|
498
532
|
res = []
|
499
533
|
for line in desc.splitlines():
|
500
534
|
line = line.strip()
|
501
535
|
res.append(line)
|
502
536
|
if line.endswith('.'):
|
503
|
-
return ' '.
|
537
|
+
return ' '.join(res)
|
504
538
|
|
505
539
|
sys.exit('Must end description with a full stop.')
|
506
540
|
|
541
|
+
|
507
542
|
def remove(args: Namespace, version: str) -> None:
|
508
|
-
|
543
|
+
"Remove a version"
|
509
544
|
vdir = args._versions / version
|
510
545
|
if not vdir.exists():
|
511
546
|
return
|
@@ -517,8 +552,9 @@ def remove(args: Namespace, version: str) -> None:
|
|
517
552
|
|
518
553
|
shutil.rmtree(vdir)
|
519
554
|
|
555
|
+
|
520
556
|
def strip_binaries(vdir: Path, distribution: str) -> bool:
|
521
|
-
|
557
|
+
"Strip binaries from files in a version directory"
|
522
558
|
from subprocess import DEVNULL, run
|
523
559
|
|
524
560
|
# Only run the strip command on Linux hosts and for Linux distributions
|
@@ -541,14 +577,17 @@ def strip_binaries(vdir: Path, distribution: str) -> bool:
|
|
541
577
|
|
542
578
|
return was_stripped
|
543
579
|
|
544
|
-
|
545
|
-
|
546
|
-
|
580
|
+
|
581
|
+
def install(
|
582
|
+
args: Namespace, vdir: Path, release: str, distribution: str, files: dict
|
583
|
+
) -> str | None:
|
584
|
+
"Install a version"
|
547
585
|
version = vdir.name
|
548
586
|
|
549
587
|
if not (url := files[version].get(distribution)):
|
550
|
-
return
|
551
|
-
|
588
|
+
return (
|
589
|
+
f'Arch "{distribution}" not found for release {release} version {version}.'
|
590
|
+
)
|
552
591
|
|
553
592
|
tmpdir = args._versions / f'.{version}-tmp'
|
554
593
|
rm_path(tmpdir)
|
@@ -560,7 +599,7 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
560
599
|
if not args.no_strip and strip_binaries(tmpdir, distribution):
|
561
600
|
data['stripped'] = 'true'
|
562
601
|
|
563
|
-
if
|
602
|
+
if error := set_json(tmpdir / args._data, data):
|
564
603
|
error = f'Failed to write {version} data file: {error}'
|
565
604
|
|
566
605
|
if error:
|
@@ -571,8 +610,9 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
571
610
|
|
572
611
|
return error
|
573
612
|
|
613
|
+
|
574
614
|
def main() -> str | None:
|
575
|
-
|
615
|
+
"Main code"
|
576
616
|
distro_default = DISTRIBUTIONS.get((platform.system(), platform.machine()))
|
577
617
|
distro_help = distro_default or '?unknown?'
|
578
618
|
|
@@ -581,41 +621,67 @@ def main() -> str | None:
|
|
581
621
|
cache_dir = platformdirs.user_cache_path() / PROG
|
582
622
|
|
583
623
|
# Parse arguments
|
584
|
-
opt = ArgumentParser(
|
585
|
-
|
586
|
-
|
587
|
-
|
624
|
+
opt = ArgumentParser(
|
625
|
+
description=__doc__,
|
626
|
+
epilog='Some commands offer aliases as shown in parentheses above. '
|
627
|
+
'Note you can set default starting global options in '
|
628
|
+
f'{CNFFILE}.',
|
629
|
+
)
|
588
630
|
|
589
631
|
# Set up main/global arguments
|
590
|
-
opt.add_argument(
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
opt.add_argument(
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
632
|
+
opt.add_argument(
|
633
|
+
'-D',
|
634
|
+
'--distribution',
|
635
|
+
help=f'{REPO} distribution. Default is "{distro_help} for this host',
|
636
|
+
)
|
637
|
+
opt.add_argument(
|
638
|
+
'-P',
|
639
|
+
'--prefix-dir',
|
640
|
+
default=prefix_dir,
|
641
|
+
help='specify prefix dir for storing versions. Default is "%(default)s"',
|
642
|
+
)
|
643
|
+
opt.add_argument(
|
644
|
+
'-C',
|
645
|
+
'--cache-dir',
|
646
|
+
default=str(cache_dir),
|
647
|
+
help='specify cache dir for downloads. Default is "%(default)s"',
|
648
|
+
)
|
649
|
+
opt.add_argument(
|
650
|
+
'-M',
|
651
|
+
'--cache-minutes',
|
652
|
+
default=60,
|
653
|
+
type=float,
|
654
|
+
help='cache latest YYYYMMDD release tag fetch for this '
|
655
|
+
'many minutes, before rechecking for latest. '
|
656
|
+
'Default is %(default)d minutes',
|
657
|
+
)
|
658
|
+
opt.add_argument(
|
659
|
+
'--purge-days',
|
660
|
+
default=90,
|
661
|
+
type=int,
|
662
|
+
help='cache YYYYMMDD release file lists and downloads for '
|
663
|
+
'this number of days after last version referencing that '
|
664
|
+
'release is removed. Default is %(default)d days',
|
665
|
+
)
|
666
|
+
opt.add_argument(
|
667
|
+
'--github-access-token',
|
668
|
+
help='optional Github access token. Can specify to reduce rate limiting.',
|
669
|
+
)
|
670
|
+
opt.add_argument(
|
671
|
+
'--no-strip', action='store_true', help='do not strip downloaded binaries'
|
672
|
+
)
|
673
|
+
opt.add_argument(
|
674
|
+
'-V', '--version', action='store_true', help=f'just show {PROG} version'
|
675
|
+
)
|
614
676
|
cmd = opt.add_subparsers(title='Commands', dest='cmdname')
|
615
677
|
|
616
678
|
# Add each command ..
|
617
|
-
for
|
618
|
-
name
|
679
|
+
for name in globals():
|
680
|
+
if not name[0].islower() or not name.endswith('_'):
|
681
|
+
continue
|
682
|
+
|
683
|
+
cls = globals()[name]
|
684
|
+
name = name[:-1]
|
619
685
|
|
620
686
|
if hasattr(cls, 'doc'):
|
621
687
|
desc = cls.doc.strip()
|
@@ -626,8 +692,7 @@ def main() -> str | None:
|
|
626
692
|
|
627
693
|
aliases = cls.aliases if hasattr(cls, 'aliases') else []
|
628
694
|
title = get_title(desc)
|
629
|
-
cmdopt = cmd.add_parser(name, description=desc, help=title,
|
630
|
-
aliases=aliases)
|
695
|
+
cmdopt = cmd.add_parser(name, description=desc, help=title, aliases=aliases)
|
631
696
|
|
632
697
|
# Set up this commands own arguments, if it has any
|
633
698
|
if hasattr(cls, 'init'):
|
@@ -661,8 +726,10 @@ def main() -> str | None:
|
|
661
726
|
|
662
727
|
distribution = args.distribution or distro_default
|
663
728
|
if not distribution:
|
664
|
-
sys.exit(
|
665
|
-
|
729
|
+
sys.exit(
|
730
|
+
'Unknown system + machine distribution. Please specify '
|
731
|
+
'using -D/--distribution option.'
|
732
|
+
)
|
666
733
|
|
667
734
|
# Keep some useful info in the namespace passed to the command
|
668
735
|
prefix_dir = Path(args.prefix_dir).expanduser().resolve()
|
@@ -685,23 +752,35 @@ def main() -> str | None:
|
|
685
752
|
update_version_symlinks(args)
|
686
753
|
return result
|
687
754
|
|
688
|
-
|
689
|
-
|
755
|
+
|
756
|
+
# COMMAND
|
757
|
+
class install_:
|
690
758
|
doc = f'Install one or more versions from a {REPO} release.'
|
691
759
|
|
692
760
|
@staticmethod
|
693
761
|
def init(parser: ArgumentParser) -> None:
|
694
|
-
parser.add_argument(
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
762
|
+
parser.add_argument(
|
763
|
+
'-r',
|
764
|
+
'--release',
|
765
|
+
help=f'install from specified {REPO} '
|
766
|
+
f'YYYYMMDD release (e.g. {SAMPL_RELEASE}), '
|
767
|
+
'default is latest release',
|
768
|
+
)
|
769
|
+
parser.add_argument(
|
770
|
+
'-f',
|
771
|
+
'--force',
|
772
|
+
action='store_true',
|
773
|
+
help='force install even if already installed',
|
774
|
+
)
|
775
|
+
parser.add_argument(
|
776
|
+
'-s',
|
777
|
+
'--include-source',
|
778
|
+
action='store_true',
|
779
|
+
help='also install source files if available in distribution download',
|
780
|
+
)
|
781
|
+
parser.add_argument(
|
782
|
+
'version', nargs='+', help='version to install. E.g. 3.12 or 3.12.3'
|
783
|
+
)
|
705
784
|
|
706
785
|
@staticmethod
|
707
786
|
def run(args: Namespace) -> str | None:
|
@@ -727,27 +806,40 @@ class _install(COMMAND):
|
|
727
806
|
|
728
807
|
print(f'Version {fmt(version, release)} installed.')
|
729
808
|
|
730
|
-
|
731
|
-
|
732
|
-
|
809
|
+
|
810
|
+
# COMMAND
|
811
|
+
class update_:
|
812
|
+
"Update one, more, or all versions to another release."
|
813
|
+
|
733
814
|
aliases = ['upgrade']
|
734
815
|
|
735
816
|
@staticmethod
|
736
817
|
def init(parser: ArgumentParser) -> None:
|
737
|
-
parser.add_argument(
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
818
|
+
parser.add_argument(
|
819
|
+
'-r',
|
820
|
+
'--release',
|
821
|
+
help='update to specified YYYMMDD release (e.g. '
|
822
|
+
f'{SAMPL_RELEASE}), default is latest release',
|
823
|
+
)
|
824
|
+
parser.add_argument(
|
825
|
+
'-a', '--all', action='store_true', help='update ALL versions'
|
826
|
+
)
|
827
|
+
parser.add_argument(
|
828
|
+
'--skip',
|
829
|
+
action='store_true',
|
830
|
+
help='skip the specified versions when '
|
831
|
+
'updating all (only can be specified with --all)',
|
832
|
+
)
|
833
|
+
parser.add_argument(
|
834
|
+
'-k',
|
835
|
+
'--keep',
|
836
|
+
action='store_true',
|
837
|
+
help='keep old version after updating (but only '
|
838
|
+
'if different version number)',
|
839
|
+
)
|
840
|
+
parser.add_argument(
|
841
|
+
'version', nargs='*', help='version to update (or to skip for --all --skip)'
|
842
|
+
)
|
751
843
|
|
752
844
|
@staticmethod
|
753
845
|
def run(args: Namespace) -> str | None:
|
@@ -773,54 +865,66 @@ class _update(COMMAND):
|
|
773
865
|
continue
|
774
866
|
|
775
867
|
if nextver == version and args.keep:
|
776
|
-
print(
|
777
|
-
|
778
|
-
|
868
|
+
print(
|
869
|
+
f'Error: {fmt(version, release)} would not be kept '
|
870
|
+
f'if update to {fmt(nextver, release_target)} '
|
871
|
+
f'distribution="{distribution}"',
|
872
|
+
file=sys.stderr,
|
873
|
+
)
|
779
874
|
continue
|
780
875
|
|
781
876
|
new_vdir = args._versions / nextver
|
782
877
|
if nextver != version and new_vdir.exists():
|
783
878
|
continue
|
784
879
|
|
785
|
-
print(
|
786
|
-
|
787
|
-
|
880
|
+
print(
|
881
|
+
f'{fmt(version, release)} updating to '
|
882
|
+
f'{fmt(nextver, release_target)} '
|
883
|
+
f'distribution="{distribution}" ..'
|
884
|
+
)
|
788
885
|
|
789
886
|
# If the source was originally included, then include it in
|
790
887
|
# the update.
|
791
888
|
args.include_source = (vdir / 'src').is_dir()
|
792
889
|
|
793
|
-
if error := install(args, new_vdir, release_target, distribution,
|
794
|
-
files):
|
890
|
+
if error := install(args, new_vdir, release_target, distribution, files):
|
795
891
|
return error
|
796
892
|
|
797
893
|
if nextver != version and not args.keep:
|
798
894
|
remove(args, version)
|
799
895
|
|
800
|
-
|
801
|
-
|
802
|
-
|
896
|
+
|
897
|
+
# COMMAND
|
898
|
+
class remove_:
|
899
|
+
"Remove/uninstall one, more, or all versions."
|
900
|
+
|
803
901
|
aliases = ['uninstall']
|
804
902
|
|
805
903
|
@staticmethod
|
806
904
|
def init(parser: ArgumentParser) -> None:
|
807
|
-
parser.add_argument(
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
905
|
+
parser.add_argument(
|
906
|
+
'-a', '--all', action='store_true', help='remove ALL versions'
|
907
|
+
)
|
908
|
+
parser.add_argument(
|
909
|
+
'--skip',
|
910
|
+
action='store_true',
|
911
|
+
help='skip the specified versions when '
|
912
|
+
'removing all (only can be specified with --all)',
|
913
|
+
)
|
914
|
+
parser.add_argument(
|
915
|
+
'-r',
|
916
|
+
'--release',
|
917
|
+
help='only remove versions if from specified '
|
918
|
+
f'YYYMMDD release (e.g. {SAMPL_RELEASE})',
|
919
|
+
)
|
920
|
+
parser.add_argument(
|
921
|
+
'version', nargs='*', help='version to remove (or to skip for --all --skip)'
|
922
|
+
)
|
818
923
|
|
819
924
|
@staticmethod
|
820
925
|
def run(args: Namespace) -> str | None:
|
821
926
|
release_del = args.release
|
822
|
-
if release_del and
|
823
|
-
(err := check_release_tag(release_del)):
|
927
|
+
if release_del and (err := check_release_tag(release_del)):
|
824
928
|
return err
|
825
929
|
|
826
930
|
for version in get_version_names(args):
|
@@ -830,20 +934,29 @@ class _remove(COMMAND):
|
|
830
934
|
remove(args, version)
|
831
935
|
print(f'Version {fmt(version, release)} removed.')
|
832
936
|
|
833
|
-
|
834
|
-
|
835
|
-
|
937
|
+
|
938
|
+
# COMMAND
|
939
|
+
class list_:
|
940
|
+
"List installed versions and show which have an update available."
|
941
|
+
|
836
942
|
@staticmethod
|
837
943
|
def init(parser: ArgumentParser) -> None:
|
838
|
-
parser.add_argument(
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
944
|
+
parser.add_argument(
|
945
|
+
'-v',
|
946
|
+
'--verbose',
|
947
|
+
action='store_true',
|
948
|
+
help='explicitly report why a version is not eligible for update',
|
949
|
+
)
|
950
|
+
parser.add_argument(
|
951
|
+
'-r',
|
952
|
+
'--release',
|
953
|
+
help='use specified YYYYMMDD release '
|
954
|
+
f'(e.g. {SAMPL_RELEASE}) for verbose compare, '
|
955
|
+
'default is latest release',
|
956
|
+
)
|
957
|
+
parser.add_argument(
|
958
|
+
'version', nargs='*', help='only list specified version, else all'
|
959
|
+
)
|
847
960
|
|
848
961
|
@staticmethod
|
849
962
|
def run(args: Namespace) -> str | None:
|
@@ -868,56 +981,72 @@ class _list(COMMAND):
|
|
868
981
|
nextver = matcher.match(version, upgrade=True)
|
869
982
|
if not nextver:
|
870
983
|
if args.verbose:
|
871
|
-
app =
|
872
|
-
|
873
|
-
|
984
|
+
app = (
|
985
|
+
' not eligible for update because '
|
986
|
+
f'release {release_target} does not provide '
|
987
|
+
'this version.'
|
988
|
+
)
|
874
989
|
else:
|
875
990
|
new_vdir = args._versions / nextver
|
876
991
|
if nextver != version and new_vdir.exists():
|
877
992
|
if args.verbose:
|
878
|
-
nrelease = get_json(
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
993
|
+
nrelease = get_json(new_vdir / args._data).get(
|
994
|
+
'release', '?'
|
995
|
+
)
|
996
|
+
app = (
|
997
|
+
f' not eligible for '
|
998
|
+
f'update because {fmt(nextver, nrelease)} '
|
999
|
+
'is already installed.'
|
1000
|
+
)
|
883
1001
|
else:
|
884
1002
|
# May not be updatable if newer release does not support
|
885
1003
|
# this same distribution anymore
|
886
1004
|
if nextver and distribution in files.get(nextver, {}):
|
887
|
-
upd = ' updatable to '
|
888
|
-
f'{fmt(nextver, release_target)}'
|
1005
|
+
upd = f' updatable to {fmt(nextver, release_target)}'
|
889
1006
|
elif args.verbose:
|
890
|
-
app =
|
891
|
-
|
892
|
-
|
893
|
-
|
1007
|
+
app = (
|
1008
|
+
' not eligible for update because '
|
1009
|
+
f'{fmt(nextver, release_target)} does '
|
1010
|
+
'not provide '
|
1011
|
+
f'distribution="{distribution}".'
|
1012
|
+
)
|
1013
|
+
|
1014
|
+
print(f'{fmt(version, release)}{upd} distribution="{distribution}"{app}')
|
894
1015
|
|
895
|
-
print(f'{fmt(version, release)}{upd} '
|
896
|
-
f'distribution="{distribution}"{app}')
|
897
1016
|
|
898
|
-
|
899
|
-
class
|
900
|
-
doc = f
|
1017
|
+
# COMMAND
|
1018
|
+
class show_:
|
1019
|
+
doc = f"""
|
901
1020
|
Show versions available from a release.
|
902
1021
|
|
903
1022
|
View available releases and their distributions at
|
904
1023
|
{GITHUB_SITE}/releases.
|
905
|
-
|
1024
|
+
"""
|
906
1025
|
|
907
1026
|
@staticmethod
|
908
1027
|
def init(parser: ArgumentParser) -> None:
|
909
1028
|
group = parser.add_mutually_exclusive_group()
|
910
|
-
group.add_argument(
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
1029
|
+
group.add_argument(
|
1030
|
+
'-l', '--list', action='store_true', help='just list recent releases'
|
1031
|
+
)
|
1032
|
+
group.add_argument(
|
1033
|
+
'-r',
|
1034
|
+
'--release',
|
1035
|
+
help=f'{REPO} YYYYMMDD release to show (e.g. '
|
1036
|
+
f'{SAMPL_RELEASE}), default is latest release',
|
1037
|
+
)
|
1038
|
+
parser.add_argument(
|
1039
|
+
'-a',
|
1040
|
+
'--all',
|
1041
|
+
action='store_true',
|
1042
|
+
help='show all available distributions for each version from the release',
|
1043
|
+
)
|
1044
|
+
parser.add_argument(
|
1045
|
+
're_match',
|
1046
|
+
nargs='?',
|
1047
|
+
help='show only versions+distributions '
|
1048
|
+
'matching this regular expression pattern',
|
1049
|
+
)
|
921
1050
|
|
922
1051
|
@staticmethod
|
923
1052
|
def run(args: Namespace) -> str | None:
|
@@ -925,6 +1054,7 @@ class _show(COMMAND):
|
|
925
1054
|
args.parser.error('Can not specify --all with --list.')
|
926
1055
|
|
927
1056
|
if args.list:
|
1057
|
+
args.release = False
|
928
1058
|
show_list(args)
|
929
1059
|
return None
|
930
1060
|
|
@@ -936,44 +1066,56 @@ class _show(COMMAND):
|
|
936
1066
|
installed = {}
|
937
1067
|
for vdir in iter_versions(args):
|
938
1068
|
data = get_json(vdir / args._data)
|
939
|
-
if data.get('release') == release and
|
940
|
-
(distro := data.get('distribution')):
|
1069
|
+
if data.get('release') == release and (distro := data.get('distribution')):
|
941
1070
|
installed[vdir.name] = distro
|
942
1071
|
|
943
1072
|
installable = False
|
944
1073
|
for version in sorted(files, key=parse_version):
|
945
1074
|
installed_distribution = installed.get(version)
|
946
1075
|
for distribution in files[version]:
|
947
|
-
app = ' (installed)'
|
948
|
-
|
949
|
-
if args.all or app \
|
950
|
-
or distribution == args._distribution:
|
1076
|
+
app = ' (installed)' if distribution == installed_distribution else ''
|
1077
|
+
if args.all or app or distribution == args._distribution:
|
951
1078
|
if distribution == args._distribution:
|
952
1079
|
installable = True
|
953
1080
|
|
954
|
-
if not args.re_match or
|
955
|
-
|
956
|
-
|
957
|
-
print(
|
958
|
-
f'
|
1081
|
+
if not args.re_match or re.search(
|
1082
|
+
args.re_match, f'{version}+{distribution}'
|
1083
|
+
):
|
1084
|
+
print(
|
1085
|
+
f'{fmt(version, release)} '
|
1086
|
+
f'distribution="{distribution}"{app}'
|
1087
|
+
)
|
959
1088
|
if not installable:
|
960
|
-
print(
|
961
|
-
|
1089
|
+
print(
|
1090
|
+
f'Warning: no distribution="{args._distribution}" '
|
1091
|
+
'versions found in '
|
1092
|
+
f'release "{release}".'
|
1093
|
+
)
|
1094
|
+
|
1095
|
+
|
1096
|
+
# COMMAND
|
1097
|
+
class path_:
|
1098
|
+
"Show path prefix to installed version base directory."
|
962
1099
|
|
963
|
-
@COMMAND.add
|
964
|
-
class _path(COMMAND):
|
965
|
-
'Show path prefix to installed version base directory.'
|
966
1100
|
@staticmethod
|
967
1101
|
def init(parser: ArgumentParser) -> None:
|
968
|
-
parser.add_argument(
|
969
|
-
|
970
|
-
|
971
|
-
|
1102
|
+
parser.add_argument(
|
1103
|
+
'-p',
|
1104
|
+
'--python-path',
|
1105
|
+
action='store_true',
|
1106
|
+
help='add path to python executable',
|
1107
|
+
)
|
1108
|
+
parser.add_argument(
|
1109
|
+
'-r', '--resolve', action='store_true', help='fully resolve given version'
|
1110
|
+
)
|
972
1111
|
group = parser.add_mutually_exclusive_group()
|
973
|
-
group.add_argument(
|
974
|
-
|
975
|
-
|
976
|
-
|
1112
|
+
group.add_argument(
|
1113
|
+
'-c',
|
1114
|
+
'--cache-path',
|
1115
|
+
action='store_true',
|
1116
|
+
help='just show path to cache dir',
|
1117
|
+
)
|
1118
|
+
group.add_argument('version', nargs='?', help='version number to show path for')
|
977
1119
|
|
978
1120
|
@staticmethod
|
979
1121
|
def run(args: Namespace) -> str | None:
|
@@ -997,10 +1139,10 @@ class _path(COMMAND):
|
|
997
1139
|
if not path.exists():
|
998
1140
|
path = basepath / 'python.exe'
|
999
1141
|
if not path.exists():
|
1000
|
-
return 'Error: Can not find python executable in '
|
1001
|
-
f'"{basepath}"'
|
1142
|
+
return f'Error: Can not find python executable in "{basepath}"'
|
1002
1143
|
|
1003
1144
|
print(path)
|
1004
1145
|
|
1146
|
+
|
1005
1147
|
if __name__ == '__main__':
|
1006
1148
|
sys.exit(main())
|
pystand-2.12.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pystand.py,sha256=l-ecHjOFK35q5EiZY-FMufu_C_jz7wUom2EvVtSTAiw,36826
|
2
|
-
pystand-2.12.dist-info/METADATA,sha256=F1TMDKy9Invr4mCU08z1-yWEpL0SFHJQFBRnAak-7yU,24843
|
3
|
-
pystand-2.12.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
4
|
-
pystand-2.12.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
-
pystand-2.12.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
-
pystand-2.12.dist-info/RECORD,,
|
File without changes
|
File without changes
|