pystand 1.2__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.2.dist-info → pystand-1.4.dist-info}/METADATA +29 -23
- pystand-1.4.dist-info/RECORD +6 -0
- {pystand-1.2.dist-info → pystand-1.4.dist-info}/WHEEL +1 -1
- pystand.py +89 -38
- pystand-1.2.dist-info/RECORD +0 -6
- {pystand-1.2.dist-info → pystand-1.4.dist-info}/entry_points.txt +0 -0
- {pystand-1.2.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
|
@@ -117,7 +117,8 @@ 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]
|
120
|
+
[--purge-days PURGE_DAYS]
|
121
|
+
[--github-access-token GITHUB_ACCESS_TOKEN] [-V]
|
121
122
|
{install,update,remove,list,show,path} ...
|
122
123
|
|
123
124
|
Command line tool to download, install, and update pre-built Python versions
|
@@ -135,12 +136,16 @@ options:
|
|
135
136
|
specify pystand base dir for storing versions and
|
136
137
|
metadata. Default is "$HOME/.local/share/pystand"
|
137
138
|
-C CACHE_MINUTES, --cache-minutes CACHE_MINUTES
|
138
|
-
cache latest release tag fetch for this many
|
139
|
-
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
|
140
142
|
--purge-days PURGE_DAYS
|
141
|
-
cache release file lists for this number of
|
142
|
-
last version referencing it is removed.
|
143
|
-
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
|
146
|
+
--github-access-token GITHUB_ACCESS_TOKEN
|
147
|
+
Optional Github access token. Can specify to reduce
|
148
|
+
rate limiting.
|
144
149
|
-V show pystand version
|
145
150
|
|
146
151
|
Commands:
|
@@ -174,8 +179,9 @@ positional arguments:
|
|
174
179
|
options:
|
175
180
|
-h, --help show this help message and exit
|
176
181
|
-r RELEASE, --release RELEASE
|
177
|
-
install from specified python-build-standalone
|
178
|
-
(e.g. 20240415), default is latest
|
182
|
+
install from specified python-build-standalone
|
183
|
+
YYYYMMDD release (e.g. 20240415), default is latest
|
184
|
+
release
|
179
185
|
-f, --force force install even if already installed
|
180
186
|
```
|
181
187
|
|
@@ -192,8 +198,8 @@ positional arguments:
|
|
192
198
|
options:
|
193
199
|
-h, --help show this help message and exit
|
194
200
|
-r RELEASE, --release RELEASE
|
195
|
-
update to specified release (e.g. 20240415),
|
196
|
-
is latest release
|
201
|
+
update to specified YYYMMDD release (e.g. 20240415),
|
202
|
+
default is latest release
|
197
203
|
-a, --all update ALL versions
|
198
204
|
--skip skip the specified versions when updating all (only
|
199
205
|
can be specified with --all)
|
@@ -217,8 +223,8 @@ options:
|
|
217
223
|
--skip skip the specified versions when removing all (only
|
218
224
|
can be specified with --all)
|
219
225
|
-r RELEASE, --release RELEASE
|
220
|
-
only remove versions if from specified release
|
221
|
-
20240415)
|
226
|
+
only remove versions if from specified YYYMMDD release
|
227
|
+
(e.g. 20240415)
|
222
228
|
```
|
223
229
|
|
224
230
|
### Command `list`
|
@@ -236,8 +242,8 @@ options:
|
|
236
242
|
-v, --verbose explicitly report why a version is not eligible for
|
237
243
|
update
|
238
244
|
-r RELEASE, --release RELEASE
|
239
|
-
use specified release (e.g. 20240415) for
|
240
|
-
compare, default is latest release
|
245
|
+
use specified YYYYMMDD release (e.g. 20240415) for
|
246
|
+
verbose compare, default is latest release
|
241
247
|
```
|
242
248
|
|
243
249
|
### Command `show`
|
@@ -248,7 +254,7 @@ usage: pystand show [-h] [-d] [release]
|
|
248
254
|
Show versions available from a release.
|
249
255
|
|
250
256
|
positional arguments:
|
251
|
-
release python-build-standalone release to show (e.g.
|
257
|
+
release python-build-standalone YYYYMMDD release to show (e.g.
|
252
258
|
20240415), default is latest release
|
253
259
|
|
254
260
|
options:
|
@@ -302,8 +308,8 @@ $ pipx uninstall pystand
|
|
302
308
|
`pystand` extrapolates any version text you specify on the command line
|
303
309
|
to the latest available corresponding installed or release version. For
|
304
310
|
example, if you specify `pystand install 3.12` then `pystand` will look
|
305
|
-
in the release files to find the latest (i.e. highest) available
|
306
|
-
|
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
|
307
313
|
that. Of course you can specify the exact version if you wish, e.g.
|
308
314
|
`3.12.3` but generally you don't need to bother. This is true for any
|
309
315
|
command that takes a version argument so be aware that this may be
|
@@ -333,8 +339,8 @@ anything after on a line) are ignored. Type `pystand` to see all
|
|
333
339
|
supported options.
|
334
340
|
|
335
341
|
The global options: `--distribution`, `--base-dir`, `--cache-minutes`,
|
336
|
-
`--purge-days` are the only sensible candidates
|
337
|
-
defaults.
|
342
|
+
`--purge-days`, `--github-access-token` are the only sensible candidates
|
343
|
+
to consider setting as defaults.
|
338
344
|
|
339
345
|
## Command Line Tab Completion
|
340
346
|
|
@@ -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
|
|
@@ -31,6 +32,16 @@ GITHUB_REPO = f'{REPO_OWNER}/{REPO}'
|
|
31
32
|
LATEST_RELEASE_URL = f'https://raw.githubusercontent.com/{GITHUB_REPO}'\
|
32
33
|
'/latest-release/latest-release.json'
|
33
34
|
|
35
|
+
# The following release is the first one that supports "install_only"
|
36
|
+
# builds so this tool does not support any releases before this.
|
37
|
+
FIRST_RELEASE = '20210724'
|
38
|
+
|
39
|
+
# Sample release tag for documentation/usage examples
|
40
|
+
SAMPL_RELEASE = '20240415'
|
41
|
+
|
42
|
+
# The following regexp pattern is used to match the end of a release file name
|
43
|
+
ENDPAT = r'-install_only[-\dT]*.tar.gz$'
|
44
|
+
|
34
45
|
PROG = Path(__file__).stem
|
35
46
|
CNFFILE = platformdirs.user_config_path(f'{PROG}-flags.conf')
|
36
47
|
|
@@ -99,7 +110,14 @@ def get_gh(args: Namespace) -> Any:
|
|
99
110
|
return get_gh_handle
|
100
111
|
|
101
112
|
from github import Github
|
102
|
-
|
113
|
+
if args.github_access_token:
|
114
|
+
from github import Auth
|
115
|
+
auth = Auth.Token(args.github_access_token)
|
116
|
+
else:
|
117
|
+
auth = None
|
118
|
+
|
119
|
+
# Save this handle globally for future use
|
120
|
+
get_gh_handle = Github(auth=auth) # type: ignore
|
103
121
|
return get_gh_handle
|
104
122
|
|
105
123
|
def rm_path(path: Path) -> None:
|
@@ -169,8 +187,30 @@ def get_version_names(args: Namespace) -> list[str]:
|
|
169
187
|
return sorted(all_names - given, key=parse_version) \
|
170
188
|
if args.all else versions
|
171
189
|
|
172
|
-
def
|
173
|
-
|
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
|
+
|
206
|
+
def get_release_tag(args: Namespace) -> str:
|
207
|
+
'Return the release tag, or latest if not specified'
|
208
|
+
if release := args.release:
|
209
|
+
if err := check_release_tag(release):
|
210
|
+
sys.exit(err)
|
211
|
+
|
212
|
+
return release
|
213
|
+
|
174
214
|
if args._latest_release.exists():
|
175
215
|
stat = args._latest_release.stat()
|
176
216
|
if time.time() < (stat.st_mtime + int(args.cache_minutes * 60)):
|
@@ -183,16 +223,25 @@ def get_latest_release_tag(args: Namespace) -> str:
|
|
183
223
|
with urllib.request.urlopen(LATEST_RELEASE_URL) as url:
|
184
224
|
data = json.load(url)
|
185
225
|
except Exception:
|
186
|
-
sys.exit('Failed to fetch latest release tag.')
|
226
|
+
sys.exit('Failed to fetch latest YYYYMMDD release tag.')
|
187
227
|
|
188
228
|
tag = data.get('tag')
|
189
229
|
|
190
230
|
if not tag:
|
191
|
-
sys.exit('Latest release tag timestamp file is corrupted.')
|
231
|
+
sys.exit('Latest YYYYMMDD release tag timestamp file is corrupted.')
|
192
232
|
|
193
233
|
args._latest_release.write_text(tag + '\n')
|
194
234
|
return tag
|
195
235
|
|
236
|
+
def get_release_name(filename: str) -> Optional[str]:
|
237
|
+
'Search for and strip the release name from the file name'
|
238
|
+
if not (m := re.search(ENDPAT, filename)):
|
239
|
+
return None
|
240
|
+
|
241
|
+
# Strip of end of file name and also strip any +8 digit embedded
|
242
|
+
# release date
|
243
|
+
return re.sub(r'\+\d{8}', '', filename[:m.start()])
|
244
|
+
|
196
245
|
def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
|
197
246
|
'Return the release files for the given tag'
|
198
247
|
# Look for tag data in our release cache
|
@@ -212,17 +261,11 @@ def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
|
|
212
261
|
|
213
262
|
# Iterate over the release assets and store the files in a dict to
|
214
263
|
# return
|
215
|
-
end = '-install_only.tar.gz'
|
216
264
|
for file in release.get_assets():
|
217
|
-
name
|
218
|
-
if not name.endswith(end):
|
265
|
+
if not (name := get_release_name(file.name)):
|
219
266
|
continue
|
220
267
|
|
221
|
-
|
222
|
-
impl_ver, rest = name.split('+', maxsplit=1)
|
223
|
-
impl, ver = impl_ver.split('-', maxsplit=1)
|
224
|
-
rest = rest.split('-', maxsplit=1)[1]
|
225
|
-
|
268
|
+
impl, ver, rest = name.split('-', maxsplit=2)
|
226
269
|
if impl not in files:
|
227
270
|
files[impl] = defaultdict(dict)
|
228
271
|
|
@@ -265,15 +308,13 @@ def update_version_symlinks(args: Namespace) -> None:
|
|
265
308
|
for name, tgt in oldlinks.items():
|
266
309
|
new_tgt = newlinks.get(name)
|
267
310
|
if not new_tgt or new_tgt != tgt:
|
268
|
-
|
269
|
-
path.unlink()
|
311
|
+
Path(base / name).unlink()
|
270
312
|
|
271
313
|
# Create all needed new links
|
272
314
|
for name, tgt in newlinks.items():
|
273
315
|
old_tgt = oldlinks.get(name)
|
274
316
|
if not old_tgt or old_tgt != tgt:
|
275
|
-
|
276
|
-
path.symlink_to(tgt, target_is_directory=True)
|
317
|
+
Path(base / name).symlink_to(tgt, target_is_directory=True)
|
277
318
|
|
278
319
|
def purge_unused_releases(args: Namespace) -> None:
|
279
320
|
'Purge old releases that are no longer needed and have expired'
|
@@ -286,10 +327,11 @@ def purge_unused_releases(args: Namespace) -> None:
|
|
286
327
|
if (release := get_json(version / args._data).get('release')):
|
287
328
|
keep.add(release)
|
288
329
|
|
289
|
-
|
330
|
+
now_secs = time.time()
|
331
|
+
end_secs = args.purge_days * 86400
|
332
|
+
for release in (releases - keep):
|
290
333
|
rdir = args._releases / release
|
291
|
-
|
292
|
-
if time.time() > (stat.st_mtime + args.purge_days * 86400):
|
334
|
+
if (rdir.stat().st_mtime + end_secs) < now_secs:
|
293
335
|
rdir.unlink()
|
294
336
|
|
295
337
|
class COMMAND:
|
@@ -381,13 +423,16 @@ def main() -> Optional[str]:
|
|
381
423
|
help=f'specify {PROG} base dir for storing '
|
382
424
|
'versions and metadata. Default is "%(default)s"')
|
383
425
|
opt.add_argument('-C', '--cache-minutes', default=60, type=float,
|
384
|
-
help='cache latest release tag fetch for this
|
385
|
-
'minutes, before rechecking for latest. '
|
426
|
+
help='cache latest YYYYMMDD release tag fetch for this '
|
427
|
+
'many minutes, before rechecking for latest. '
|
386
428
|
'Default is %(default)d minutes')
|
387
|
-
opt.add_argument('--purge-days', default=
|
388
|
-
help='cache release file lists for this number '
|
429
|
+
opt.add_argument('--purge-days', default=90, type=int,
|
430
|
+
help='cache YYYYMMDD release file lists for this number '
|
389
431
|
'of days after last version referencing it is removed. '
|
390
432
|
'Default is %(default)d days')
|
433
|
+
opt.add_argument('--github-access-token',
|
434
|
+
help='Optional Github access token. Can specify to reduce '
|
435
|
+
'rate limiting.')
|
391
436
|
opt.add_argument('-V', action='store_true',
|
392
437
|
help=f'show {PROG} version')
|
393
438
|
cmd = opt.add_subparsers(title='Commands', dest='cmdname')
|
@@ -466,7 +511,7 @@ class _install(COMMAND):
|
|
466
511
|
def init(parser: ArgumentParser) -> None:
|
467
512
|
parser.add_argument('-r', '--release',
|
468
513
|
help=f'install from specified {REPO} '
|
469
|
-
'release (e.g.
|
514
|
+
f'YYYYMMDD release (e.g. {SAMPL_RELEASE}), '
|
470
515
|
'default is latest release')
|
471
516
|
parser.add_argument('-f', '--force', action='store_true',
|
472
517
|
help='force install even if already installed')
|
@@ -475,7 +520,7 @@ class _install(COMMAND):
|
|
475
520
|
|
476
521
|
@staticmethod
|
477
522
|
def run(args: Namespace) -> Optional[str]:
|
478
|
-
release =
|
523
|
+
release = get_release_tag(args)
|
479
524
|
files = get_release_files(args, release, 'cpython')
|
480
525
|
if not files:
|
481
526
|
return f'Release "{release}" not found, or has no compatible files.'
|
@@ -503,8 +548,8 @@ class _update(COMMAND):
|
|
503
548
|
@staticmethod
|
504
549
|
def init(parser: ArgumentParser) -> None:
|
505
550
|
parser.add_argument('-r', '--release',
|
506
|
-
help='update to specified release (e.g.
|
507
|
-
'default is latest release')
|
551
|
+
help='update to specified YYYMMDD release (e.g. '
|
552
|
+
f'{SAMPL_RELEASE}), default is latest release')
|
508
553
|
parser.add_argument('-a', '--all', action='store_true',
|
509
554
|
help='update ALL versions')
|
510
555
|
parser.add_argument('--skip', action='store_true',
|
@@ -519,7 +564,7 @@ class _update(COMMAND):
|
|
519
564
|
|
520
565
|
@staticmethod
|
521
566
|
def run(args: Namespace) -> Optional[str]:
|
522
|
-
release_target =
|
567
|
+
release_target = get_release_tag(args)
|
523
568
|
files = get_release_files(args, release_target, 'cpython')
|
524
569
|
if not files:
|
525
570
|
return f'Release "{release_target}" not found.'
|
@@ -571,18 +616,23 @@ class _remove(COMMAND):
|
|
571
616
|
help='skip the specified versions when '
|
572
617
|
'removing all (only can be specified with --all)')
|
573
618
|
parser.add_argument('-r', '--release',
|
574
|
-
help='only remove versions if from '
|
575
|
-
'
|
619
|
+
help='only remove versions if from specified '
|
620
|
+
f'YYYMMDD release (e.g. {SAMPL_RELEASE})')
|
576
621
|
parser.add_argument('version', nargs='*',
|
577
622
|
help='version to remove (or to skip for '
|
578
623
|
'--all --skip)')
|
579
624
|
|
580
625
|
@staticmethod
|
581
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
|
+
|
582
632
|
for version in get_version_names(args):
|
583
633
|
dfile = args._versions / version / args._data
|
584
634
|
release = get_json(dfile).get('release') or '?'
|
585
|
-
if not
|
635
|
+
if not release_del or release == release_del:
|
586
636
|
remove(args, version)
|
587
637
|
print(f'Version {fmt(version, release)} removed.')
|
588
638
|
|
@@ -595,14 +645,15 @@ class _list(COMMAND):
|
|
595
645
|
help='explicitly report why a version is '
|
596
646
|
'not eligible for update')
|
597
647
|
parser.add_argument('-r', '--release',
|
598
|
-
help='use specified release
|
599
|
-
'verbose compare,
|
648
|
+
help='use specified YYYYMMDD release '
|
649
|
+
f'(e.g. {SAMPL_RELEASE}) for verbose compare, '
|
650
|
+
'default is latest release')
|
600
651
|
parser.add_argument('version', nargs='*',
|
601
652
|
help='only list specified version, else all')
|
602
653
|
|
603
654
|
@staticmethod
|
604
655
|
def run(args: Namespace) -> Optional[str]:
|
605
|
-
release_target =
|
656
|
+
release_target = get_release_tag(args)
|
606
657
|
files = get_release_files(args, release_target, 'cpython')
|
607
658
|
if not files:
|
608
659
|
return f'Release "{release_target}" not found.'
|
@@ -651,12 +702,12 @@ class _show(COMMAND):
|
|
651
702
|
help='also show all available distributions for '
|
652
703
|
'each version from the release')
|
653
704
|
parser.add_argument('release', nargs='?',
|
654
|
-
help=f'{REPO} release to show (e.g.
|
655
|
-
'default is latest release')
|
705
|
+
help=f'{REPO} YYYYMMDD release to show (e.g. '
|
706
|
+
f'{SAMPL_RELEASE}), default is latest release')
|
656
707
|
|
657
708
|
@staticmethod
|
658
709
|
def run(args: Namespace) -> None:
|
659
|
-
release =
|
710
|
+
release = get_release_tag(args)
|
660
711
|
files = get_release_files(args, release, 'cpython')
|
661
712
|
if not files:
|
662
713
|
sys.exit(f'Error: release "{release}" not found.')
|
pystand-1.2.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pystand.py,sha256=5X--S1zNpaEauP4FkubSe9J5FM3qQ1OO92ZcPlk__4I,26248
|
2
|
-
pystand-1.2.dist-info/METADATA,sha256=V72dn_zsBb0W0ym4lYh3EljiaOqY0s2l9fuszbK_EFg,14304
|
3
|
-
pystand-1.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
4
|
-
pystand-1.2.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
-
pystand-1.2.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
-
pystand-1.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|