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/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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ yd = yd.cli:app
3
+
@@ -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.