yd-cli 0.3__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.
- yd/__init__.py +30 -0
- yd/__main__.py +11 -0
- yd/cli.py +484 -0
- yd/commands.py +255 -0
- yd/execution.py +153 -0
- yd/io.py +200 -0
- yd/py.typed +0 -0
- yd/types.py +159 -0
- yd_cli-0.3.dist-info/METADATA +132 -0
- yd_cli-0.3.dist-info/RECORD +13 -0
- yd_cli-0.3.dist-info/WHEEL +4 -0
- yd_cli-0.3.dist-info/entry_points.txt +3 -0
- yd_cli-0.3.dist-info/licenses/LICENSES/MIT.txt +9 -0
yd/types.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Christian Heinze
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""Shared data structures."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import dataclasses
|
|
9
|
+
import enum
|
|
10
|
+
from typing import TYPE_CHECKING, Annotated
|
|
11
|
+
|
|
12
|
+
import msgspec
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import datetime as dt
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SyfError(RuntimeError): ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigLoadError(SyfError):
|
|
23
|
+
"""Exception raised when loading a configuration fails."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EnvCaptureError(SyfError):
|
|
27
|
+
"""Exception raised when capturing the environment fails."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CommandBuildError(SyfError):
|
|
31
|
+
"""Exception raised when building the rsync command fails."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OutputParseError(SyfError):
|
|
35
|
+
"""Exception raised when parsing output of other CLI apps fails."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OutputConsumeError(SyfError):
|
|
39
|
+
"""Exception raised when the consumption of parsed output fails."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclasses.dataclass(slots=True, kw_only=True)
|
|
43
|
+
class Environment:
|
|
44
|
+
current_time: dt.datetime
|
|
45
|
+
log_level: int | None
|
|
46
|
+
|
|
47
|
+
home_dir: Path
|
|
48
|
+
config_dir: Path
|
|
49
|
+
|
|
50
|
+
rsync_bin: str
|
|
51
|
+
echo_bin: str
|
|
52
|
+
editor_bin: str | None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclasses.dataclass(slots=True, frozen=True)
|
|
56
|
+
class AvailableConfig:
|
|
57
|
+
name: str
|
|
58
|
+
description: str | None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
type _NonemptyStr = Annotated[str, msgspec.Meta(min_length=1)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Does not explicitly omit default values as not used for serialization.
|
|
65
|
+
class CommandGroup(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
|
66
|
+
"""Configuration for a group of rsync commands.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
src_home/target_home
|
|
71
|
+
Absolute path used to turn relative src/target path in individual commands into
|
|
72
|
+
absolute paths.
|
|
73
|
+
|
|
74
|
+
The `target_home` is passed to `datetime.strftime` via the `format` parameter,
|
|
75
|
+
that is, placeholders for date/time components like `%Y`, `%m`, etc., are
|
|
76
|
+
replaced with the actual values at runtime.
|
|
77
|
+
mtp_target
|
|
78
|
+
Set True if communication with the target works with MTP. Due to
|
|
79
|
+
technical limitations of that protocol, synchronization needs to be done
|
|
80
|
+
in-place (rather than copying to a temporary file and renaming once
|
|
81
|
+
done). This may lead to corrupted files if the process is interrupted;
|
|
82
|
+
don't set unless necessary.
|
|
83
|
+
backup
|
|
84
|
+
Path to backup directory. Added to each command and if relative, then taken
|
|
85
|
+
as relative to target home. If None, a default relative path is used.
|
|
86
|
+
|
|
87
|
+
The `target` is passed to `datetime.strftime` via the `format` parameter, that
|
|
88
|
+
is, placeholders for date/time components like `%Y`, `%m`, etc., are replaced
|
|
89
|
+
with the actual values at runtime.
|
|
90
|
+
exclude
|
|
91
|
+
Exclude pattern joined to each command's individual exclude patterns.
|
|
92
|
+
|
|
93
|
+
Patterns are matched against every file/directory to be transferred.
|
|
94
|
+
- If not `/` or `**` is included in the pattern, then matches against full path.
|
|
95
|
+
- Otherwise matches against filename.
|
|
96
|
+
- Trailing `/` only matches directories.
|
|
97
|
+
- `*` matches any number of non-`/` characters; `**` any number of any chars.
|
|
98
|
+
- If pattern starts with `/` it's anchored at the start of the path.
|
|
99
|
+
- `?` matches a single character; `[123]` matches one of 1, 2, or 3.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
src_home: _NonemptyStr | None = None
|
|
103
|
+
target_home: _NonemptyStr | None = None
|
|
104
|
+
|
|
105
|
+
mtp_target: bool = False
|
|
106
|
+
backup: _NonemptyStr | None = None
|
|
107
|
+
exclude: list[_NonemptyStr] = msgspec.field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
commands: Annotated[list[Command], msgspec.Meta(min_length=1)]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Command(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
|
113
|
+
"""Individual rsync command specification.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
src, target
|
|
118
|
+
Source/target directory. Required to be relative (to src_home/target_home) path.
|
|
119
|
+
|
|
120
|
+
The `target` is passed to `datetime.strftime` via the `format` parameter, that
|
|
121
|
+
is, placeholders for date/time components like `%Y`, `%m`, etc., are replaced
|
|
122
|
+
with the actual values at runtime.
|
|
123
|
+
delete_extra
|
|
124
|
+
If `True`, then elements present in `target` but not `src` are deleted.
|
|
125
|
+
exclude
|
|
126
|
+
Patterns describing contents of `src` which is not transferred.
|
|
127
|
+
Will be joined with settings in the containing `CommandGroup` object.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
src: _NonemptyStr
|
|
131
|
+
target: _NonemptyStr | None = None
|
|
132
|
+
|
|
133
|
+
delete_extra: bool = True
|
|
134
|
+
exclude: list[_NonemptyStr] = msgspec.field(default_factory=list)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Action(enum.StrEnum):
|
|
138
|
+
DELETE = "*"
|
|
139
|
+
CREATE = "c"
|
|
140
|
+
COPY = ">"
|
|
141
|
+
ATTRUPDATE = "."
|
|
142
|
+
HARDLINK = "h"
|
|
143
|
+
|
|
144
|
+
def __str__(self) -> str:
|
|
145
|
+
return self.name.lower()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Transaction(msgspec.Struct):
|
|
149
|
+
filename: str
|
|
150
|
+
transfer_bytes: int
|
|
151
|
+
info: Annotated[
|
|
152
|
+
# The first character alternative match the non-`*`-options in `Action`.
|
|
153
|
+
str, msgspec.Meta(min_length=11, max_length=11, pattern=r"^(\*|c|>|h|\.)")
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# Custom `dec_hook` requires an obj type unknown to msgspec to be triggered.
|
|
157
|
+
@property
|
|
158
|
+
def action(self) -> Action:
|
|
159
|
+
return Action(self.info[0])
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yd-cli
|
|
3
|
+
Version: 0.3
|
|
4
|
+
Summary: CLI tool to synchronize directories using rsync.
|
|
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 :: System :: Archiving
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: msgspec>=0.20
|
|
18
|
+
Requires-Dist: rich>=14.3
|
|
19
|
+
Requires-Dist: typer>=0.24
|
|
20
|
+
Requires-Python: >=3.14
|
|
21
|
+
Project-URL: Repository, https://codeberg.org/christianheinze/syf
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Directory synchronization tool `yd`
|
|
25
|
+
|
|
26
|
+
Build and execute `rsync` commands from *TOML* configuration files.
|
|
27
|
+
|
|
28
|
+
## Create a config
|
|
29
|
+
|
|
30
|
+
Create a new configuration called `photos` with:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
yd edit --new photos
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This command
|
|
37
|
+
|
|
38
|
+
- creates `~/.config/yd/photos.toml` (or under `$XDG_CONFIG_HOME/yd` if set; relative paths are resolved from your home directory), and
|
|
39
|
+
- opens it in the editor selected via `EDITOR`.
|
|
40
|
+
|
|
41
|
+
Reopen an existing configuration with:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
yd edit photos
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration format
|
|
48
|
+
|
|
49
|
+
### Example
|
|
50
|
+
|
|
51
|
+
```toml
|
|
52
|
+
# Phone backup
|
|
53
|
+
src_home = "/home/alice"
|
|
54
|
+
target_home = "/mnt/backup"
|
|
55
|
+
backup = "deleted/%Y-%m-%d"
|
|
56
|
+
exclude = [".venv/", "__pycache__/"]
|
|
57
|
+
|
|
58
|
+
[[commands]]
|
|
59
|
+
src = "Documents"
|
|
60
|
+
exclude = ["*.log"]
|
|
61
|
+
|
|
62
|
+
[[commands]]
|
|
63
|
+
src = "Pictures"
|
|
64
|
+
target = "pics-%Y-%m-%d"
|
|
65
|
+
delete_extra = false
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Leading comment lines directly at the top of the file are treated as the configuration description and are shown by `yd ls`.
|
|
69
|
+
|
|
70
|
+
### Top-level options
|
|
71
|
+
|
|
72
|
+
| Key | Meaning |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `src_home` | Base directory for all `src` paths. Relative paths are resolved from your home directory. |
|
|
75
|
+
| `target_home` | Base directory for all target paths. Relative paths are resolved from your home directory. `strftime` placeholders such as `%Y-%m-%d` are supported. |
|
|
76
|
+
| `mtp_target` | Use in-place syncing for MTP targets. |
|
|
77
|
+
| `backup` | Backup directory for replaced or deleted files. Relative paths are resolved from `target_home`. `strftime` placeholders are supported. |
|
|
78
|
+
| `exclude` | Exclude patterns applied to every command. |
|
|
79
|
+
|
|
80
|
+
`mtp_target` matters because `rsync` normally copies to a temporary file and renames it afterward, but that is not possible when syncing via *MTP*.
|
|
81
|
+
|
|
82
|
+
### Command options
|
|
83
|
+
|
|
84
|
+
| Key | Meaning |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `src` | Relative source directory below `src_home`. |
|
|
87
|
+
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders are supported. |
|
|
88
|
+
| `delete_extra` | Delete files in the target that do not exist in the source. Defaults to `true`. |
|
|
89
|
+
| `exclude` | Extra exclude patterns for this command only. |
|
|
90
|
+
|
|
91
|
+
### Notes
|
|
92
|
+
|
|
93
|
+
- `src` and `target` must stay within `src_home` and `target_home` after path resolution.
|
|
94
|
+
- `src_home` or `target_home` may be omitted from the configuration, but every missing value must then be supplied when running (via CLI parameters).
|
|
95
|
+
- Exactly one of `--src-home -` and `--target-home -` may read from standard input in a single run.
|
|
96
|
+
- If `backup` is omitted, `yd` creates a timestamped backup directory automatically unless `--no-backup` is specified.
|
|
97
|
+
- If a configured source directory exists but is empty, `yd` reports that no synchronization was performed for that command. E.g., forgetting to mount an external drive does not delete all corresponding copies on harddrive).
|
|
98
|
+
|
|
99
|
+
## Run a config
|
|
100
|
+
|
|
101
|
+
Run a saved configuration by name:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
yd run photos
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Options
|
|
108
|
+
|
|
109
|
+
| Option | Meaning |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| `--dry-run` | Show what would happen without changing files. |
|
|
112
|
+
| `--no-backup` | Disable backup handling for this run. |
|
|
113
|
+
| `--keep-newer` | Skip updates when the target file is newer. |
|
|
114
|
+
| `--rename-speedup` | Enable `rsync` options tuned for rename-heavy targets. This may require more disk space on the target. |
|
|
115
|
+
| `--src-home PATH` | Override `src_home` from the config. Use `-` to read the value from standard input. |
|
|
116
|
+
| `--target-home PATH` | Override `target_home` from the config. Use `-` to read the value from standard input. |
|
|
117
|
+
|
|
118
|
+
## List configs
|
|
119
|
+
|
|
120
|
+
List available configurations with:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
yd ls
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This shows the configuration name together with the optional leading-comment description.
|
|
127
|
+
|
|
128
|
+
## Why `yd`?
|
|
129
|
+
|
|
130
|
+
- Has one character from `synchronize` and one from `directory`.
|
|
131
|
+
- Easy to type with both *QWERTZ* and *QUERTY* keyboards.
|
|
132
|
+
- Name was still available on *PyPI*.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
yd/__init__.py,sha256=r2mprMYiyYZ1lIxZRseO4qOjsCLf5UNccL71EPm8Tcw,878
|
|
2
|
+
yd/__main__.py,sha256=6NlT1w1H9uWETLssY37wSpRuIFouOoomfY5DcQ1YU10,201
|
|
3
|
+
yd/cli.py,sha256=2dqSDRpS_-G3i2fkqQ4jYsCGMSEChN-eCI4yqO02GDw,15619
|
|
4
|
+
yd/commands.py,sha256=BlU9KM1EpO2sXhZITzs9rv1Hymlx-WAsGDv-PKodFKo,9059
|
|
5
|
+
yd/execution.py,sha256=UHMi2JJEou_V0uIVJ4NLHLv8QNEytwlOkp4ou7NK2Do,4778
|
|
6
|
+
yd/io.py,sha256=XYVugCKhbYkS8lEqDcGsnI8fo7I5CoshXJ63jm95Ims,6428
|
|
7
|
+
yd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
yd/types.py,sha256=6jb8zkvaDIQ_1-6lxM86VCe8NQsFFms-ygRKwNXy2o8,5035
|
|
9
|
+
yd_cli-0.3.dist-info/licenses/LICENSES/MIT.txt,sha256=unowdk_KzQ7oBGrEJBQJOTzhui6aTsTzSSfjf447LKM,1077
|
|
10
|
+
yd_cli-0.3.dist-info/WHEEL,sha256=bEhYrD-rjlF0iRRHiAnfJ0mEjMsRwm29hhDD7yRgWCY,80
|
|
11
|
+
yd_cli-0.3.dist-info/entry_points.txt,sha256=D1fJ5Xv9YGtR8HaP_AC4h36jwV1FZpysiMLQIqgNW3Q,35
|
|
12
|
+
yd_cli-0.3.dist-info/METADATA,sha256=WMy6Jcjvl6L2WFJcjeCVVQJIuuG8kzHxfAeEfzSRtmI,4543
|
|
13
|
+
yd_cli-0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Christian Heinze
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|