dotlocalslashbin 0.0.25__py3-none-any.whl → 0.0.26__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.4
2
2
  Name: dotlocalslashbin
3
- Version: 0.0.25
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 --input=tofu.toml --output=.
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
@@ -0,0 +1,6 @@
1
+ dotlocalslashbin.py,sha256=uMHP26aZuzbWUiJF85GmkPw9f81637iIDMzqOu9YPYQ,7609
2
+ dotlocalslashbin-0.0.26.dist-info/METADATA,sha256=JzggGSi5oF0gpk5JZHVbW0krb4rEQjt-uVG8s3UdPdI,3217
3
+ dotlocalslashbin-0.0.26.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
4
+ dotlocalslashbin-0.0.26.dist-info/entry_points.txt,sha256=eK8C0lW2h7WAcr78hM-_OBrLotRptdLvjbnT7f67m34,58
5
+ dotlocalslashbin-0.0.26.dist-info/licenses/LICENSES/MPL-2.0.txt,sha256=ZqMQfVrWoFiqt1PqrCBHzLLtDjlGXdD-WETaPjANUXI,16727
6
+ dotlocalslashbin-0.0.26.dist-info/RECORD,,
dotlocalslashbin.py CHANGED
@@ -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.25"
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 application."""
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
- print(" ".join((prompt, arg0.replace(_HOME, "~"), item.version)))
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
- result = parser.parse_args(namespace=_CustomNamespace())
147
- if not result.input:
148
- result.input = [Path(_INPUT)]
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 gzip.open(item.downloaded, "r") as fsrc, item.target.open("wb") as fdst:
167
+ with GzipFile(item.downloaded, "r") as fsrc, item.target.open("wb") as fdst:
174
168
  copyfileobj(fsrc, fdst)
175
- elif item.action == Action.untar:
176
- with tarfile.open(item.downloaded, "r") as file:
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 in item.ignore:
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
- elif item.action == Action.command and item.command is not None:
186
- cmd = item.command.format(target=item.target, downloaded=item.downloaded)
187
- run(split(cmd), check=True)
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:
@@ -1,6 +0,0 @@
1
- dotlocalslashbin.py,sha256=vaiG3ALD4zg48dwMIvf7zkDu9QwI5v5EEPksllELzlk,6743
2
- dotlocalslashbin-0.0.25.dist-info/METADATA,sha256=1sfk0_6h7gGa9mYHgfM_1cfIgLdf4qgIS5B4Ftou8lI,3225
3
- dotlocalslashbin-0.0.25.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
4
- dotlocalslashbin-0.0.25.dist-info/entry_points.txt,sha256=eK8C0lW2h7WAcr78hM-_OBrLotRptdLvjbnT7f67m34,58
5
- dotlocalslashbin-0.0.25.dist-info/licenses/LICENSES/MPL-2.0.txt,sha256=ZqMQfVrWoFiqt1PqrCBHzLLtDjlGXdD-WETaPjANUXI,16727
6
- dotlocalslashbin-0.0.25.dist-info/RECORD,,