pystand 1.3__py3-none-any.whl → 1.4__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-1.3.dist-info → pystand-1.4.dist-info}/METADATA +22 -20
- pystand-1.4.dist-info/RECORD +6 -0
- {pystand-1.3.dist-info → pystand-1.4.dist-info}/WHEEL +1 -1
- pystand.py +49 -28
- pystand-1.3.dist-info/RECORD +0 -6
- {pystand-1.3.dist-info → pystand-1.4.dist-info}/entry_points.txt +0 -0
- {pystand-1.3.dist-info → pystand-1.4.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pystand
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.4
|
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
|
@@ -33,7 +33,7 @@ provided:
|
|
33
33
|
|`path` |Show path prefix to installed version base directory |
|
34
34
|
|
35
35
|
By default, Python versions are sourced from the latest
|
36
|
-
`python-build-standalone` [release][pbs-rel] available but you can
|
36
|
+
`python-build-standalone` [release][pbs-rel] available (e.g. "`20240415`") but you can
|
37
37
|
optionally specify any older release. The required
|
38
38
|
[distribution](https://gregoryszorc.com/docs/python-build-standalone/main/running.html)
|
39
39
|
for your machine architecture is normally auto-detected but can be
|
@@ -99,8 +99,8 @@ $ pipx install --python $(pystand path -p 3.12) cowsay
|
|
99
99
|
See detailed usage information in the [Usage](#usage) section that
|
100
100
|
follows.
|
101
101
|
|
102
|
-
Note that unlike nearly all similar tools such as [`pyenv`][pyenv],
|
103
|
-
python`][pdmpy], and [`hatch python`][hatchpy], `pystand` directly
|
102
|
+
Note that unlike nearly all similar tools such as [`pyenv`][pyenv],
|
103
|
+
[`pdm python`][pdmpy], and [`hatch python`][hatchpy], `pystand` directly
|
104
104
|
checks the [`python-build-standalone`][pbs] github site to fetch for new
|
105
105
|
[releases][pbs-rel] but those other tools require a software update
|
106
106
|
before they can see new releases. This means that Python updates are
|
@@ -136,12 +136,13 @@ options:
|
|
136
136
|
specify pystand base dir for storing versions and
|
137
137
|
metadata. Default is "$HOME/.local/share/pystand"
|
138
138
|
-C CACHE_MINUTES, --cache-minutes CACHE_MINUTES
|
139
|
-
cache latest release tag fetch for this many
|
140
|
-
before rechecking for latest. Default is 60
|
139
|
+
cache latest YYYYMMDD release tag fetch for this many
|
140
|
+
minutes, before rechecking for latest. Default is 60
|
141
|
+
minutes
|
141
142
|
--purge-days PURGE_DAYS
|
142
|
-
cache release file lists for this number of
|
143
|
-
last version referencing it is removed.
|
144
|
-
days
|
143
|
+
cache YYYYMMDD release file lists for this number of
|
144
|
+
days after last version referencing it is removed.
|
145
|
+
Default is 90 days
|
145
146
|
--github-access-token GITHUB_ACCESS_TOKEN
|
146
147
|
Optional Github access token. Can specify to reduce
|
147
148
|
rate limiting.
|
@@ -178,8 +179,9 @@ positional arguments:
|
|
178
179
|
options:
|
179
180
|
-h, --help show this help message and exit
|
180
181
|
-r RELEASE, --release RELEASE
|
181
|
-
install from specified python-build-standalone
|
182
|
-
(e.g. 20240415), default is latest
|
182
|
+
install from specified python-build-standalone
|
183
|
+
YYYYMMDD release (e.g. 20240415), default is latest
|
184
|
+
release
|
183
185
|
-f, --force force install even if already installed
|
184
186
|
```
|
185
187
|
|
@@ -196,8 +198,8 @@ positional arguments:
|
|
196
198
|
options:
|
197
199
|
-h, --help show this help message and exit
|
198
200
|
-r RELEASE, --release RELEASE
|
199
|
-
update to specified release (e.g. 20240415),
|
200
|
-
is latest release
|
201
|
+
update to specified YYYMMDD release (e.g. 20240415),
|
202
|
+
default is latest release
|
201
203
|
-a, --all update ALL versions
|
202
204
|
--skip skip the specified versions when updating all (only
|
203
205
|
can be specified with --all)
|
@@ -221,8 +223,8 @@ options:
|
|
221
223
|
--skip skip the specified versions when removing all (only
|
222
224
|
can be specified with --all)
|
223
225
|
-r RELEASE, --release RELEASE
|
224
|
-
only remove versions if from specified release
|
225
|
-
20240415)
|
226
|
+
only remove versions if from specified YYYMMDD release
|
227
|
+
(e.g. 20240415)
|
226
228
|
```
|
227
229
|
|
228
230
|
### Command `list`
|
@@ -240,8 +242,8 @@ options:
|
|
240
242
|
-v, --verbose explicitly report why a version is not eligible for
|
241
243
|
update
|
242
244
|
-r RELEASE, --release RELEASE
|
243
|
-
use specified release (e.g. 20240415) for
|
244
|
-
compare, default is latest release
|
245
|
+
use specified YYYYMMDD release (e.g. 20240415) for
|
246
|
+
verbose compare, default is latest release
|
245
247
|
```
|
246
248
|
|
247
249
|
### Command `show`
|
@@ -252,7 +254,7 @@ usage: pystand show [-h] [-d] [release]
|
|
252
254
|
Show versions available from a release.
|
253
255
|
|
254
256
|
positional arguments:
|
255
|
-
release python-build-standalone release to show (e.g.
|
257
|
+
release python-build-standalone YYYYMMDD release to show (e.g.
|
256
258
|
20240415), default is latest release
|
257
259
|
|
258
260
|
options:
|
@@ -306,8 +308,8 @@ $ pipx uninstall pystand
|
|
306
308
|
`pystand` extrapolates any version text you specify on the command line
|
307
309
|
to the latest available corresponding installed or release version. For
|
308
310
|
example, if you specify `pystand install 3.12` then `pystand` will look
|
309
|
-
in the release files to find the latest (i.e. highest) available
|
310
|
-
|
311
|
+
in the release files to find the latest (i.e. highest) available version
|
312
|
+
of `3.12`, e.g. `3.12.3` (at the time of writing), and will install
|
311
313
|
that. Of course you can specify the exact version if you wish, e.g.
|
312
314
|
`3.12.3` but generally you don't need to bother. This is true for any
|
313
315
|
command that takes a version argument so be aware that this may be
|
@@ -0,0 +1,6 @@
|
|
1
|
+
pystand.py,sha256=ZwryYWIROt0rscb3vQTx9uE8eZus4x7iUOhHeJBC3SA,28134
|
2
|
+
pystand-1.4.dist-info/METADATA,sha256=D-nGRE3vUfZFEJb-NsD74DgROuSzM2E_kcbmBQVvNBc,14679
|
3
|
+
pystand-1.4.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
|
4
|
+
pystand-1.4.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
+
pystand-1.4.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
+
pystand-1.4.dist-info/RECORD,,
|
pystand.py
CHANGED
@@ -18,6 +18,7 @@ import time
|
|
18
18
|
import urllib.request
|
19
19
|
from argparse import ArgumentParser, Namespace
|
20
20
|
from collections import defaultdict
|
21
|
+
from datetime import date
|
21
22
|
from pathlib import Path
|
22
23
|
from typing import Any, Iterable, Iterator, Optional
|
23
24
|
|
@@ -35,6 +36,9 @@ LATEST_RELEASE_URL = f'https://raw.githubusercontent.com/{GITHUB_REPO}'\
|
|
35
36
|
# builds so this tool does not support any releases before this.
|
36
37
|
FIRST_RELEASE = '20210724'
|
37
38
|
|
39
|
+
# Sample release tag for documentation/usage examples
|
40
|
+
SAMPL_RELEASE = '20240415'
|
41
|
+
|
38
42
|
# The following regexp pattern is used to match the end of a release file name
|
39
43
|
ENDPAT = r'-install_only[-\dT]*.tar.gz$'
|
40
44
|
|
@@ -183,15 +187,27 @@ def get_version_names(args: Namespace) -> list[str]:
|
|
183
187
|
return sorted(all_names - given, key=parse_version) \
|
184
188
|
if args.all else versions
|
185
189
|
|
190
|
+
def check_release_tag(release: str, *,
|
191
|
+
check_first: bool = True) -> Optional[str]:
|
192
|
+
'Check the specified release tag is valid'
|
193
|
+
if not release.isdigit() or len(release) != len(FIRST_RELEASE):
|
194
|
+
return 'Release must be a YYYYMMDD string.'
|
195
|
+
|
196
|
+
try:
|
197
|
+
_ = date.fromisoformat(f'{release[:4]}-{release[4:6]}-{release[6:]}')
|
198
|
+
except Exception:
|
199
|
+
return 'Release must be a YYYYMMDD date string.'
|
200
|
+
|
201
|
+
if check_first and release < FIRST_RELEASE:
|
202
|
+
return f'Releases before "{FIRST_RELEASE}" do not include '\
|
203
|
+
'"*-install_only" builds so are not supported.'
|
204
|
+
return None
|
205
|
+
|
186
206
|
def get_release_tag(args: Namespace) -> str:
|
187
207
|
'Return the release tag, or latest if not specified'
|
188
208
|
if release := args.release:
|
189
|
-
if
|
190
|
-
sys.exit(
|
191
|
-
if release < FIRST_RELEASE:
|
192
|
-
sys.exit(f'Releases before "{FIRST_RELEASE}" do not '
|
193
|
-
'include "*-install_only" builds so are '
|
194
|
-
'not supported.')
|
209
|
+
if err := check_release_tag(release):
|
210
|
+
sys.exit(err)
|
195
211
|
|
196
212
|
return release
|
197
213
|
|
@@ -207,12 +223,12 @@ def get_release_tag(args: Namespace) -> str:
|
|
207
223
|
with urllib.request.urlopen(LATEST_RELEASE_URL) as url:
|
208
224
|
data = json.load(url)
|
209
225
|
except Exception:
|
210
|
-
sys.exit('Failed to fetch latest release tag.')
|
226
|
+
sys.exit('Failed to fetch latest YYYYMMDD release tag.')
|
211
227
|
|
212
228
|
tag = data.get('tag')
|
213
229
|
|
214
230
|
if not tag:
|
215
|
-
sys.exit('Latest release tag timestamp file is corrupted.')
|
231
|
+
sys.exit('Latest YYYYMMDD release tag timestamp file is corrupted.')
|
216
232
|
|
217
233
|
args._latest_release.write_text(tag + '\n')
|
218
234
|
return tag
|
@@ -292,15 +308,13 @@ def update_version_symlinks(args: Namespace) -> None:
|
|
292
308
|
for name, tgt in oldlinks.items():
|
293
309
|
new_tgt = newlinks.get(name)
|
294
310
|
if not new_tgt or new_tgt != tgt:
|
295
|
-
|
296
|
-
path.unlink()
|
311
|
+
Path(base / name).unlink()
|
297
312
|
|
298
313
|
# Create all needed new links
|
299
314
|
for name, tgt in newlinks.items():
|
300
315
|
old_tgt = oldlinks.get(name)
|
301
316
|
if not old_tgt or old_tgt != tgt:
|
302
|
-
|
303
|
-
path.symlink_to(tgt, target_is_directory=True)
|
317
|
+
Path(base / name).symlink_to(tgt, target_is_directory=True)
|
304
318
|
|
305
319
|
def purge_unused_releases(args: Namespace) -> None:
|
306
320
|
'Purge old releases that are no longer needed and have expired'
|
@@ -313,10 +327,11 @@ def purge_unused_releases(args: Namespace) -> None:
|
|
313
327
|
if (release := get_json(version / args._data).get('release')):
|
314
328
|
keep.add(release)
|
315
329
|
|
316
|
-
|
330
|
+
now_secs = time.time()
|
331
|
+
end_secs = args.purge_days * 86400
|
332
|
+
for release in (releases - keep):
|
317
333
|
rdir = args._releases / release
|
318
|
-
|
319
|
-
if time.time() > (stat.st_mtime + args.purge_days * 86400):
|
334
|
+
if (rdir.stat().st_mtime + end_secs) < now_secs:
|
320
335
|
rdir.unlink()
|
321
336
|
|
322
337
|
class COMMAND:
|
@@ -408,11 +423,11 @@ def main() -> Optional[str]:
|
|
408
423
|
help=f'specify {PROG} base dir for storing '
|
409
424
|
'versions and metadata. Default is "%(default)s"')
|
410
425
|
opt.add_argument('-C', '--cache-minutes', default=60, type=float,
|
411
|
-
help='cache latest release tag fetch for this
|
412
|
-
'minutes, before rechecking for latest. '
|
426
|
+
help='cache latest YYYYMMDD release tag fetch for this '
|
427
|
+
'many minutes, before rechecking for latest. '
|
413
428
|
'Default is %(default)d minutes')
|
414
429
|
opt.add_argument('--purge-days', default=90, type=int,
|
415
|
-
help='cache release file lists for this number '
|
430
|
+
help='cache YYYYMMDD release file lists for this number '
|
416
431
|
'of days after last version referencing it is removed. '
|
417
432
|
'Default is %(default)d days')
|
418
433
|
opt.add_argument('--github-access-token',
|
@@ -496,7 +511,7 @@ class _install(COMMAND):
|
|
496
511
|
def init(parser: ArgumentParser) -> None:
|
497
512
|
parser.add_argument('-r', '--release',
|
498
513
|
help=f'install from specified {REPO} '
|
499
|
-
'release (e.g.
|
514
|
+
f'YYYYMMDD release (e.g. {SAMPL_RELEASE}), '
|
500
515
|
'default is latest release')
|
501
516
|
parser.add_argument('-f', '--force', action='store_true',
|
502
517
|
help='force install even if already installed')
|
@@ -533,8 +548,8 @@ class _update(COMMAND):
|
|
533
548
|
@staticmethod
|
534
549
|
def init(parser: ArgumentParser) -> None:
|
535
550
|
parser.add_argument('-r', '--release',
|
536
|
-
help='update to specified release (e.g.
|
537
|
-
'default is latest release')
|
551
|
+
help='update to specified YYYMMDD release (e.g. '
|
552
|
+
f'{SAMPL_RELEASE}), default is latest release')
|
538
553
|
parser.add_argument('-a', '--all', action='store_true',
|
539
554
|
help='update ALL versions')
|
540
555
|
parser.add_argument('--skip', action='store_true',
|
@@ -601,18 +616,23 @@ class _remove(COMMAND):
|
|
601
616
|
help='skip the specified versions when '
|
602
617
|
'removing all (only can be specified with --all)')
|
603
618
|
parser.add_argument('-r', '--release',
|
604
|
-
help='only remove versions if from '
|
605
|
-
'
|
619
|
+
help='only remove versions if from specified '
|
620
|
+
f'YYYMMDD release (e.g. {SAMPL_RELEASE})')
|
606
621
|
parser.add_argument('version', nargs='*',
|
607
622
|
help='version to remove (or to skip for '
|
608
623
|
'--all --skip)')
|
609
624
|
|
610
625
|
@staticmethod
|
611
626
|
def run(args: Namespace) -> Optional[str]:
|
627
|
+
release_del = args.release
|
628
|
+
if release_del and \
|
629
|
+
(err := check_release_tag(release_del, check_first=False)):
|
630
|
+
return err
|
631
|
+
|
612
632
|
for version in get_version_names(args):
|
613
633
|
dfile = args._versions / version / args._data
|
614
634
|
release = get_json(dfile).get('release') or '?'
|
615
|
-
if not
|
635
|
+
if not release_del or release == release_del:
|
616
636
|
remove(args, version)
|
617
637
|
print(f'Version {fmt(version, release)} removed.')
|
618
638
|
|
@@ -625,8 +645,9 @@ class _list(COMMAND):
|
|
625
645
|
help='explicitly report why a version is '
|
626
646
|
'not eligible for update')
|
627
647
|
parser.add_argument('-r', '--release',
|
628
|
-
help='use specified release
|
629
|
-
'verbose compare,
|
648
|
+
help='use specified YYYYMMDD release '
|
649
|
+
f'(e.g. {SAMPL_RELEASE}) for verbose compare, '
|
650
|
+
'default is latest release')
|
630
651
|
parser.add_argument('version', nargs='*',
|
631
652
|
help='only list specified version, else all')
|
632
653
|
|
@@ -681,8 +702,8 @@ class _show(COMMAND):
|
|
681
702
|
help='also show all available distributions for '
|
682
703
|
'each version from the release')
|
683
704
|
parser.add_argument('release', nargs='?',
|
684
|
-
help=f'{REPO} release to show (e.g.
|
685
|
-
'default is latest release')
|
705
|
+
help=f'{REPO} YYYYMMDD release to show (e.g. '
|
706
|
+
f'{SAMPL_RELEASE}), default is latest release')
|
686
707
|
|
687
708
|
@staticmethod
|
688
709
|
def run(args: Namespace) -> None:
|
pystand-1.3.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pystand.py,sha256=OI2SWREnCD8Hyu8Xk6z6FZqPDM6NfRtku_Djb6nJ7gM,27357
|
2
|
-
pystand-1.3.dist-info/METADATA,sha256=aXLnhaYuWDwnbWEbo5eZb2mzAOklpihUEuWuAzoaztk,14550
|
3
|
-
pystand-1.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
4
|
-
pystand-1.3.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
-
pystand-1.3.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
-
pystand-1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|