pystand 1.1__py3-none-any.whl → 1.3__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.1
3
+ Version: 1.3
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
@@ -18,8 +18,8 @@ Requires-Dist: pygithub
18
18
  [![PyPi](https://img.shields.io/pypi/v/pystand)](https://pypi.org/project/pystand/)
19
19
  [![AUR](https://img.shields.io/aur/version/pystand)](https://aur.archlinux.org/packages/pystand/)
20
20
 
21
- [`pystand`][pystand] is a command line tool to facilitate the
22
- installation and update of pre-built Python versions from the
21
+ [`pystand`][pystand] is a command line tool to facilitate the download,
22
+ installation, and update of pre-built Python versions from the
23
23
  [`python-build-standalone`][pbs] project. The following commands are
24
24
  provided:
25
25
 
@@ -117,11 +117,13 @@ Type `pystand` or `pystand -h` to view the usage summary:
117
117
 
118
118
  ```
119
119
  usage: pystand [-h] [-D DISTRIBUTION] [-B BASE_DIR] [-C CACHE_MINUTES]
120
- [--purge-days PURGE_DAYS] [-V]
120
+ [--purge-days PURGE_DAYS]
121
+ [--github-access-token GITHUB_ACCESS_TOKEN] [-V]
121
122
  {install,update,remove,list,show,path} ...
122
123
 
123
- Command line tool to install pre-built Python versions from the python-build-
124
- standalone project.
124
+ Command line tool to download, install, and update pre-built Python versions
125
+ from the python-build-standalone project at
126
+ https://github.com/indygreg/python-build-standalone.
125
127
 
126
128
  options:
127
129
  -h, --help show this help message and exit
@@ -138,8 +140,11 @@ options:
138
140
  before rechecking for latest. Default is 60 minutes
139
141
  --purge-days PURGE_DAYS
140
142
  cache release file lists for this number of days after
141
- last version referencing it is removed. Default is 30
143
+ last version referencing it is removed. Default is 90
142
144
  days
145
+ --github-access-token GITHUB_ACCESS_TOKEN
146
+ Optional Github access token. Can specify to reduce
147
+ rate limiting.
143
148
  -V show pystand version
144
149
 
145
150
  Commands:
@@ -181,7 +186,7 @@ options:
181
186
  ### Command `update`
182
187
 
183
188
  ```
184
- usage: pystand update [-h] [-r RELEASE] [-a] [--skip] [version ...]
189
+ usage: pystand update [-h] [-r RELEASE] [-a] [--skip] [-k] [version ...]
185
190
 
186
191
  Update one, more, or all versions to another release.
187
192
 
@@ -196,6 +201,8 @@ options:
196
201
  -a, --all update ALL versions
197
202
  --skip skip the specified versions when updating all (only
198
203
  can be specified with --all)
204
+ -k, --keep keep old version after updating (but only if different
205
+ version number)
199
206
  ```
200
207
 
201
208
  ### Command `remove`
@@ -330,8 +337,8 @@ anything after on a line) are ignored. Type `pystand` to see all
330
337
  supported options.
331
338
 
332
339
  The global options: `--distribution`, `--base-dir`, `--cache-minutes`,
333
- `--purge-days` are the only sensible candidates to consider setting as
334
- defaults.
340
+ `--purge-days`, `--github-access-token` are the only sensible candidates
341
+ to consider setting as defaults.
335
342
 
336
343
  ## Command Line Tab Completion
337
344
 
@@ -0,0 +1,6 @@
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,,
pystand.py CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/python3
2
2
  # PYTHON_ARGCOMPLETE_OK
3
3
  '''
4
- Command line tool to install pre-built Python versions from the
5
- python-build-standalone project.
4
+ Command line tool to download, install, and update pre-built Python
5
+ versions from the python-build-standalone project at
6
+ https://github.com/indygreg/python-build-standalone.
6
7
  '''
7
8
  from __future__ import annotations
8
9
 
@@ -30,6 +31,13 @@ GITHUB_REPO = f'{REPO_OWNER}/{REPO}'
30
31
  LATEST_RELEASE_URL = f'https://raw.githubusercontent.com/{GITHUB_REPO}'\
31
32
  '/latest-release/latest-release.json'
32
33
 
34
+ # The following release is the first one that supports "install_only"
35
+ # builds so this tool does not support any releases before this.
36
+ FIRST_RELEASE = '20210724'
37
+
38
+ # The following regexp pattern is used to match the end of a release file name
39
+ ENDPAT = r'-install_only[-\dT]*.tar.gz$'
40
+
33
41
  PROG = Path(__file__).stem
34
42
  CNFFILE = platformdirs.user_config_path(f'{PROG}-flags.conf')
35
43
 
@@ -98,7 +106,14 @@ def get_gh(args: Namespace) -> Any:
98
106
  return get_gh_handle
99
107
 
100
108
  from github import Github
101
- get_gh_handle = Github() # type: ignore
109
+ if args.github_access_token:
110
+ from github import Auth
111
+ auth = Auth.Token(args.github_access_token)
112
+ else:
113
+ auth = None
114
+
115
+ # Save this handle globally for future use
116
+ get_gh_handle = Github(auth=auth) # type: ignore
102
117
  return get_gh_handle
103
118
 
104
119
  def rm_path(path: Path) -> None:
@@ -168,8 +183,18 @@ def get_version_names(args: Namespace) -> list[str]:
168
183
  return sorted(all_names - given, key=parse_version) \
169
184
  if args.all else versions
170
185
 
171
- def get_latest_release_tag(args: Namespace) -> str:
172
- 'Return the latest release tag'
186
+ def get_release_tag(args: Namespace) -> str:
187
+ 'Return the release tag, or latest if not specified'
188
+ 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.')
195
+
196
+ return release
197
+
173
198
  if args._latest_release.exists():
174
199
  stat = args._latest_release.stat()
175
200
  if time.time() < (stat.st_mtime + int(args.cache_minutes * 60)):
@@ -192,11 +217,25 @@ def get_latest_release_tag(args: Namespace) -> str:
192
217
  args._latest_release.write_text(tag + '\n')
193
218
  return tag
194
219
 
220
+ def get_release_name(filename: str) -> Optional[str]:
221
+ 'Search for and strip the release name from the file name'
222
+ if not (m := re.search(ENDPAT, filename)):
223
+ return None
224
+
225
+ # Strip of end of file name and also strip any +8 digit embedded
226
+ # release date
227
+ return re.sub(r'\+\d{8}', '', filename[:m.start()])
228
+
195
229
  def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
196
230
  'Return the release files for the given tag'
197
231
  # Look for tag data in our release cache
198
232
  jfile = args._releases / tag
199
233
  if not (files := get_json(jfile)):
234
+
235
+ # May have read this release before but it has no assets
236
+ if jfile.exists():
237
+ return {}
238
+
200
239
  # Not in cache so fetch it (and also store in cache)
201
240
  gh = get_gh(args)
202
241
  try:
@@ -206,17 +245,11 @@ def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
206
245
 
207
246
  # Iterate over the release assets and store the files in a dict to
208
247
  # return
209
- end = '-install_only.tar.gz'
210
248
  for file in release.get_assets():
211
- name = file.name
212
- if not name.endswith(end):
249
+ if not (name := get_release_name(file.name)):
213
250
  continue
214
251
 
215
- name = name[:-len(end)]
216
- impl_ver, rest = name.split('+', maxsplit=1)
217
- impl, ver = impl_ver.split('-', maxsplit=1)
218
- rest = rest.split('-', maxsplit=1)[1]
219
-
252
+ impl, ver, rest = name.split('-', maxsplit=2)
220
253
  if impl not in files:
221
254
  files[impl] = defaultdict(dict)
222
255
 
@@ -378,10 +411,13 @@ def main() -> Optional[str]:
378
411
  help='cache latest release tag fetch for this many '
379
412
  'minutes, before rechecking for latest. '
380
413
  'Default is %(default)d minutes')
381
- opt.add_argument('--purge-days', default=30, type=int,
414
+ opt.add_argument('--purge-days', default=90, type=int,
382
415
  help='cache release file lists for this number '
383
416
  'of days after last version referencing it is removed. '
384
417
  'Default is %(default)d days')
418
+ opt.add_argument('--github-access-token',
419
+ help='Optional Github access token. Can specify to reduce '
420
+ 'rate limiting.')
385
421
  opt.add_argument('-V', action='store_true',
386
422
  help=f'show {PROG} version')
387
423
  cmd = opt.add_subparsers(title='Commands', dest='cmdname')
@@ -469,10 +505,10 @@ class _install(COMMAND):
469
505
 
470
506
  @staticmethod
471
507
  def run(args: Namespace) -> Optional[str]:
472
- release = args.release or get_latest_release_tag(args)
508
+ release = get_release_tag(args)
473
509
  files = get_release_files(args, release, 'cpython')
474
510
  if not files:
475
- return f'Release "{release}" not found.'
511
+ return f'Release "{release}" not found, or has no compatible files.'
476
512
 
477
513
  matcher = VersionMatcher(files)
478
514
  for version in args.version:
@@ -504,13 +540,16 @@ class _update(COMMAND):
504
540
  parser.add_argument('--skip', action='store_true',
505
541
  help='skip the specified versions when '
506
542
  'updating all (only can be specified with --all)')
543
+ parser.add_argument('-k', '--keep', action='store_true',
544
+ help='keep old version after updating (but only '
545
+ 'if different version number)')
507
546
  parser.add_argument('version', nargs='*',
508
547
  help='version to update (or to skip for '
509
548
  '--all --skip)')
510
549
 
511
550
  @staticmethod
512
551
  def run(args: Namespace) -> Optional[str]:
513
- release_target = args.release or get_latest_release_tag(args)
552
+ release_target = get_release_tag(args)
514
553
  files = get_release_files(args, release_target, 'cpython')
515
554
  if not files:
516
555
  return f'Release "{release_target}" not found.'
@@ -525,14 +564,21 @@ class _update(COMMAND):
525
564
  continue
526
565
 
527
566
  nextver = matcher.match(version, upconvert_minor=True)
528
- new_vdir = args._versions / nextver
529
- if nextver != version and new_vdir.exists():
530
- continue
531
567
 
532
568
  distribution = data.get('distribution')
533
569
  if not distribution or distribution not in files.get(nextver, {}):
534
570
  continue
535
571
 
572
+ if nextver == version and args.keep:
573
+ print(f'Error: {fmt(version, release)} would not be kept '
574
+ f'if update to {fmt(nextver, release_target)} '
575
+ f'distribution="{distribution}"', file=sys.stderr)
576
+ continue
577
+
578
+ new_vdir = args._versions / nextver
579
+ if nextver != version and new_vdir.exists():
580
+ continue
581
+
536
582
  print(f'{fmt(version, release)} updating to '
537
583
  f'{fmt(nextver, release_target)} '
538
584
  f'distribution="{distribution}" ..')
@@ -541,7 +587,7 @@ class _update(COMMAND):
541
587
  files):
542
588
  return error
543
589
 
544
- if nextver != version:
590
+ if nextver != version and not args.keep:
545
591
  remove(args, version)
546
592
 
547
593
  @COMMAND.add
@@ -586,7 +632,7 @@ class _list(COMMAND):
586
632
 
587
633
  @staticmethod
588
634
  def run(args: Namespace) -> Optional[str]:
589
- release_target = args.release or get_latest_release_tag(args)
635
+ release_target = get_release_tag(args)
590
636
  files = get_release_files(args, release_target, 'cpython')
591
637
  if not files:
592
638
  return f'Release "{release_target}" not found.'
@@ -640,7 +686,7 @@ class _show(COMMAND):
640
686
 
641
687
  @staticmethod
642
688
  def run(args: Namespace) -> None:
643
- release = args.release or get_latest_release_tag(args)
689
+ release = get_release_tag(args)
644
690
  files = get_release_files(args, release, 'cpython')
645
691
  if not files:
646
692
  sys.exit(f'Error: release "{release}" not found.')
@@ -1,6 +0,0 @@
1
- pystand.py,sha256=mZmfP9CHLmj2rtwqQuteY7dwpkqtsLsU6d8oPxGgeUI,25512
2
- pystand-1.1.dist-info/METADATA,sha256=CtnNqg_-4iv49ZXU_gyMVPCZUqiKgKcOyQ3l1kEZ9Pw,14093
3
- pystand-1.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
4
- pystand-1.1.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
5
- pystand-1.1.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
6
- pystand-1.1.dist-info/RECORD,,
File without changes