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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yd-cli
3
- Version: 0.12
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 (`%Y`, `%m`, and `%d` only!) are supported. A newly created dated target is removed if it contains only directories after synchronization. |
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 `strftime` placeholders are included in the `target`, then the `target_home` is scanned for matching directories (possibly) created by this rule before today.
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 (`%Y`, `%m`, and `%d` only!) are supported. A newly created dated target is removed if it contains only directories after synchronization. |
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 `strftime` placeholders are included in the `target`, then the `target_home` is scanned for matching directories (possibly) created by this rule before today.
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
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.10,<0.12" ]
4
4
 
5
5
  [project]
6
6
  name = "yd-cli"
7
- version = "0.12"
7
+ version = "0.13"
8
8
  description = "CLI tool to synchronize directories using rsync."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- f"Failed to start rsync"
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
- f"Failed to handle rsync output"
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
- def _derive_command_paths(
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
- target_spec = (
172
- cmd.src if cmd.target is None else env.current_time.strftime(cmd.target)
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(r"(%Y|%m|%d)")
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