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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pystand
3
- Version: 1.3
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], [`pdm
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 minutes,
140
- before rechecking for latest. Default is 60 minutes
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 days after
143
- last version referencing it is removed. Default is 90
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 release
182
- (e.g. 20240415), default is latest release
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), default
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 (e.g.
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 verbose
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
- version of `3.12`, e.g. `3.12.3` (at the time of writing), and will install
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (70.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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 not release.isdigit() or len(release) != len(FIRST_RELEASE):
190
- sys.exit('Release must be a YYYYMMDD string.')
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
- path = Path(base / name)
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
- path = Path(base / name)
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
- for release in releases - keep:
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
- stat = rdir.stat()
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 many '
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. 20240415), '
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. 20240415), '
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
- 'specified release (e.g. 20240415)')
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 args.release or release == args.release:
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 (e.g. 20240415) for '
629
- 'verbose compare, default is latest release')
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. 20240415), '
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:
@@ -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,,