lid-cli 0.2__tar.gz → 0.4__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.4/PKG-INFO +54 -0
- lid_cli-0.4/README.md +32 -0
- {lid_cli-0.2 → lid_cli-0.4}/pyproject.toml +2 -2
- {lid_cli-0.2 → lid_cli-0.4}/src/lid/__init__.py +1 -1
- {lid_cli-0.2 → lid_cli-0.4}/src/lid/cli.py +84 -25
- {lid_cli-0.2 → lid_cli-0.4}/src/lid/gio.py +54 -53
- {lid_cli-0.2 → lid_cli-0.4}/src/lid/subprocess.py +8 -2
- {lid_cli-0.2 → lid_cli-0.4}/src/lid/types.py +10 -10
- lid_cli-0.2/PKG-INFO +0 -31
- lid_cli-0.2/README.md +0 -9
- {lid_cli-0.2 → lid_cli-0.4}/LICENSES/MIT.txt +0 -0
- {lid_cli-0.2 → lid_cli-0.4}/src/lid/__main__.py +0 -0
- {lid_cli-0.2 → lid_cli-0.4}/src/lid/io.py +0 -0
- {lid_cli-0.2 → lid_cli-0.4}/src/lid/py.typed +0 -0
lid_cli-0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lid-cli
|
|
3
|
+
Version: 0.4
|
|
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.4/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.4"
|
|
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,54 @@ 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
|
-
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
|
|
83
81
|
|
|
84
82
|
try:
|
|
85
|
-
mp = await _find_mountpoint(
|
|
86
|
-
|
|
83
|
+
mp = await _find_mountpoint(
|
|
84
|
+
volume.data["activation_root"], env=env, runner=runner
|
|
85
|
+
)
|
|
86
|
+
except types.GioError as err:
|
|
87
87
|
log.exception("Found no mount point for `%s`.", name)
|
|
88
|
-
raise types.
|
|
88
|
+
raise types.GioError(f"found no mount point for `{name}`") from err
|
|
89
89
|
|
|
90
90
|
return types.Mount(name=name, mountpoint=mp, triggered=mount_id is not None)
|
|
91
91
|
|
|
@@ -98,23 +98,25 @@ async def umount(
|
|
|
98
98
|
env: types.Environment,
|
|
99
99
|
runner: AsyncRunner,
|
|
100
100
|
) -> None:
|
|
101
|
-
"""Unmount
|
|
101
|
+
"""Unmount volume."""
|
|
102
102
|
if kw is not None:
|
|
103
|
-
|
|
103
|
+
volume_info = await _query_matching_volume_info(
|
|
104
104
|
kw, include_details=True, env=env, runner=runner
|
|
105
105
|
)
|
|
106
|
-
|
|
106
|
+
volume = _extract_volume(kw, volume_info=volume_info)
|
|
107
107
|
|
|
108
|
-
if not
|
|
109
|
-
log.info("
|
|
108
|
+
if not volume.is_mounted:
|
|
109
|
+
log.info("Volume `%s` not mounted.")
|
|
110
110
|
return
|
|
111
|
-
name
|
|
111
|
+
name = volume.name
|
|
112
112
|
|
|
113
113
|
try:
|
|
114
|
-
mountpoint = await _find_mountpoint(
|
|
115
|
-
|
|
114
|
+
mountpoint = await _find_mountpoint(
|
|
115
|
+
volume.data["activation_root"], env=env, runner=runner
|
|
116
|
+
)
|
|
117
|
+
except types.GioError as err:
|
|
116
118
|
log.exception("Found no mount point for `%s`.", name)
|
|
117
|
-
raise types.
|
|
119
|
+
raise types.GioError(f"found no mount point for `{name}`") from err
|
|
118
120
|
else:
|
|
119
121
|
if TYPE_CHECKING:
|
|
120
122
|
mount = cast("types.Mount", mount)
|
|
@@ -123,23 +125,26 @@ async def umount(
|
|
|
123
125
|
try:
|
|
124
126
|
await runner(env.gio_bin, "mount", "--unmount", mountpoint)
|
|
125
127
|
except types.SubprocessError as err:
|
|
126
|
-
raise types.
|
|
128
|
+
raise types.GioError(f"failed to unmount `{mountpoint}`") from err
|
|
127
129
|
|
|
128
130
|
|
|
129
131
|
@dataclass(slots=True, kw_only=True)
|
|
130
|
-
class
|
|
132
|
+
class _Volume:
|
|
131
133
|
name: str
|
|
132
134
|
id_: str
|
|
135
|
+
|
|
133
136
|
is_mounted: bool
|
|
134
137
|
data: dict[str, Any]
|
|
135
138
|
|
|
136
139
|
|
|
137
|
-
async def
|
|
140
|
+
async def _query_matching_volume_info(
|
|
138
141
|
kw: str, /, include_details: bool, env: types.Environment, runner: AsyncRunner
|
|
139
142
|
) -> Sequence[dict[str, Any]]:
|
|
140
|
-
|
|
141
|
-
raise ValueError("empty keyword cannot be used for device identification")
|
|
143
|
+
"""Query volume information.
|
|
142
144
|
|
|
145
|
+
Only volumes with names containing `kw` (interpreted as a regular expression)
|
|
146
|
+
are returned.
|
|
147
|
+
"""
|
|
143
148
|
if include_details:
|
|
144
149
|
gio_args = ("mount", "--list", "--detail")
|
|
145
150
|
else:
|
|
@@ -147,30 +152,28 @@ async def _query_matching_device_info(
|
|
|
147
152
|
try:
|
|
148
153
|
gio_out = await runner(env.gio_bin, *gio_args)
|
|
149
154
|
except types.SubprocessError as err:
|
|
150
|
-
raise types.
|
|
151
|
-
"failed to collect gio output for
|
|
155
|
+
raise types.GioError(
|
|
156
|
+
"failed to collect gio output for volume discovery"
|
|
152
157
|
) from err
|
|
153
158
|
try:
|
|
154
159
|
info = _parse_output(gio_out)
|
|
155
|
-
except types.
|
|
156
|
-
raise types.
|
|
157
|
-
"failed to parse gio output for device discovery"
|
|
158
|
-
) from err
|
|
160
|
+
except types.GioError as err:
|
|
161
|
+
raise types.GioError("failed to parse gio output for volume discovery") from err
|
|
159
162
|
|
|
160
163
|
pattern = re.compile(kw, re.IGNORECASE)
|
|
161
164
|
return [
|
|
162
165
|
v
|
|
163
166
|
for k, v in info.items()
|
|
164
|
-
if k.startswith(
|
|
167
|
+
if k.startswith("Volume")
|
|
165
168
|
and isinstance(v, dict)
|
|
166
169
|
and pattern.search(v.get("__name__", "")) is not None
|
|
167
170
|
]
|
|
168
171
|
|
|
169
172
|
|
|
170
|
-
def
|
|
171
|
-
match
|
|
173
|
+
def _extract_volume(kw: str, /, volume_info: Sequence[dict[str, Any]]) -> _Volume:
|
|
174
|
+
match volume_info:
|
|
172
175
|
case []:
|
|
173
|
-
raise types.GioNotFoundError(f"found no
|
|
176
|
+
raise types.GioNotFoundError(f"found no volume matching for `{kw}`")
|
|
174
177
|
case [{"__name__": name} as v]:
|
|
175
178
|
log.debug("Matched `%s` to full name `%s`.", kw, name)
|
|
176
179
|
|
|
@@ -183,35 +186,33 @@ def _extract_device(kw: str, /, device_info: Sequence[dict[str, Any]]) -> _Devic
|
|
|
183
186
|
case _:
|
|
184
187
|
log.error("Failed to find mount ID in `%s`.", v)
|
|
185
188
|
raise types.GioNotFoundError(f"failed to find ID for `{name}`")
|
|
186
|
-
return
|
|
189
|
+
return _Volume(name=name, id_=mount_id, is_mounted=is_mounted, data=v)
|
|
187
190
|
case [f, s, *ms]:
|
|
188
191
|
fn, sn, lms = f["__name__"], s["__name__"], len(ms)
|
|
189
192
|
raise types.GioNotFoundError(
|
|
190
193
|
f"found multiple matches for `{kw}`: `{fn}`, `{sn}`, and {lms} more."
|
|
191
194
|
)
|
|
192
195
|
case _:
|
|
193
|
-
raise types.
|
|
196
|
+
raise types.GioError("unsupported gio output structure")
|
|
194
197
|
|
|
195
198
|
|
|
196
199
|
async def _find_mountpoint(
|
|
197
200
|
id_: str, /, env: types.Environment, runner: AsyncRunner
|
|
198
201
|
) -> str:
|
|
199
202
|
try:
|
|
200
|
-
# Requires
|
|
203
|
+
# Requires volume to be mounted.
|
|
201
204
|
# Need to the US locale as the output key are adapted to that setting.
|
|
202
205
|
gio_out = await runner(
|
|
203
206
|
env.gio_bin, "info", "-a", "local", id_, env_mods={"LANG": "en_US-UTF-8"}
|
|
204
207
|
)
|
|
205
208
|
except types.SubprocessError as err:
|
|
206
|
-
raise types.
|
|
209
|
+
raise types.GioError(
|
|
207
210
|
"failed to collect gio output for mountpoint discovery"
|
|
208
211
|
) from err
|
|
209
212
|
try:
|
|
210
213
|
gio_out = _parse_output(gio_out)
|
|
211
|
-
except types.
|
|
212
|
-
raise types.
|
|
213
|
-
"failed to parse gio output for path discovery"
|
|
214
|
-
) from err
|
|
214
|
+
except types.GioError as err:
|
|
215
|
+
raise types.GioError("failed to parse gio output for path discovery") from err
|
|
215
216
|
|
|
216
217
|
return gio_out["local path"]
|
|
217
218
|
|
|
@@ -233,7 +234,7 @@ def _parse_output(text: str, /, header_name: str = "__name__") -> dict[str, Any]
|
|
|
233
234
|
stack.pop()
|
|
234
235
|
current = stack[-1]
|
|
235
236
|
if line.indent < current.indent:
|
|
236
|
-
raise types.
|
|
237
|
+
raise types.GioError(
|
|
237
238
|
f"indent {line.indent} does not match any previous level"
|
|
238
239
|
)
|
|
239
240
|
|
|
@@ -293,7 +294,7 @@ def _parse_line(
|
|
|
293
294
|
log.debug("Extracted content: `%s` (indent: %s).", content, indent)
|
|
294
295
|
|
|
295
296
|
if (spec_match := _SPEC.fullmatch(content)) is None:
|
|
296
|
-
raise types.
|
|
297
|
+
raise types.GioError("line not matching expected pattern: `%s`", content)
|
|
297
298
|
|
|
298
299
|
key, value = spec_match.group("key"), spec_match.group("val") or None
|
|
299
300
|
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.2/PKG-INFO
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lid-cli
|
|
3
|
-
Version: 0.2
|
|
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.
|
|
26
|
-
|
|
27
|
-
## TODO
|
|
28
|
-
|
|
29
|
-
- More error handling and testing.
|
|
30
|
-
- Build CLI with mount and umount commands.
|
|
31
|
-
- Possibly write target to disk in a small file for umount to read from.
|
lid_cli-0.2/README.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# lid-cli
|
|
2
|
-
|
|
3
|
-
To find device, run `gio mount --list --detail` and select ID in the first line.
|
|
4
|
-
|
|
5
|
-
## TODO
|
|
6
|
-
|
|
7
|
-
- More error handling and testing.
|
|
8
|
-
- Build CLI with mount and umount commands.
|
|
9
|
-
- Possibly write target to disk in a small file for umount to read from.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|