lid-cli 0.3__tar.gz → 0.5__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.
lid_cli-0.5/PKG-INFO ADDED
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: lid-cli
3
+ Version: 0.5
4
+ Summary: gio-based volume mounting and unmounting tool.
5
+ Author: Christian Heinze
6
+ License-Expression: MIT
7
+ License-File: LICENSES/MIT.txt
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Utilities
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: msgspec>=0.21
17
+ Requires-Dist: rich>=14.3
18
+ Requires-Dist: typer>=0.24
19
+ Requires-Python: >=3.14
20
+ Project-URL: Repository, https://codeberg.org/christianheinze/lid-cli
21
+ Description-Content-Type: text/markdown
22
+
23
+ # `gio`-based volume handling tool `lid`
24
+
25
+ ## (Un)Install
26
+
27
+ To install, run
28
+
29
+ ```bash
30
+ uv tool install lid-cli
31
+ ```
32
+
33
+ Then run
34
+
35
+ ```bash
36
+ yd --install-completion
37
+ ```
38
+
39
+ to install auto-completion in your shell.
40
+
41
+ In `bash`, the completion code is stored in `~/.bash_completions/lid.sh` and that file is sourced from `~/.bashrc`.
42
+ Remove both and call `uv tool uninstall lid-cli` to remove this tool.
43
+
44
+ ## (Un)Mounting
45
+
46
+ Currently, `lid` allows to (un)mount volumes using `gio`.
47
+
48
+ To find available volumes, run `lid ls` (add the `--mounted` option to restrict to mounted volumes).
49
+
50
+ Use `lid mount NAME` to mount a volume wherein name is the name displayed by `lid ls`.
51
+
52
+ Use `lid umount NAME` to unmount. `lid` will only unmount what it has itself mounted.
53
+ This allows to sandwich another action between a `lid mount` and a `lid umount` call to have the volume mounted during the action and retain the initial state afterwards
54
+ (if the volume was mounted before calling `lid mount` it is still mounted after `lid umount`).
lid_cli-0.5/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # `gio`-based volume handling tool `lid`
2
+
3
+ ## (Un)Install
4
+
5
+ To install, run
6
+
7
+ ```bash
8
+ uv tool install lid-cli
9
+ ```
10
+
11
+ Then run
12
+
13
+ ```bash
14
+ yd --install-completion
15
+ ```
16
+
17
+ to install auto-completion in your shell.
18
+
19
+ In `bash`, the completion code is stored in `~/.bash_completions/lid.sh` and that file is sourced from `~/.bashrc`.
20
+ Remove both and call `uv tool uninstall lid-cli` to remove this tool.
21
+
22
+ ## (Un)Mounting
23
+
24
+ Currently, `lid` allows to (un)mount volumes using `gio`.
25
+
26
+ To find available volumes, run `lid ls` (add the `--mounted` option to restrict to mounted volumes).
27
+
28
+ Use `lid mount NAME` to mount a volume wherein name is the name displayed by `lid ls`.
29
+
30
+ Use `lid umount NAME` to unmount. `lid` will only unmount what it has itself mounted.
31
+ This allows to sandwich another action between a `lid mount` and a `lid umount` call to have the volume mounted during the action and retain the initial state afterwards
32
+ (if the volume was mounted before calling `lid mount` it is still mounted after `lid umount`).
@@ -4,8 +4,8 @@ requires = [ "uv-build>=0.10,<0.11" ]
4
4
 
5
5
  [project]
6
6
  name = "lid-cli"
7
- version = "0.3"
8
- description = "Project Lid Cli."
7
+ version = "0.5"
8
+ description = "gio-based volume mounting and unmounting tool."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  license-files = [ "LICENSES/MIT.txt" ]
@@ -1,7 +1,7 @@
1
1
  # SPDX-FileCopyrightText: Christian Heinze
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- """lid-cli."""
4
+ """lid CLI tool."""
5
5
 
6
6
  from __future__ import annotations
7
7
 
@@ -11,6 +11,7 @@ import pathlib as pl
11
11
  from typing import Annotated
12
12
 
13
13
  import rich.console
14
+ import rich.table
14
15
  import rich.theme
15
16
  import typer
16
17
 
@@ -22,7 +23,7 @@ env: lid.types.Environment
22
23
  console: rich.console.Console
23
24
 
24
25
  _HELP = """
25
- Resource activation and communication helper.
26
+ `gio`-based volume (un)mounter.
26
27
  """
27
28
 
28
29
  app = typer.Typer(help=_HELP, no_args_is_help=True, rich_markup_mode="markdown")
@@ -90,11 +91,14 @@ def _complete_mount_kw(incomplete: str) -> list[str]:
90
91
  import asyncio
91
92
 
92
93
  env = lid.io.capture_environment()
93
- return asyncio.run(
94
- lid.gio.find_device_names(
95
- incomplete, mounted_only=False, env=env, runner=lid.subprocess.run
94
+ try:
95
+ return asyncio.run(
96
+ lid.gio.find_volume_names(
97
+ incomplete, mounted_only=False, env=env, runner=lid.subprocess.run
98
+ )
96
99
  )
97
- )
100
+ except lid.types.GioError:
101
+ return []
98
102
 
99
103
 
100
104
  def _require_nonempty(param: typer.CallbackParam, v: str | None) -> str | None:
@@ -103,12 +107,13 @@ def _require_nonempty(param: typer.CallbackParam, v: str | None) -> str | None:
103
107
  raise typer.BadParameter(f"parameter `{param.name}` must not be an empty string")
104
108
 
105
109
 
106
- @app.command()
110
+ @app.command(help="Mount volume.")
107
111
  def mount(
108
112
  kw: Annotated[
109
113
  str,
110
114
  typer.Argument(
111
- help="Device keyword.",
115
+ help="Volume name as displayed by `lid ls` (partial names"
116
+ " are sufficient as long as they uniquely identify a single volume name).",
112
117
  autocompletion=_complete_mount_kw,
113
118
  callback=_require_nonempty,
114
119
  ),
@@ -124,8 +129,8 @@ def mount(
124
129
  mt = runner.run(lid.gio.mount(kw, env=env, runner=lid.subprocess.run))
125
130
  except lid.types.GioNotFoundError as err:
126
131
  log.exception("Failed to mount for keyword `%s`.", kw)
127
- raise typer.BadParameter(f"Device not determined: {err}.") from None
128
- except lid.types.GioMountError as err:
132
+ raise typer.BadParameter(f"Volume not determined: {err}.") from None
133
+ except lid.types.GioError as err:
129
134
  log.exception("Failed to mount for keyword `%s`.", kw)
130
135
  console.print(f"Mount failure: {err}.", style="error")
131
136
  raise typer.Exit(2) from None
@@ -138,11 +143,13 @@ def mount(
138
143
  env.runtime_dir.joinpath(str(uuid.uuid4())).write_bytes(mt_info)
139
144
  except OSError, PermissionError:
140
145
  log.exception("Failed to create mount marker file.")
146
+ log.debug("Attempting unmount.")
141
147
  try:
142
148
  runner.run(
143
149
  lid.gio.umount(mount=mt, env=env, runner=lid.subprocess.run)
144
150
  )
145
- except lid.types.GioMountError as err:
151
+ except lid.types.GioError as err:
152
+ log.exception("Unmounting failed.")
146
153
  console.print(
147
154
  f"Failed to create marker file, then failed to umount: {err}.",
148
155
  style="error",
@@ -154,26 +161,80 @@ def mount(
154
161
  return 0
155
162
 
156
163
 
164
+ @app.command("ls", help="List available volumes.")
165
+ def list_volumes(
166
+ mounted: Annotated[
167
+ bool,
168
+ typer.Option(
169
+ "--mounted",
170
+ help="Only list mounted volumes.",
171
+ ),
172
+ ] = False,
173
+ ) -> int:
174
+ import asyncio
175
+
176
+ try:
177
+ names = asyncio.run(
178
+ lid.gio.find_volume_names(
179
+ "", mounted_only=mounted, env=env, runner=lid.subprocess.run
180
+ )
181
+ )
182
+ except lid.types.GioError as err:
183
+ log.exception("Failed to list volumes.")
184
+ console.print(f"Volume discovery failure: {err}.", style="error")
185
+ raise typer.Exit(2) from None
186
+
187
+ if not names:
188
+ console.print("No volumes found.", style="warning")
189
+ return 0
190
+
191
+ table = rich.table.Table(title="Volumes")
192
+ table.add_column("Name", overflow="fold")
193
+ for name in names:
194
+ table.add_row(name)
195
+ console.print(table)
196
+ return 0
197
+
198
+
157
199
  def _complete_umount_kw(incomplete: str) -> list[str]:
158
200
  import asyncio
159
201
 
160
202
  env = lid.io.capture_environment()
161
- return asyncio.run(
162
- lid.gio.find_device_names(
163
- incomplete, mounted_only=True, env=env, runner=lid.subprocess.run
203
+ try:
204
+ return asyncio.run(
205
+ lid.gio.find_volume_names(
206
+ incomplete, mounted_only=True, env=env, runner=lid.subprocess.run
207
+ )
164
208
  )
165
- )
209
+ except lid.types.GioError:
210
+ return []
166
211
 
167
212
 
168
213
  async def _umount(kw: str, /, env: lid.types.Environment) -> int:
169
214
  import asyncio
170
215
 
171
- names, markers = await asyncio.gather(
172
- lid.gio.find_device_names(
173
- kw, mounted_only=True, env=env, runner=lid.subprocess.run
174
- ),
175
- asyncio.to_thread(lid.io.read_mount_markers, env),
176
- )
216
+ async with asyncio.TaskGroup() as tg:
217
+ names_task = tg.create_task(
218
+ lid.gio.find_volume_names(
219
+ kw, mounted_only=True, env=env, runner=lid.subprocess.run
220
+ )
221
+ )
222
+ markers_task = tg.create_task(
223
+ asyncio.to_thread(lid.io.read_mount_markers, env),
224
+ )
225
+ try:
226
+ names, markers = await names_task, await markers_task
227
+ except* (OSError, PermissionError) as err:
228
+ log.exception(
229
+ "Failed to access mount marker files in `%s`.", env.runtime_dir
230
+ )
231
+ console.print("Failed to access marker files.", style="error")
232
+ raise typer.Exit(2) from err
233
+ except* lid.types.GioError as err:
234
+ log.exception("Failed to gather volume information for keyword `%s`.", kw)
235
+ console.print(f"Unmount failure: {err}.", style="error")
236
+ raise typer.Exit(2) from err
237
+
177
238
  match [(p, m) for n, (p, m) in it.product(names, markers) if n == m.name]:
178
239
  case []:
179
240
  log.info("Found no matching marker file. Exiting.")
@@ -181,10 +242,7 @@ async def _umount(kw: str, /, env: lid.types.Environment) -> int:
181
242
  case [[pl.Path() as f, lid.types.Mount() as mt]]:
182
243
  try:
183
244
  await lid.gio.umount(mount=mt, env=env, runner=lid.subprocess.run)
184
- except lid.types.GioNotFoundError as err:
185
- log.exception("Failed to mount for keyword `%s` (match: `%s`.", kw, mt)
186
- raise typer.BadParameter(f"Device not determined: {err}.") from None
187
- except lid.types.GioMountError as err:
245
+ except lid.types.GioError as err:
188
246
  log.exception(
189
247
  "Failed to unmount for keyword `%s` (match: `%s`).", kw, mt
190
248
  )
@@ -218,7 +276,8 @@ def umount(
218
276
  kw: Annotated[
219
277
  str,
220
278
  typer.Argument(
221
- help="Device keyword.",
279
+ help="Volume name as displayed by `lid ls` (partial names"
280
+ " are sufficient as long as they uniquely identify a single volume name).",
222
281
  autocompletion=_complete_umount_kw,
223
282
  callback=_require_nonempty,
224
283
  ),
@@ -38,54 +38,60 @@ if TYPE_CHECKING:
38
38
  ) -> None: ...
39
39
 
40
40
 
41
- __all__ = ["find_device_names", "mount", "umount"]
41
+ __all__ = ["find_volume_names", "mount", "umount"]
42
42
 
43
43
  log = logging.getLogger(__name__)
44
44
 
45
45
 
46
46
  # Return type dictated by use a completion suggester.
47
- async def find_device_names(
47
+ async def find_volume_names(
48
48
  kw: str, /, mounted_only: bool, env: types.Environment, runner: AsyncRunner
49
49
  ) -> list[str]:
50
- device_info = await _query_matching_device_info(
50
+ volume_info = await _query_matching_volume_info(
51
51
  kw, include_details=False, env=env, runner=runner
52
52
  )
53
53
  if not mounted_only:
54
- return [d["__name__"] for d in device_info]
55
- return [d["__name__"] for d in device_info if any(k.startswith("Mount") for k in d)]
54
+ return [d["__name__"] for d in volume_info]
55
+ return [d["__name__"] for d in volume_info if any(k.startswith("Mount") for k in d)]
56
56
 
57
57
 
58
58
  async def mount(kw: str, /, env: types.Environment, runner: AsyncRunner) -> types.Mount:
59
- """Mount device.
59
+ """Mount volume.
60
60
 
61
61
  Parameters
62
62
  ----------
63
63
  kw
64
- (Partial) name of device as given in the top line of the gio output section.
64
+ (Partial) name of volume as given in the top line of the gio output section.
65
65
  """
66
- device_info = await _query_matching_device_info(
66
+ volume_info = await _query_matching_volume_info(
67
67
  kw, include_details=True, env=env, runner=runner
68
68
  )
69
- device = _extract_device(kw, device_info=device_info)
69
+ volume = _extract_volume(kw, volume_info=volume_info)
70
70
 
71
- name, mount_id, is_mounted = device.name, device.id_, device.is_mounted
71
+ name, mount_id, is_mounted = volume.name, volume.id_, volume.is_mounted
72
72
  if is_mounted:
73
- log.info("Device `%s` already mounted.", name)
73
+ log.info("Volume `%s` already mounted.", name)
74
74
  else:
75
- log.info("Mounting device `%s`.", name)
75
+ log.info("Mounting volume `%s`.", name)
76
76
  try:
77
77
  await runner(env.gio_bin, "mount", "--device", mount_id)
78
78
  except types.SubprocessError as err:
79
- log.exception("Failed to mount device ID `%s`.", mount_id)
80
- raise types.GioMountError(
81
- f"failed to mount device with ID`{mount_id}`"
82
- ) from err
79
+ log.exception("Failed to mount volume ID `%s`.", mount_id)
80
+ raise types.GioError(f"failed to mount volume with ID`{mount_id}`") from err
81
+
82
+ # Update information to make sure mount information is present.
83
+ volume_info = await _query_matching_volume_info(
84
+ kw, include_details=True, env=env, runner=runner
85
+ )
86
+ volume = _extract_volume(kw, volume_info=volume_info)
83
87
 
84
88
  try:
85
- mp = await _find_mountpoint(mount_id, env=env, runner=runner)
86
- except types.GioMountError as err:
89
+ mp = await _find_mountpoint(
90
+ _extract_gio_location(volume.data), env=env, runner=runner
91
+ )
92
+ except (ValueError, types.GioError) as err:
87
93
  log.exception("Found no mount point for `%s`.", name)
88
- raise types.GioMountError(f"found no mount point for `{name}`") from err
94
+ raise types.GioError(f"found no mount point for `{name}`") from err
89
95
 
90
96
  return types.Mount(name=name, mountpoint=mp, triggered=mount_id is not None)
91
97
 
@@ -98,23 +104,25 @@ async def umount(
98
104
  env: types.Environment,
99
105
  runner: AsyncRunner,
100
106
  ) -> None:
101
- """Unmount device."""
107
+ """Unmount volume."""
102
108
  if kw is not None:
103
- device_info = await _query_matching_device_info(
109
+ volume_info = await _query_matching_volume_info(
104
110
  kw, include_details=True, env=env, runner=runner
105
111
  )
106
- device = _extract_device(kw, device_info=device_info)
112
+ volume = _extract_volume(kw, volume_info=volume_info)
107
113
 
108
- if not device.is_mounted:
109
- log.info("Device `%s` not mounted.")
114
+ if not volume.is_mounted:
115
+ log.info("Volume `%s` not mounted.")
110
116
  return
111
- name, mount_id = device.name, device.id_
117
+ name = volume.name
112
118
 
113
119
  try:
114
- mountpoint = await _find_mountpoint(mount_id, env=env, runner=runner)
115
- except types.GioMountError as err:
120
+ mountpoint = await _find_mountpoint(
121
+ _extract_gio_location(volume.data), env=env, runner=runner
122
+ )
123
+ except (ValueError, types.GioError) as err:
116
124
  log.exception("Found no mount point for `%s`.", name)
117
- raise types.GioMountError(f"found no mount point for `{name}`") from err
125
+ raise types.GioError(f"found no mount point for `{name}`") from err
118
126
  else:
119
127
  if TYPE_CHECKING:
120
128
  mount = cast("types.Mount", mount)
@@ -123,23 +131,26 @@ async def umount(
123
131
  try:
124
132
  await runner(env.gio_bin, "mount", "--unmount", mountpoint)
125
133
  except types.SubprocessError as err:
126
- raise types.GioMountError(f"failed to unmount `{mountpoint}`") from err
134
+ raise types.GioError(f"failed to unmount `{mountpoint}`") from err
127
135
 
128
136
 
129
137
  @dataclass(slots=True, kw_only=True)
130
- class _Device:
138
+ class _Volume:
131
139
  name: str
132
140
  id_: str
141
+
133
142
  is_mounted: bool
134
143
  data: dict[str, Any]
135
144
 
136
145
 
137
- async def _query_matching_device_info(
146
+ async def _query_matching_volume_info(
138
147
  kw: str, /, include_details: bool, env: types.Environment, runner: AsyncRunner
139
148
  ) -> Sequence[dict[str, Any]]:
140
- if not kw:
141
- raise ValueError("empty keyword cannot be used for device identification")
149
+ """Query volume information.
142
150
 
151
+ Only volumes with names containing `kw` (interpreted as a regular expression)
152
+ are returned.
153
+ """
143
154
  if include_details:
144
155
  gio_args = ("mount", "--list", "--detail")
145
156
  else:
@@ -147,30 +158,28 @@ async def _query_matching_device_info(
147
158
  try:
148
159
  gio_out = await runner(env.gio_bin, *gio_args)
149
160
  except types.SubprocessError as err:
150
- raise types.GioMountError(
151
- "failed to collect gio output for device discovery"
161
+ raise types.GioError(
162
+ "failed to collect gio output for volume discovery"
152
163
  ) from err
153
164
  try:
154
165
  info = _parse_output(gio_out)
155
- except types.GioParseError as err:
156
- raise types.GioMountError(
157
- "failed to parse gio output for device discovery"
158
- ) from err
166
+ except types.GioError as err:
167
+ raise types.GioError("failed to parse gio output for volume discovery") from err
159
168
 
160
169
  pattern = re.compile(kw, re.IGNORECASE)
161
170
  return [
162
171
  v
163
172
  for k, v in info.items()
164
- if k.startswith(("Volume", "Drive"))
173
+ if k.startswith("Volume")
165
174
  and isinstance(v, dict)
166
175
  and pattern.search(v.get("__name__", "")) is not None
167
176
  ]
168
177
 
169
178
 
170
- def _extract_device(kw: str, /, device_info: Sequence[dict[str, Any]]) -> _Device:
171
- match device_info:
179
+ def _extract_volume(kw: str, /, volume_info: Sequence[dict[str, Any]]) -> _Volume:
180
+ match volume_info:
172
181
  case []:
173
- raise types.GioNotFoundError(f"found no device matching for `{kw}`")
182
+ raise types.GioNotFoundError(f"found no volume matching for `{kw}`")
174
183
  case [{"__name__": name} as v]:
175
184
  log.debug("Matched `%s` to full name `%s`.", kw, name)
176
185
 
@@ -183,37 +192,55 @@ def _extract_device(kw: str, /, device_info: Sequence[dict[str, Any]]) -> _Devic
183
192
  case _:
184
193
  log.error("Failed to find mount ID in `%s`.", v)
185
194
  raise types.GioNotFoundError(f"failed to find ID for `{name}`")
186
- return _Device(name=name, id_=mount_id, is_mounted=is_mounted, data=v)
195
+ return _Volume(name=name, id_=mount_id, is_mounted=is_mounted, data=v)
187
196
  case [f, s, *ms]:
188
197
  fn, sn, lms = f["__name__"], s["__name__"], len(ms)
189
198
  raise types.GioNotFoundError(
190
199
  f"found multiple matches for `{kw}`: `{fn}`, `{sn}`, and {lms} more."
191
200
  )
192
201
  case _:
193
- raise types.GioNotFoundError("unsupported gio output structure")
202
+ raise types.GioError("unsupported gio output structure")
203
+
204
+
205
+ def _extract_gio_location(info: dict[str, Any], /) -> str:
206
+ try:
207
+ mount_info = next(v for k, v in info.items() if k.startswith("Mount"))
208
+ except StopIteration:
209
+ raise ValueError("no mount information found") from None
210
+ if not isinstance(mount_info, dict):
211
+ raise ValueError("invalid mount information found")
212
+
213
+ if (loc := mount_info.get("default_location")) is not None:
214
+ return loc
215
+ mount_name, loc_pattern = mount_info.get("__name__", ""), r"->\s*(?P<loc>.+?)$"
216
+ if (m := re.search(loc_pattern, mount_name)) is not None:
217
+ return m.group("loc")
218
+
219
+ raise ValueError("no mount location found")
194
220
 
195
221
 
196
222
  async def _find_mountpoint(
197
- id_: str, /, env: types.Environment, runner: AsyncRunner
223
+ loc: str, /, env: types.Environment, runner: AsyncRunner
198
224
  ) -> str:
199
225
  try:
200
- # Requires device to be mounted.
226
+ # Requires volume to be mounted.
201
227
  # Need to the US locale as the output key are adapted to that setting.
202
228
  gio_out = await runner(
203
- env.gio_bin, "info", "-a", "local", id_, env_mods={"LANG": "en_US-UTF-8"}
229
+ env.gio_bin, "info", "-a", "local", loc, env_mods={"LANG": "en_US-UTF-8"}
204
230
  )
205
231
  except types.SubprocessError as err:
206
- raise types.GioMountError(
232
+ raise types.GioError(
207
233
  "failed to collect gio output for mountpoint discovery"
208
234
  ) from err
209
235
  try:
210
236
  gio_out = _parse_output(gio_out)
211
- except types.GioParseError as err:
212
- raise types.GioMountError(
213
- "failed to parse gio output for path discovery"
214
- ) from err
237
+ except types.GioError as err:
238
+ raise types.GioError("failed to parse gio output for path discovery") from err
215
239
 
216
- return gio_out["local path"]
240
+ try:
241
+ return gio_out["local path"]
242
+ except KeyError:
243
+ raise types.GioError(f"`{loc}` has no local filesystem path") from None
217
244
 
218
245
 
219
246
  def _parse_output(text: str, /, header_name: str = "__name__") -> dict[str, Any]:
@@ -233,7 +260,7 @@ def _parse_output(text: str, /, header_name: str = "__name__") -> dict[str, Any]
233
260
  stack.pop()
234
261
  current = stack[-1]
235
262
  if line.indent < current.indent:
236
- raise types.GioParseError(
263
+ raise types.GioError(
237
264
  f"indent {line.indent} does not match any previous level"
238
265
  )
239
266
 
@@ -293,7 +320,7 @@ def _parse_line(
293
320
  log.debug("Extracted content: `%s` (indent: %s).", content, indent)
294
321
 
295
322
  if (spec_match := _SPEC.fullmatch(content)) is None:
296
- raise types.GioParseError("line not matching expected pattern: `%s`", content)
323
+ raise types.GioError("line not matching expected pattern: `%s`", content)
297
324
 
298
325
  key, value = spec_match.group("key"), spec_match.group("val") or None
299
326
  log.debug("Found key/value: `%s`=`%s`.", key, value)
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import asyncio
9
9
  import contextvars
10
+ import logging
10
11
  import os
11
12
 
12
13
  from lid import types
@@ -18,6 +19,8 @@ if TYPE_CHECKING:
18
19
 
19
20
  __all__ = ["raise_on_stderr", "run"]
20
21
 
22
+ log = logging.getLogger()
23
+
21
24
  RUNNING = contextvars.ContextVar[str]("_RUNNING")
22
25
 
23
26
 
@@ -46,7 +49,8 @@ async def run(
46
49
  stderr=asyncio.subprocess.PIPE,
47
50
  **proc_kwargs,
48
51
  )
49
- token = RUNNING.set(f"`{bin_} {' '.join(args)}`")
52
+ token = RUNNING.set(running := f"`{bin_} {' '.join(args)}`")
53
+ log.debug("Running %s.", running)
50
54
  try:
51
55
  stdout, stderr = await proc.communicate()
52
56
  except asyncio.CancelledError:
@@ -55,7 +59,7 @@ async def run(
55
59
  raise
56
60
 
57
61
  if proc.returncode != 0:
58
- running = RUNNING.get()
62
+ log.error("Running %s failed (error code %s).", running, proc.returncode)
59
63
  RUNNING.reset(token)
60
64
  raise types.SubprocessError(
61
65
  f"{running} exited with return code {proc.returncode}:"
@@ -63,6 +67,8 @@ async def run(
63
67
  )
64
68
  try:
65
69
  if stderr:
70
+ log.warning("Found STDERR output: `%s`.", stderr)
71
+
66
72
  handle_stderr(stderr)
67
73
  stdout = stdout.decode("utf-8")
68
74
  finally:
@@ -26,16 +26,16 @@ class EnvCaptureError(LidError):
26
26
  """Exception raised when capturing the environment fails."""
27
27
 
28
28
 
29
- class GioParseError(LidError):
30
- """Raised when gio CLI output cannot be parsed."""
29
+ class GioError(LidError):
30
+ """Exception raised when interaction with `gio` failes.
31
31
 
32
-
33
- class GioMountError(LidError):
34
- """Exception raised when (un)mounting device fails."""
32
+ This includes all issues not related to user input, e.g., unsupported formats
33
+ int the `gio` output.
34
+ """
35
35
 
36
36
 
37
- class GioNotFoundError(GioMountError):
38
- """Exception raised when (un)mounting device fails due to missing device."""
37
+ class GioNotFoundError(LidError):
38
+ """Exception raised when no matching volume can be found."""
39
39
 
40
40
 
41
41
  @dataclass(slots=True)
@@ -55,11 +55,11 @@ class Mount(msgspec.Struct, forbid_unknown_fields=True, kw_only=True):
55
55
  Parameters
56
56
  ----------
57
57
  name
58
- Device ID as displayed by `gio mount --list`.
58
+ Volume ID as displayed by `gio mount --list`.
59
59
  mountpoint
60
- Mountpoint (local directory logically linked to device file system).
60
+ Mountpoint (local directory logically linked to volume file system).
61
61
  triggered
62
- True if the attempt actually mounted the device.
62
+ True if the attempt actually mounted the volume.
63
63
  """
64
64
 
65
65
  name: str
lid_cli-0.3/PKG-INFO DELETED
@@ -1,25 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: lid-cli
3
- Version: 0.3
4
- Summary: Project Lid Cli.
5
- Author: Christian Heinze
6
- License-Expression: MIT
7
- License-File: LICENSES/MIT.txt
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Environment :: Console
10
- Classifier: Intended Audience :: End Users/Desktop
11
- Classifier: Operating System :: POSIX :: Linux
12
- Classifier: Programming Language :: Python :: 3 :: Only
13
- Classifier: Programming Language :: Python :: 3.14
14
- Classifier: Topic :: Utilities
15
- Classifier: Typing :: Typed
16
- Requires-Dist: msgspec>=0.21
17
- Requires-Dist: rich>=14.3
18
- Requires-Dist: typer>=0.24
19
- Requires-Python: >=3.14
20
- Project-URL: Repository, https://codeberg.org/christianheinze/lid-cli
21
- Description-Content-Type: text/markdown
22
-
23
- # lid-cli
24
-
25
- To find device, run `gio mount --list --detail` and select ID in the first line.
lid_cli-0.3/README.md DELETED
@@ -1,3 +0,0 @@
1
- # lid-cli
2
-
3
- To find device, run `gio mount --list --detail` and select ID in the first line.
File without changes
File without changes
File without changes
File without changes