dotlocalslashbin 0.0.25__tar.gz → 0.0.26__tar.gz
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.
- {dotlocalslashbin-0.0.25 → dotlocalslashbin-0.0.26}/PKG-INFO +2 -2
- {dotlocalslashbin-0.0.25 → dotlocalslashbin-0.0.26}/README.md +1 -1
- {dotlocalslashbin-0.0.25 → dotlocalslashbin-0.0.26}/src/dotlocalslashbin.py +53 -31
- {dotlocalslashbin-0.0.25 → dotlocalslashbin-0.0.26}/LICENSES/MPL-2.0.txt +0 -0
- {dotlocalslashbin-0.0.25 → dotlocalslashbin-0.0.26}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotlocalslashbin
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.26
|
|
4
4
|
Summary: Download and extract files to `~/.local/bin/`.
|
|
5
5
|
Author-email: Keith Maxwell <keith.maxwell@gmail.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -81,7 +81,7 @@ ignore = ["LICENSE", "README.md", "CHANGELOG.md"]
|
|
|
81
81
|
|
|
82
82
|
Command:
|
|
83
83
|
|
|
84
|
-
uv tool run dotlocalslashbin --
|
|
84
|
+
uv tool run dotlocalslashbin --output=. tofu.toml
|
|
85
85
|
|
|
86
86
|
Further examples are available in files like `linux-amd64.toml` and
|
|
87
87
|
`github.toml` in the `bin` directory of
|
|
@@ -57,7 +57,7 @@ ignore = ["LICENSE", "README.md", "CHANGELOG.md"]
|
|
|
57
57
|
|
|
58
58
|
Command:
|
|
59
59
|
|
|
60
|
-
uv tool run dotlocalslashbin --
|
|
60
|
+
uv tool run dotlocalslashbin --output=. tofu.toml
|
|
61
61
|
|
|
62
62
|
Further examples are available in files like `linux-amd64.toml` and
|
|
63
63
|
`github.toml` in the `bin` directory of
|
|
@@ -8,27 +8,26 @@
|
|
|
8
8
|
# ///
|
|
9
9
|
"""Download and extract files to `~/.local/bin/`."""
|
|
10
10
|
|
|
11
|
-
import gzip
|
|
12
|
-
import tarfile
|
|
13
11
|
from argparse import ArgumentParser, BooleanOptionalAction, Namespace
|
|
14
12
|
from dataclasses import dataclass
|
|
15
13
|
from enum import Enum
|
|
14
|
+
from gzip import GzipFile
|
|
16
15
|
from hashlib import file_digest
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from shlex import split
|
|
19
18
|
from shutil import copy, copyfileobj
|
|
20
19
|
from stat import S_IEXEC
|
|
21
20
|
from subprocess import run
|
|
21
|
+
from tarfile import open as tar_open, TarFile, TarInfo
|
|
22
22
|
from tomllib import load
|
|
23
23
|
from urllib.error import HTTPError
|
|
24
24
|
from urllib.request import urlopen
|
|
25
|
-
from zipfile import ZipFile
|
|
25
|
+
from zipfile import ZipFile, ZipInfo
|
|
26
26
|
|
|
27
|
-
__version__ = "0.0.
|
|
27
|
+
__version__ = "0.0.26"
|
|
28
28
|
|
|
29
29
|
_CACHE = Path("~/.cache/dotlocalslashbin/")
|
|
30
30
|
_HOME = str(Path("~").expanduser())
|
|
31
|
-
_INPUT = "bin.toml"
|
|
32
31
|
_OUTPUT = Path("~/.local/bin/")
|
|
33
32
|
_SHA512_LENGTH = 128
|
|
34
33
|
|
|
@@ -44,7 +43,7 @@ Action = Enum("Action", ["command", "copy", "gunzip", "symlink", "untar", "unzip
|
|
|
44
43
|
|
|
45
44
|
@dataclass(init=False)
|
|
46
45
|
class Item:
|
|
47
|
-
"""Class for an
|
|
46
|
+
"""Class for an artifact listed in input."""
|
|
48
47
|
|
|
49
48
|
name: str
|
|
50
49
|
url: str
|
|
@@ -58,9 +57,9 @@ class Item:
|
|
|
58
57
|
ignore: set
|
|
59
58
|
|
|
60
59
|
|
|
61
|
-
def main() -> int:
|
|
60
|
+
def main(_args: list[str] | None = None) -> int:
|
|
62
61
|
"""Parse command line arguments and download each file."""
|
|
63
|
-
args = _parse_args()
|
|
62
|
+
args = _parse_args(_args)
|
|
64
63
|
|
|
65
64
|
if args.clear:
|
|
66
65
|
for path in args.cache.expanduser().iterdir():
|
|
@@ -100,7 +99,11 @@ def main() -> int:
|
|
|
100
99
|
|
|
101
100
|
arg0 = str(item.target.absolute())
|
|
102
101
|
prompt = "#" if item.version else "$"
|
|
103
|
-
|
|
102
|
+
if item.target.exists():
|
|
103
|
+
print(" ".join((prompt, arg0.replace(_HOME, "~"), item.version)))
|
|
104
|
+
else:
|
|
105
|
+
destination = str(item.target.parent).replace(_HOME, "~")
|
|
106
|
+
print(f"$ {destination} now contains {item.name}")
|
|
104
107
|
if item.version:
|
|
105
108
|
run([arg0, *split(item.version)], check=True)
|
|
106
109
|
print()
|
|
@@ -125,28 +128,22 @@ def _process(item: Item) -> None:
|
|
|
125
128
|
item.target.parent.mkdir(parents=True, exist_ok=True)
|
|
126
129
|
item.target.unlink(missing_ok=True)
|
|
127
130
|
_action(item)
|
|
128
|
-
if not item.target.is_symlink():
|
|
131
|
+
if item.target.exists() and not item.target.is_symlink():
|
|
129
132
|
item.target.chmod(item.target.stat().st_mode | S_IEXEC)
|
|
130
133
|
|
|
131
134
|
|
|
132
|
-
def _parse_args() -> _CustomNamespace:
|
|
133
|
-
parser = ArgumentParser(
|
|
134
|
-
prog=Path(__file__).name,
|
|
135
|
-
epilog="¹ --input can be specified multiple times",
|
|
136
|
-
)
|
|
135
|
+
def _parse_args(args: list[str] | None) -> _CustomNamespace:
|
|
136
|
+
parser = ArgumentParser(prog=Path(__file__).name)
|
|
137
137
|
parser.add_argument("--version", action="version", version=__version__)
|
|
138
|
-
help_ = f"TOML specification (default: {_INPUT})¹"
|
|
139
|
-
parser.add_argument("--input", action="append", help=help_, type=Path)
|
|
140
138
|
help_ = f"Target directory (default: {_OUTPUT})"
|
|
141
139
|
parser.add_argument("--output", default=_OUTPUT, help=help_, type=Path)
|
|
142
140
|
help_ = f"Cache directory (default: {_CACHE})"
|
|
143
141
|
parser.add_argument("--cache", default=_CACHE, help=help_, type=Path)
|
|
144
142
|
help_ = "Clear the cache directory first (default: --no-clear)"
|
|
145
143
|
parser.add_argument("--clear", action=BooleanOptionalAction, help=help_)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return result
|
|
144
|
+
help_ = "input specification in TOML"
|
|
145
|
+
parser.add_argument("input", nargs="+", help=help_, type=Path)
|
|
146
|
+
return parser.parse_args(args, namespace=_CustomNamespace())
|
|
150
147
|
|
|
151
148
|
|
|
152
149
|
def _download(item: Item) -> None:
|
|
@@ -166,25 +163,50 @@ def _action(item: Item) -> None:
|
|
|
166
163
|
copy(item.downloaded, item.target)
|
|
167
164
|
elif item.action == Action.symlink:
|
|
168
165
|
item.target.symlink_to(item.downloaded)
|
|
169
|
-
elif item.action == Action.unzip:
|
|
170
|
-
with ZipFile(item.downloaded, "r") as file:
|
|
171
|
-
file.extract(item.target.name, path=item.target.parent)
|
|
172
166
|
elif item.action == Action.gunzip:
|
|
173
|
-
with
|
|
167
|
+
with GzipFile(item.downloaded, "r") as fsrc, item.target.open("wb") as fdst:
|
|
174
168
|
copyfileobj(fsrc, fdst)
|
|
175
|
-
elif item.action
|
|
176
|
-
|
|
169
|
+
elif item.action in (Action.unzip, Action.untar):
|
|
170
|
+
_many_files(item)
|
|
171
|
+
elif item.action == Action.command and item.command is not None:
|
|
172
|
+
cmd = item.command.format(target=item.target, downloaded=item.downloaded)
|
|
173
|
+
run(split(cmd), check=True)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _many_files(item: Item) -> None:
|
|
177
|
+
"""Unzip or untar an item.
|
|
178
|
+
|
|
179
|
+
These two actions should respect 'ignore' and 'prefix' similarly.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def _should_continue(filename: str) -> bool:
|
|
183
|
+
return any(
|
|
184
|
+
[
|
|
185
|
+
filename in item.ignore,
|
|
186
|
+
filename == item.prefix,
|
|
187
|
+
item.prefix != "" and not filename.startswith(item.prefix),
|
|
188
|
+
],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
file: TarFile | ZipFile
|
|
192
|
+
member: TarInfo | ZipInfo
|
|
193
|
+
if item.action == Action.untar:
|
|
194
|
+
with tar_open(item.downloaded, "r") as file:
|
|
177
195
|
for member in file.getmembers():
|
|
178
|
-
if member.name
|
|
196
|
+
if _should_continue(member.name):
|
|
179
197
|
continue
|
|
180
198
|
member.name = member.name.removeprefix(item.prefix)
|
|
181
199
|
try:
|
|
182
200
|
file.extract(member, path=item.target.parent, filter="tar")
|
|
183
201
|
except TypeError: # before 3.11.4 e.g. Debian 12
|
|
184
202
|
file.extract(member, path=item.target.parent)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
else:
|
|
204
|
+
with ZipFile(item.downloaded, "r") as file:
|
|
205
|
+
for member in file.infolist():
|
|
206
|
+
if _should_continue(member.filename):
|
|
207
|
+
continue
|
|
208
|
+
member.filename = member.filename.removeprefix(item.prefix)
|
|
209
|
+
file.extract(member, path=item.target.parent)
|
|
188
210
|
|
|
189
211
|
|
|
190
212
|
def _guess_action(item: Item) -> Action:
|
|
File without changes
|
|
File without changes
|