yd-cli 0.12__tar.gz → 0.13__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.12 → yd_cli-0.13}/PKG-INFO +3 -3
- {yd_cli-0.12 → yd_cli-0.13}/README.md +2 -2
- {yd_cli-0.12 → yd_cli-0.13}/pyproject.toml +1 -1
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/cli.py +11 -2
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/commands.py +32 -6
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/types.py +4 -0
- {yd_cli-0.12 → yd_cli-0.13}/LICENSES/MIT.txt +0 -0
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/__init__.py +0 -0
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/__main__.py +0 -0
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/echo.py +0 -0
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/execution.py +0 -0
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/io.py +0 -0
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/logger.py +0 -0
- {yd_cli-0.12 → yd_cli-0.13}/src/yd/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yd-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13
|
|
4
4
|
Summary: CLI tool to synchronize directories using rsync.
|
|
5
5
|
Author: Christian Heinze
|
|
6
6
|
License-Expression: MIT
|
|
@@ -104,7 +104,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
104
104
|
| Key | Meaning |
|
|
105
105
|
| --- | --- |
|
|
106
106
|
| `src` | Relative source directory below `src_home`. |
|
|
107
|
-
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders
|
|
107
|
+
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders `%Y`, `%m`, and `%d` are supported. Each may appear at most once; no other use of `%` is allowed. A newly created dated target is removed if it contains only directories after synchronization. |
|
|
108
108
|
| `delete_extra` | Delete files in the target that do not exist in the source. Defaults to `true`. |
|
|
109
109
|
| `exclude` | Extra exclude patterns for this command only. |
|
|
110
110
|
|
|
@@ -116,7 +116,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
116
116
|
- Exactly one of `--src-home -` and `--target-home -` may read from standard input in a single run.
|
|
117
117
|
- If `backup` is omitted, `yd` creates a timestamped backup directory automatically unless `--no-backup` is specified.
|
|
118
118
|
- 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 hard-drive.
|
|
119
|
-
- If
|
|
119
|
+
- If a valid date placeholder is included in the `target`, then the `target_home` is scanned for matching directories (possibly) created by this rule before today.
|
|
120
120
|
The 20 most recent matches are included as additional comparison destinations in the synchronization: if a file in `src` is included in one of those reference directories, then it will not be copied to `target`.
|
|
121
121
|
|
|
122
122
|
## Run a config
|
|
@@ -81,7 +81,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
81
81
|
| Key | Meaning |
|
|
82
82
|
| --- | --- |
|
|
83
83
|
| `src` | Relative source directory below `src_home`. |
|
|
84
|
-
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders
|
|
84
|
+
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders `%Y`, `%m`, and `%d` are supported. Each may appear at most once; no other use of `%` is allowed. A newly created dated target is removed if it contains only directories after synchronization. |
|
|
85
85
|
| `delete_extra` | Delete files in the target that do not exist in the source. Defaults to `true`. |
|
|
86
86
|
| `exclude` | Extra exclude patterns for this command only. |
|
|
87
87
|
|
|
@@ -93,7 +93,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
93
93
|
- Exactly one of `--src-home -` and `--target-home -` may read from standard input in a single run.
|
|
94
94
|
- If `backup` is omitted, `yd` creates a timestamped backup directory automatically unless `--no-backup` is specified.
|
|
95
95
|
- 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 hard-drive.
|
|
96
|
-
- If
|
|
96
|
+
- If a valid date placeholder is included in the `target`, then the `target_home` is scanned for matching directories (possibly) created by this rule before today.
|
|
97
97
|
The 20 most recent matches are included as additional comparison destinations in the synchronization: if a file in `src` is included in one of those reference directories, then it will not be copied to `target`.
|
|
98
98
|
|
|
99
99
|
## Run a config
|
|
@@ -230,16 +230,22 @@ def _report_schedule_result(result: yd.types.ScheduleResult, /) -> None:
|
|
|
230
230
|
)
|
|
231
231
|
case yd.types.ProcessStartError():
|
|
232
232
|
console.print(
|
|
233
|
-
|
|
233
|
+
"Failed to start rsync"
|
|
234
234
|
f" (command {cmd.id_}) for `{cmd.target_path}`.",
|
|
235
235
|
style="error",
|
|
236
236
|
)
|
|
237
237
|
case yd.types.OutputParseError() | yd.types.OutputConsumeError():
|
|
238
238
|
console.print(
|
|
239
|
-
|
|
239
|
+
"Failed to handle rsync output"
|
|
240
240
|
f" (command {cmd.id_}) for `{cmd.target_path}`.",
|
|
241
241
|
style="error",
|
|
242
242
|
)
|
|
243
|
+
case yd.types.CleanupError():
|
|
244
|
+
console.print(
|
|
245
|
+
"Failed to remove empty but newly"
|
|
246
|
+
f" created target `{cmd.target_path}`.",
|
|
247
|
+
style="error",
|
|
248
|
+
)
|
|
243
249
|
case Exception():
|
|
244
250
|
console.print(
|
|
245
251
|
f"Command {cmd.id_} (target `{cmd.target_path}`) failed.",
|
|
@@ -423,6 +429,8 @@ async def _execute_commands(cs: Iterable[yd.types.Command]) -> int:
|
|
|
423
429
|
with rich.progress.Progress(
|
|
424
430
|
rich.progress.TimeElapsedColumn(table_column=rich.table.Column(width=8)),
|
|
425
431
|
rich.progress.TextColumn(
|
|
432
|
+
# TODO: if possible replace this by a gren checkmark once the command is
|
|
433
|
+
# done.
|
|
426
434
|
":file_folder:",
|
|
427
435
|
table_column=rich.table.Column(width=2),
|
|
428
436
|
),
|
|
@@ -539,6 +547,7 @@ async def _execute_commands(cs: Iterable[yd.types.Command]) -> int:
|
|
|
539
547
|
"Failed to remove empty but newly created target `%s`.",
|
|
540
548
|
target_path,
|
|
541
549
|
)
|
|
550
|
+
raise yd.types.CleanupError from None
|
|
542
551
|
else:
|
|
543
552
|
log.info(
|
|
544
553
|
"Removed empty but newly created target `%s`.", target_path
|
|
@@ -148,7 +148,13 @@ class CommandPaths:
|
|
|
148
148
|
)
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
_VAL_DATE_CMP_SPECS = ("%Y", "%m", "%d")
|
|
152
|
+
_INVAL_DATE_CMP_SPEC = re.compile(
|
|
153
|
+
rf"%(?:[^{''.join(p.removeprefix('%') for p in _VAL_DATE_CMP_SPECS)}]|$)"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _derive_command_paths( # noqa: PLR0915
|
|
152
158
|
cmd: types.CommandSpec,
|
|
153
159
|
/,
|
|
154
160
|
env: types.Environment,
|
|
@@ -168,9 +174,30 @@ def _derive_command_paths(
|
|
|
168
174
|
)
|
|
169
175
|
src_is_empty = _inspect_source(src)
|
|
170
176
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
if cmd.target is None:
|
|
178
|
+
target_spec = cmd.src
|
|
179
|
+
target_has_date_placeholder = False
|
|
180
|
+
elif inval_date_cmp := _INVAL_DATE_CMP_SPEC.findall(cmd.target):
|
|
181
|
+
inval_date_cmps = "`, `".join(inval_date_cmp)
|
|
182
|
+
raise types.CommandBuildError(
|
|
183
|
+
"found unsupported potential date specifiers"
|
|
184
|
+
f" `{inval_date_cmps}` in target `{cmd.target}`"
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
date_cmps = {
|
|
188
|
+
c: n for c in _VAL_DATE_CMP_SPECS if (n := cmd.target.count(c)) > 0
|
|
189
|
+
}
|
|
190
|
+
if any(n > 1 for n in date_cmps.values()):
|
|
191
|
+
raise types.CommandBuildError(
|
|
192
|
+
f"found repeated date specifier in `{cmd.target}`"
|
|
193
|
+
)
|
|
194
|
+
if date_cmps:
|
|
195
|
+
target_spec = env.current_time.strftime(cmd.target)
|
|
196
|
+
target_has_date_placeholder = True
|
|
197
|
+
else:
|
|
198
|
+
target_spec = cmd.target
|
|
199
|
+
target_has_date_placeholder = False
|
|
200
|
+
|
|
174
201
|
if (target_path := Path(target_spec)).is_absolute():
|
|
175
202
|
raise types.CommandBuildError(
|
|
176
203
|
f"found absolute target path `{target_path}` in command specification"
|
|
@@ -216,7 +243,6 @@ def _derive_command_paths(
|
|
|
216
243
|
f"target `{target}` intersects with backup path `{backup}`"
|
|
217
244
|
)
|
|
218
245
|
|
|
219
|
-
target_has_date_placeholder = cmd.target is not None and target_spec != cmd.target
|
|
220
246
|
if not target_has_date_placeholder:
|
|
221
247
|
cmp_dests: list[Path] = []
|
|
222
248
|
else:
|
|
@@ -264,7 +290,7 @@ class _DatePathMatch:
|
|
|
264
290
|
|
|
265
291
|
|
|
266
292
|
# The capturing parenthesis modify the behavior of `split` used below.
|
|
267
|
-
_PLACEHOLDER = re.compile(
|
|
293
|
+
_PLACEHOLDER = re.compile(rf"({'|'.join(_VAL_DATE_CMP_SPECS)})")
|
|
268
294
|
|
|
269
295
|
|
|
270
296
|
def _match_dirs(
|
|
@@ -31,6 +31,10 @@ class CommandBuildError(YdError):
|
|
|
31
31
|
"""Exception raised when building the rsync command fails."""
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class CleanupError(YdError):
|
|
35
|
+
"""Exception raised when clean-up fails."""
|
|
36
|
+
|
|
37
|
+
|
|
34
38
|
class ProcessStartError(YdError):
|
|
35
39
|
"""Exception raised when a process fails to start.
|
|
36
40
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|