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 +54 -0
- lid_cli-0.5/README.md +32 -0
- {lid_cli-0.3 → lid_cli-0.5}/pyproject.toml +2 -2
- {lid_cli-0.3 → lid_cli-0.5}/src/lid/__init__.py +1 -1
- {lid_cli-0.3 → lid_cli-0.5}/src/lid/cli.py +84 -25
- {lid_cli-0.3 → lid_cli-0.5}/src/lid/gio.py +83 -56
- {lid_cli-0.3 → lid_cli-0.5}/src/lid/subprocess.py +8 -2
- {lid_cli-0.3 → lid_cli-0.5}/src/lid/types.py +10 -10
- lid_cli-0.3/PKG-INFO +0 -25
- lid_cli-0.3/README.md +0 -3
- {lid_cli-0.3 → lid_cli-0.5}/LICENSES/MIT.txt +0 -0
- {lid_cli-0.3 → lid_cli-0.5}/src/lid/__main__.py +0 -0
- {lid_cli-0.3 → lid_cli-0.5}/src/lid/io.py +0 -0
- {lid_cli-0.3 → lid_cli-0.5}/src/lid/py.typed +0 -0
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.
|
|
8
|
-
description = "
|
|
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" ]
|
|
@@ -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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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="
|
|
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"
|
|
128
|
-
except lid.types.
|
|
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.
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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.
|
|
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="
|
|
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__ = ["
|
|
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
|
|
47
|
+
async def find_volume_names(
|
|
48
48
|
kw: str, /, mounted_only: bool, env: types.Environment, runner: AsyncRunner
|
|
49
49
|
) -> list[str]:
|
|
50
|
-
|
|
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
|
|
55
|
-
return [d["__name__"] for d in
|
|
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
|
|
59
|
+
"""Mount volume.
|
|
60
60
|
|
|
61
61
|
Parameters
|
|
62
62
|
----------
|
|
63
63
|
kw
|
|
64
|
-
(Partial) name of
|
|
64
|
+
(Partial) name of volume as given in the top line of the gio output section.
|
|
65
65
|
"""
|
|
66
|
-
|
|
66
|
+
volume_info = await _query_matching_volume_info(
|
|
67
67
|
kw, include_details=True, env=env, runner=runner
|
|
68
68
|
)
|
|
69
|
-
|
|
69
|
+
volume = _extract_volume(kw, volume_info=volume_info)
|
|
70
70
|
|
|
71
|
-
name, mount_id, is_mounted =
|
|
71
|
+
name, mount_id, is_mounted = volume.name, volume.id_, volume.is_mounted
|
|
72
72
|
if is_mounted:
|
|
73
|
-
log.info("
|
|
73
|
+
log.info("Volume `%s` already mounted.", name)
|
|
74
74
|
else:
|
|
75
|
-
log.info("Mounting
|
|
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
|
|
80
|
-
raise types.
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
86
|
-
|
|
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.
|
|
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
|
|
107
|
+
"""Unmount volume."""
|
|
102
108
|
if kw is not None:
|
|
103
|
-
|
|
109
|
+
volume_info = await _query_matching_volume_info(
|
|
104
110
|
kw, include_details=True, env=env, runner=runner
|
|
105
111
|
)
|
|
106
|
-
|
|
112
|
+
volume = _extract_volume(kw, volume_info=volume_info)
|
|
107
113
|
|
|
108
|
-
if not
|
|
109
|
-
log.info("
|
|
114
|
+
if not volume.is_mounted:
|
|
115
|
+
log.info("Volume `%s` not mounted.")
|
|
110
116
|
return
|
|
111
|
-
name
|
|
117
|
+
name = volume.name
|
|
112
118
|
|
|
113
119
|
try:
|
|
114
|
-
mountpoint = await _find_mountpoint(
|
|
115
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
151
|
-
"failed to collect gio output for
|
|
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.
|
|
156
|
-
raise types.
|
|
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(
|
|
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
|
|
171
|
-
match
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
223
|
+
loc: str, /, env: types.Environment, runner: AsyncRunner
|
|
198
224
|
) -> str:
|
|
199
225
|
try:
|
|
200
|
-
# Requires
|
|
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",
|
|
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.
|
|
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.
|
|
212
|
-
raise types.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
30
|
-
"""
|
|
29
|
+
class GioError(LidError):
|
|
30
|
+
"""Exception raised when interaction with `gio` failes.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"""
|
|
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(
|
|
38
|
-
"""Exception raised when
|
|
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
|
-
|
|
58
|
+
Volume ID as displayed by `gio mount --list`.
|
|
59
59
|
mountpoint
|
|
60
|
-
Mountpoint (local directory logically linked to
|
|
60
|
+
Mountpoint (local directory logically linked to volume file system).
|
|
61
61
|
triggered
|
|
62
|
-
True if the attempt actually mounted the
|
|
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
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|