yd-cli 0.7__tar.gz → 0.9__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.
- {yd_cli-0.7 → yd_cli-0.9}/PKG-INFO +1 -1
- {yd_cli-0.7 → yd_cli-0.9}/pyproject.toml +3 -1
- {yd_cli-0.7 → yd_cli-0.9}/src/yd/cli.py +23 -32
- {yd_cli-0.7 → yd_cli-0.9}/src/yd/commands.py +5 -2
- yd_cli-0.9/src/yd/echo.py +12 -0
- {yd_cli-0.7 → yd_cli-0.9}/src/yd/io.py +8 -10
- {yd_cli-0.7 → yd_cli-0.9}/src/yd/types.py +7 -7
- {yd_cli-0.7 → yd_cli-0.9}/LICENSES/MIT.txt +0 -0
- {yd_cli-0.7 → yd_cli-0.9}/README.md +0 -0
- {yd_cli-0.7 → yd_cli-0.9}/src/yd/__init__.py +0 -0
- {yd_cli-0.7 → yd_cli-0.9}/src/yd/__main__.py +0 -0
- {yd_cli-0.7 → yd_cli-0.9}/src/yd/execution.py +0 -0
- {yd_cli-0.7 → yd_cli-0.9}/src/yd/py.typed +0 -0
|
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.10,<0.12" ]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yd-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9"
|
|
8
8
|
description = "CLI tool to synchronize directories using rsync."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -147,6 +147,8 @@ lint.ignore = [
|
|
|
147
147
|
"PLR0913",
|
|
148
148
|
# Comparison with hardcoded literals allowed.
|
|
149
149
|
"PLR2004",
|
|
150
|
+
# open(Path) is ok
|
|
151
|
+
"PTH123",
|
|
150
152
|
"Q000",
|
|
151
153
|
"Q001",
|
|
152
154
|
"Q002",
|
|
@@ -24,7 +24,6 @@ import yd
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
25
|
import enum
|
|
26
26
|
from collections.abc import Callable
|
|
27
|
-
from collections.abc import Set as AbstractSet
|
|
28
27
|
|
|
29
28
|
from rich.text import Text
|
|
30
29
|
|
|
@@ -41,7 +40,6 @@ class StrEnumHighlighter[T: enum.StrEnum](
|
|
|
41
40
|
enum_type: type[T]
|
|
42
41
|
|
|
43
42
|
_: dataclasses.KW_ONLY
|
|
44
|
-
ignore: AbstractSet[T] = dataclasses.field(default_factory=set)
|
|
45
43
|
base_style_name: str | None = None
|
|
46
44
|
|
|
47
45
|
@property
|
|
@@ -55,45 +53,43 @@ class StrEnumHighlighter[T: enum.StrEnum](
|
|
|
55
53
|
def highlight(self, text: Text) -> None:
|
|
56
54
|
base_style, hl = self.base_style, text.highlight_regex
|
|
57
55
|
for cat in self.enum_type:
|
|
58
|
-
if cat in self.ignore:
|
|
59
|
-
continue
|
|
60
56
|
# Using `__str__` instead of `cat.name.lower()` for more flexibility.
|
|
61
57
|
hl(rf"(?P<{cat}>{cat})", style_prefix=base_style)
|
|
62
58
|
|
|
63
59
|
def create_theme_addon(self, *, styler: Callable[[T], str]) -> dict[str, str]:
|
|
64
60
|
base_style = self.base_style
|
|
65
|
-
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
return {base_style + str(cat): styler(cat) for cat in self.enum_type}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _action_name(action: yd.types.Action) -> str:
|
|
65
|
+
if action is yd.types.Action.HARDLINK:
|
|
66
|
+
return str(yd.types.Action.COPY)
|
|
67
|
+
return str(action)
|
|
70
68
|
|
|
71
69
|
|
|
72
70
|
def _action_styler(action: yd.types.Action) -> str:
|
|
73
71
|
match action:
|
|
74
72
|
case yd.types.Action.CREATE:
|
|
75
|
-
return "bold
|
|
73
|
+
return "bold black on blue"
|
|
76
74
|
case yd.types.Action.COPY:
|
|
77
|
-
return "bold
|
|
75
|
+
return "bold black on green"
|
|
78
76
|
case yd.types.Action.DELETE:
|
|
79
|
-
return "bold
|
|
77
|
+
return "bold black on red"
|
|
80
78
|
case yd.types.Action.HARDLINK:
|
|
81
|
-
return "bold
|
|
79
|
+
return "bold black on bright_black"
|
|
82
80
|
case yd.types.Action.ATTRUPDATE:
|
|
83
|
-
return "bold
|
|
81
|
+
return "bold black on yellow"
|
|
84
82
|
|
|
85
83
|
|
|
86
84
|
def _setup_rich_devices() -> tuple[rich.console.Console, StrEnumHighlighter]:
|
|
87
|
-
highlighter = StrEnumHighlighter(
|
|
88
|
-
yd.types.Action, ignore={yd.types.Action.ATTRUPDATE}
|
|
89
|
-
)
|
|
85
|
+
highlighter = StrEnumHighlighter(yd.types.Action)
|
|
90
86
|
theme = rich.theme.Theme(
|
|
91
87
|
highlighter.create_theme_addon(styler=_action_styler)
|
|
92
88
|
| {
|
|
93
|
-
"progress.elapsed": "italic
|
|
94
|
-
"info": "italic
|
|
95
|
-
"warning": "italic
|
|
96
|
-
"error": "bold
|
|
89
|
+
"progress.elapsed": "italic yellow",
|
|
90
|
+
"info": "italic yellow",
|
|
91
|
+
"warning": "italic red",
|
|
92
|
+
"error": "bold bright_red",
|
|
97
93
|
}
|
|
98
94
|
)
|
|
99
95
|
console = rich.console.Console(highlighter=highlighter, theme=theme)
|
|
@@ -329,14 +325,12 @@ def run(
|
|
|
329
325
|
rich.progress.TextColumn(
|
|
330
326
|
f"{{task.fields[{action_name}]}}",
|
|
331
327
|
justify="right",
|
|
332
|
-
table_column=rich.table.Column(min_width=
|
|
328
|
+
table_column=rich.table.Column(min_width=5),
|
|
333
329
|
),
|
|
334
330
|
)
|
|
335
|
-
for action_name in
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if cat not in highlighter.ignore
|
|
339
|
-
)
|
|
331
|
+
for action_name in {
|
|
332
|
+
_action_name(cat): None for cat in highlighter.enum_type
|
|
333
|
+
}
|
|
340
334
|
),
|
|
341
335
|
console=console,
|
|
342
336
|
) as progress,
|
|
@@ -355,19 +349,16 @@ def run(
|
|
|
355
349
|
console.print(text, style="info")
|
|
356
350
|
return
|
|
357
351
|
case yd.types.Transaction() as transaction:
|
|
358
|
-
if transaction.action is yd.types.Action.ATTRUPDATE:
|
|
359
|
-
return
|
|
360
|
-
|
|
361
352
|
console.print(
|
|
362
353
|
template.format(
|
|
363
|
-
action=transaction.action,
|
|
354
|
+
action=_action_name(transaction.action),
|
|
364
355
|
filename=target.joinpath(transaction.filename).as_posix()
|
|
365
356
|
if not Path(transaction.filename).is_absolute()
|
|
366
357
|
else transaction.filename,
|
|
367
358
|
transfer_bytes=transaction.transfer_bytes,
|
|
368
359
|
)
|
|
369
360
|
)
|
|
370
|
-
counter[
|
|
361
|
+
counter[_action_name(transaction.action)] += 1
|
|
371
362
|
progress.update(
|
|
372
363
|
taskid,
|
|
373
364
|
**counter, # type: ignore[ty:invalid-argument-type]
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import itertools as it
|
|
9
9
|
import logging
|
|
10
|
+
import sys
|
|
10
11
|
import uuid
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
@@ -48,8 +49,10 @@ def _format_path(path: Path, /) -> str:
|
|
|
48
49
|
|
|
49
50
|
def _create_error_command(
|
|
50
51
|
enc_msg: str, /, environment: types.Environment
|
|
51
|
-
) -> tuple[str, tuple[str]]:
|
|
52
|
-
|
|
52
|
+
) -> tuple[str, tuple[str, ...]]:
|
|
53
|
+
if environment.echo_bin is not None:
|
|
54
|
+
return environment.echo_bin, (enc_msg,)
|
|
55
|
+
return sys.executable, ("-m", "yd.echo", enc_msg)
|
|
53
56
|
|
|
54
57
|
|
|
55
58
|
def create( # noqa: PLR0915
|
|
@@ -59,10 +59,7 @@ def _find_binary(binary: str, /, accept_failure: bool = False) -> str | None:
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def capture_environment() -> types.Environment:
|
|
62
|
-
"""Capture environment.
|
|
63
|
-
|
|
64
|
-
Raises `types.ConfigLoadError` in case of failure.
|
|
65
|
-
"""
|
|
62
|
+
"""Capture environment."""
|
|
66
63
|
home_dir = Path.home()
|
|
67
64
|
if not home_dir.exists():
|
|
68
65
|
raise types.EnvCaptureError(f"home dir `{home_dir}` does not exist")
|
|
@@ -85,11 +82,12 @@ def capture_environment() -> types.Environment:
|
|
|
85
82
|
rsync_bin = _find_binary(_getenv("YD_RSYNC", "rsync"))
|
|
86
83
|
except FileNotFoundError as err:
|
|
87
84
|
raise types.EnvCaptureError("rsync binary not found") from err
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
raise types.EnvCaptureError("echo binary not found") from err
|
|
85
|
+
echo_bin = _find_binary(_getenv("YD_ECHO", "echo"), accept_failure=True)
|
|
86
|
+
if echo_bin is None:
|
|
87
|
+
log.warning("Failed to find echo command. Resorting to builtin alternative.")
|
|
92
88
|
editor_bin = _find_binary(_getenv("EDITOR", "vim"), accept_failure=True)
|
|
89
|
+
if editor_bin is None:
|
|
90
|
+
log.warning("Failed to find editor command.")
|
|
93
91
|
|
|
94
92
|
try:
|
|
95
93
|
log_level: int | None = int(os.getenv("YD_LOGLEVEL", ""))
|
|
@@ -116,7 +114,7 @@ def load_command_group(path: Path, /) -> types.CommandGroup:
|
|
|
116
114
|
Path to TOML file specifying the program.
|
|
117
115
|
"""
|
|
118
116
|
try:
|
|
119
|
-
with
|
|
117
|
+
with open(path, mode="rb") as fh:
|
|
120
118
|
return msgspec.toml.decode(fh.read(), type=types.CommandGroup)
|
|
121
119
|
except FileNotFoundError as err:
|
|
122
120
|
log.exception("missing configuration file `%s`", path)
|
|
@@ -141,7 +139,7 @@ def list_available_configs(config_dir: Path, /) -> Iterable[types.AvailableConfi
|
|
|
141
139
|
if not path.is_file():
|
|
142
140
|
continue
|
|
143
141
|
try:
|
|
144
|
-
with
|
|
142
|
+
with open(path, encoding="utf-8") as fh:
|
|
145
143
|
# Benchmarked variants using one or more of `itertools.takewhile` and
|
|
146
144
|
# `yield` but this had best runtime.
|
|
147
145
|
lines = []
|
|
@@ -16,26 +16,26 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class
|
|
19
|
+
class YdError(RuntimeError): ...
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class ConfigLoadError(
|
|
22
|
+
class ConfigLoadError(YdError):
|
|
23
23
|
"""Exception raised when loading a configuration fails."""
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class EnvCaptureError(
|
|
26
|
+
class EnvCaptureError(YdError):
|
|
27
27
|
"""Exception raised when capturing the environment fails."""
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
class CommandBuildError(
|
|
30
|
+
class CommandBuildError(YdError):
|
|
31
31
|
"""Exception raised when building the rsync command fails."""
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class OutputParseError(
|
|
34
|
+
class OutputParseError(YdError):
|
|
35
35
|
"""Exception raised when parsing output of other CLI apps fails."""
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
class OutputConsumeError(
|
|
38
|
+
class OutputConsumeError(YdError):
|
|
39
39
|
"""Exception raised when the consumption of parsed output fails."""
|
|
40
40
|
|
|
41
41
|
|
|
@@ -48,7 +48,7 @@ class Environment:
|
|
|
48
48
|
config_dir: Path
|
|
49
49
|
|
|
50
50
|
rsync_bin: str
|
|
51
|
-
echo_bin: str
|
|
51
|
+
echo_bin: str | None
|
|
52
52
|
editor_bin: str | None
|
|
53
53
|
|
|
54
54
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|