yd-cli 0.10__tar.gz → 0.11__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.10 → yd_cli-0.11}/PKG-INFO +25 -3
- {yd_cli-0.10 → yd_cli-0.11}/README.md +24 -2
- {yd_cli-0.10 → yd_cli-0.11}/pyproject.toml +1 -1
- {yd_cli-0.10 → yd_cli-0.11}/src/yd/cli.py +39 -12
- {yd_cli-0.10 → yd_cli-0.11}/src/yd/commands.py +81 -7
- {yd_cli-0.10 → yd_cli-0.11}/src/yd/execution.py +1 -0
- {yd_cli-0.10 → yd_cli-0.11}/src/yd/io.py +0 -5
- {yd_cli-0.10 → yd_cli-0.11}/src/yd/types.py +1 -5
- {yd_cli-0.10 → yd_cli-0.11}/LICENSES/MIT.txt +0 -0
- {yd_cli-0.10 → yd_cli-0.11}/src/yd/__init__.py +0 -0
- {yd_cli-0.10 → yd_cli-0.11}/src/yd/__main__.py +0 -0
- {yd_cli-0.10 → yd_cli-0.11}/src/yd/echo.py +0 -0
- {yd_cli-0.10 → yd_cli-0.11}/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.11
|
|
4
4
|
Summary: CLI tool to synchronize directories using rsync.
|
|
5
5
|
Author: Christian Heinze
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,6 +25,26 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
Build and execute `rsync` commands from *TOML* configuration files.
|
|
27
27
|
|
|
28
|
+
## (Un)Install
|
|
29
|
+
|
|
30
|
+
To install, run
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install yd-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then run
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
yd --install-completion
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
to install auto-completion in your shell.
|
|
43
|
+
|
|
44
|
+
In `bash`, the completion code is stored in `~/.bash_completions/yd.sh` and that file is sourced from `~/.bashrc`.
|
|
45
|
+
Remove both and call `uv tool uninstall yd-cli` to remove this tool.
|
|
46
|
+
The below mentioned configuration files need to be removed separately.
|
|
47
|
+
|
|
28
48
|
## Create a config
|
|
29
49
|
|
|
30
50
|
Create a new configuration called `photos` with:
|
|
@@ -72,7 +92,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
72
92
|
| Key | Meaning |
|
|
73
93
|
| --- | --- |
|
|
74
94
|
| `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.
|
|
95
|
+
| `target_home` | Base directory for all target paths. Relative paths are resolved from your home directory. |
|
|
76
96
|
| `mtp_target` | Use in-place syncing for MTP targets. |
|
|
77
97
|
| `backup` | Backup directory for replaced or deleted files. Relative paths are resolved from `target_home`. `strftime` placeholders are supported. |
|
|
78
98
|
| `exclude` | Exclude patterns applied to every command. |
|
|
@@ -84,7 +104,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
84
104
|
| Key | Meaning |
|
|
85
105
|
| --- | --- |
|
|
86
106
|
| `src` | Relative source directory below `src_home`. |
|
|
87
|
-
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders are supported. |
|
|
107
|
+
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders (`%Y`, `%m`, and `%d` only!) are supported. |
|
|
88
108
|
| `delete_extra` | Delete files in the target that do not exist in the source. Defaults to `true`. |
|
|
89
109
|
| `exclude` | Extra exclude patterns for this command only. |
|
|
90
110
|
|
|
@@ -95,6 +115,8 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
95
115
|
- Exactly one of `--src-home -` and `--target-home -` may read from standard input in a single run.
|
|
96
116
|
- If `backup` is omitted, `yd` creates a timestamped backup directory automatically unless `--no-backup` is specified.
|
|
97
117
|
- 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).
|
|
118
|
+
- If `strftime` placeholders are included in the `target`, then the `target_home` is scanned for directories that may have been created by this rule in the past. All matches are included as additional references in the synchronization: if a file in `src` is included in one of those reference directories, then it will not be copied to `target`.
|
|
119
|
+
- If the `target` directory did not exist before and no files are transferred into it, then it will be removed again.
|
|
98
120
|
|
|
99
121
|
## Run a config
|
|
100
122
|
|
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Build and execute `rsync` commands from *TOML* configuration files.
|
|
4
4
|
|
|
5
|
+
## (Un)Install
|
|
6
|
+
|
|
7
|
+
To install, run
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv tool install yd-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yd --install-completion
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
to install auto-completion in your shell.
|
|
20
|
+
|
|
21
|
+
In `bash`, the completion code is stored in `~/.bash_completions/yd.sh` and that file is sourced from `~/.bashrc`.
|
|
22
|
+
Remove both and call `uv tool uninstall yd-cli` to remove this tool.
|
|
23
|
+
The below mentioned configuration files need to be removed separately.
|
|
24
|
+
|
|
5
25
|
## Create a config
|
|
6
26
|
|
|
7
27
|
Create a new configuration called `photos` with:
|
|
@@ -49,7 +69,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
49
69
|
| Key | Meaning |
|
|
50
70
|
| --- | --- |
|
|
51
71
|
| `src_home` | Base directory for all `src` paths. Relative paths are resolved from your home directory. |
|
|
52
|
-
| `target_home` | Base directory for all target paths. Relative paths are resolved from your home directory.
|
|
72
|
+
| `target_home` | Base directory for all target paths. Relative paths are resolved from your home directory. |
|
|
53
73
|
| `mtp_target` | Use in-place syncing for MTP targets. |
|
|
54
74
|
| `backup` | Backup directory for replaced or deleted files. Relative paths are resolved from `target_home`. `strftime` placeholders are supported. |
|
|
55
75
|
| `exclude` | Exclude patterns applied to every command. |
|
|
@@ -61,7 +81,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
61
81
|
| Key | Meaning |
|
|
62
82
|
| --- | --- |
|
|
63
83
|
| `src` | Relative source directory below `src_home`. |
|
|
64
|
-
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders are supported. |
|
|
84
|
+
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders (`%Y`, `%m`, and `%d` only!) are supported. |
|
|
65
85
|
| `delete_extra` | Delete files in the target that do not exist in the source. Defaults to `true`. |
|
|
66
86
|
| `exclude` | Extra exclude patterns for this command only. |
|
|
67
87
|
|
|
@@ -72,6 +92,8 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
72
92
|
- Exactly one of `--src-home -` and `--target-home -` may read from standard input in a single run.
|
|
73
93
|
- If `backup` is omitted, `yd` creates a timestamped backup directory automatically unless `--no-backup` is specified.
|
|
74
94
|
- 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).
|
|
95
|
+
- If `strftime` placeholders are included in the `target`, then the `target_home` is scanned for directories that may have been created by this rule in the past. All matches are included as additional references in the synchronization: if a file in `src` is included in one of those reference directories, then it will not be copied to `target`.
|
|
96
|
+
- If the `target` directory did not exist before and no files are transferred into it, then it will be removed again.
|
|
75
97
|
|
|
76
98
|
## Run a config
|
|
77
99
|
|
|
@@ -27,6 +27,8 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
from rich.text import Text
|
|
29
29
|
|
|
30
|
+
log = logging.getLogger(__name__)
|
|
31
|
+
|
|
30
32
|
env: yd.types.Environment
|
|
31
33
|
console: rich.console.Console
|
|
32
34
|
highlighter: StrEnumHighlighter
|
|
@@ -176,7 +178,6 @@ def _setup_io() -> None:
|
|
|
176
178
|
|
|
177
179
|
_encode_json = msgspec.json.Encoder().encode
|
|
178
180
|
|
|
179
|
-
@dataclasses.dataclass(slots=True)
|
|
180
181
|
class JsonLineFormatter(logging.Formatter):
|
|
181
182
|
def format(self, record: logging.LogRecord) -> str:
|
|
182
183
|
return _encode_json(
|
|
@@ -267,7 +268,9 @@ def run(
|
|
|
267
268
|
) -> int:
|
|
268
269
|
"""Run a saved configuration by name."""
|
|
269
270
|
import asyncio
|
|
270
|
-
import
|
|
271
|
+
import contextlib
|
|
272
|
+
import functools as ft
|
|
273
|
+
import shutil
|
|
271
274
|
from pathlib import Path
|
|
272
275
|
|
|
273
276
|
import rich.progress
|
|
@@ -291,7 +294,6 @@ def run(
|
|
|
291
294
|
keep_newer=keep_newer,
|
|
292
295
|
rename_speedup=rename_speedup,
|
|
293
296
|
no_backup=no_backup,
|
|
294
|
-
error_encoder=yd.io.encode_error,
|
|
295
297
|
)
|
|
296
298
|
)
|
|
297
299
|
except yd.types.CommandBuildError as err:
|
|
@@ -373,6 +375,13 @@ def run(
|
|
|
373
375
|
**counter, # type: ignore[ty:invalid-argument-type]
|
|
374
376
|
total=1,
|
|
375
377
|
)
|
|
378
|
+
|
|
379
|
+
target = Path(args[-1])
|
|
380
|
+
target_existed = target.is_dir()
|
|
381
|
+
progress_updater = ft.partial(
|
|
382
|
+
show_progress, counter=counter, taskid=taskid, target=target
|
|
383
|
+
)
|
|
384
|
+
|
|
376
385
|
try:
|
|
377
386
|
runner.run(
|
|
378
387
|
yd.execution.run(
|
|
@@ -380,12 +389,7 @@ def run(
|
|
|
380
389
|
*args,
|
|
381
390
|
parser_stdout=yd.io.decode_from_stdout,
|
|
382
391
|
parser_stderr=yd.io.decode_from_stderr,
|
|
383
|
-
consumer=
|
|
384
|
-
show_progress,
|
|
385
|
-
counter=counter,
|
|
386
|
-
taskid=taskid,
|
|
387
|
-
target=Path(args[-1]),
|
|
388
|
-
),
|
|
392
|
+
consumer=progress_updater,
|
|
389
393
|
)
|
|
390
394
|
)
|
|
391
395
|
except (KeyboardInterrupt, asyncio.CancelledError) as err:
|
|
@@ -398,6 +402,28 @@ def run(
|
|
|
398
402
|
except (yd.types.OutputParseError, yd.types.OutputConsumeError) as err:
|
|
399
403
|
console.print("Failed to handle rsync output.", style="error")
|
|
400
404
|
raise typer.Exit(2) from err
|
|
405
|
+
|
|
406
|
+
# If target was created but no files were copied, then remove it.
|
|
407
|
+
if (
|
|
408
|
+
not target_existed
|
|
409
|
+
and target.is_dir() # Ensure rglob can be used.
|
|
410
|
+
and not any(
|
|
411
|
+
path.is_file() or path.is_symlink() for path in target.rglob("*")
|
|
412
|
+
)
|
|
413
|
+
):
|
|
414
|
+
with contextlib.suppress(OSError):
|
|
415
|
+
shutil.rmtree(target)
|
|
416
|
+
log.info("Removed empty but newly created target `%s`.", target)
|
|
417
|
+
runner.run(
|
|
418
|
+
progress_updater(
|
|
419
|
+
yd.types.Transaction(
|
|
420
|
+
filename=target.as_posix(),
|
|
421
|
+
transfer_bytes=0,
|
|
422
|
+
info="*deleting",
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
401
427
|
progress.update(taskid, completed=1)
|
|
402
428
|
|
|
403
429
|
return 0
|
|
@@ -439,17 +465,18 @@ def edit(
|
|
|
439
465
|
] = False,
|
|
440
466
|
) -> int:
|
|
441
467
|
"""Open an existing configuration or create a new one in the editor."""
|
|
442
|
-
import contextlib
|
|
443
468
|
import subprocess
|
|
444
469
|
|
|
445
470
|
if (editor_bin := env.editor_bin) is None:
|
|
446
471
|
raise typer.BadParameter("environment variable `EDITOR` is not set")
|
|
447
472
|
|
|
448
|
-
|
|
449
|
-
with contextlib.suppress(subprocess.CalledProcessError):
|
|
473
|
+
try:
|
|
450
474
|
vers = subprocess.run(
|
|
451
475
|
[editor_bin, "--version"], encoding="utf-8", capture_output=True, check=True
|
|
452
476
|
).stdout
|
|
477
|
+
except subprocess.CalledProcessError:
|
|
478
|
+
is_nvim = False
|
|
479
|
+
else:
|
|
453
480
|
is_nvim = re.match(r"\s*NVIM", vers) is not None
|
|
454
481
|
|
|
455
482
|
config_path = env.config_dir / f"{cfg_name}.toml"
|
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import datetime as dt
|
|
8
9
|
import itertools as it
|
|
9
10
|
import logging
|
|
11
|
+
import re
|
|
10
12
|
import sys
|
|
11
13
|
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
12
15
|
from pathlib import Path
|
|
13
16
|
|
|
14
17
|
from yd import types
|
|
15
18
|
|
|
16
19
|
TYPE_CHECKING = False
|
|
17
20
|
if TYPE_CHECKING:
|
|
18
|
-
from collections.abc import
|
|
21
|
+
from collections.abc import Iterator, Sequence
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
log = logging.getLogger(__name__)
|
|
@@ -47,7 +50,57 @@ def _format_path(path: Path, /) -> str:
|
|
|
47
50
|
return f"{path.as_posix()}/"
|
|
48
51
|
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class _DatePathMatch:
|
|
55
|
+
path: Path
|
|
56
|
+
relative_path: str
|
|
57
|
+
datetime: dt.datetime | None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_PLACEHOLDER = re.compile("%Y|%m|%d")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _match_dirs(
|
|
64
|
+
*, rel_path_pattern: str, home: Path, env: types.Environment
|
|
65
|
+
) -> Iterator[_DatePathMatch]:
|
|
66
|
+
"""Find subdirectories matching a target pattern."""
|
|
67
|
+
if "%" not in rel_path_pattern:
|
|
68
|
+
log.debug("No date placeholder found in `%s`.", rel_path_pattern)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Crude filter. More careful checking below.
|
|
72
|
+
glob_pattern = re.sub(
|
|
73
|
+
"(?P<f>[^/])$", r"\g<f>/", _PLACEHOLDER.sub("*", rel_path_pattern)
|
|
74
|
+
)
|
|
75
|
+
if "%" in glob_pattern:
|
|
76
|
+
raise ValueError("pattern contains unsupported date placeholders")
|
|
77
|
+
rel_path_pattern = re.sub("/$", "", rel_path_pattern)
|
|
78
|
+
|
|
79
|
+
log.debug("Searching for sub-directories of the form `%s`.", home / glob_pattern)
|
|
80
|
+
for path in home.glob(glob_pattern):
|
|
81
|
+
rel_path = path.relative_to(home).as_posix()
|
|
82
|
+
log.debug("Scanning for date in `%s`.", rel_path)
|
|
83
|
+
try:
|
|
84
|
+
parsed = dt.datetime.strptime(rel_path, rel_path_pattern)
|
|
85
|
+
except ValueError:
|
|
86
|
+
log.debug("Found no date in `%s`.", rel_path)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
log.info("Found date `%s` in `%s`.", parsed.strftime("%Y-%m-%d"), rel_path)
|
|
90
|
+
if (rendered := parsed.strftime(rel_path_pattern)) != rel_path:
|
|
91
|
+
log.debug("Path pattern rendered to `%s` != `%s`.", rendered, rel_path)
|
|
92
|
+
continue
|
|
93
|
+
if parsed.date() >= env.current_time.date():
|
|
94
|
+
log.debug(
|
|
95
|
+
"Date `%s` not before current date `%s`.",
|
|
96
|
+
parsed.strftime("%Y-%m-%d"),
|
|
97
|
+
env.current_time.strftime("%Y-%m-%d"),
|
|
98
|
+
)
|
|
99
|
+
continue
|
|
100
|
+
yield _DatePathMatch(path=path, relative_path=rel_path, datetime=parsed)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _create_echo_command(
|
|
51
104
|
enc_msg: str, /, environment: types.Environment
|
|
52
105
|
) -> tuple[str, tuple[str, ...]]:
|
|
53
106
|
if environment.echo_bin is not None:
|
|
@@ -67,8 +120,7 @@ def create( # noqa: PLR0915
|
|
|
67
120
|
rename_speedup: bool,
|
|
68
121
|
no_backup: bool,
|
|
69
122
|
out_format: str = '{"filename": "%n", "transfer_bytes": %b, "info": "%i"}',
|
|
70
|
-
|
|
71
|
-
) -> Iterable[tuple[str, Sequence[str]]]:
|
|
123
|
+
) -> Iterator[tuple[str, Sequence[str]]]:
|
|
72
124
|
"""Build CLI commands.
|
|
73
125
|
|
|
74
126
|
If the source directory is empty, then an `echo` command will be created. If
|
|
@@ -91,7 +143,6 @@ def create( # noqa: PLR0915
|
|
|
91
143
|
if not (target_home := cg.target_home if target_home is None else target_home):
|
|
92
144
|
raise types.CommandBuildError("missing target home specification")
|
|
93
145
|
|
|
94
|
-
target_home = env.current_time.strftime(target_home)
|
|
95
146
|
src_hpath, target_hpath = (
|
|
96
147
|
_complete_path(Path(src_home), home=env.home_dir),
|
|
97
148
|
_complete_path(Path(target_home), home=env.home_dir),
|
|
@@ -160,9 +211,9 @@ def create( # noqa: PLR0915
|
|
|
160
211
|
msg = f"Source directory `{src}` is empty. No synchronization will be done."
|
|
161
212
|
if cmd.delete_extra:
|
|
162
213
|
msg += " Delete the folder contents manually to synchronize."
|
|
163
|
-
|
|
164
214
|
log.warning(msg)
|
|
165
|
-
|
|
215
|
+
|
|
216
|
+
yield _create_echo_command(msg, environment=env)
|
|
166
217
|
continue
|
|
167
218
|
|
|
168
219
|
target_spec = (
|
|
@@ -211,6 +262,29 @@ def create( # noqa: PLR0915
|
|
|
211
262
|
# A relative path will be considered relative to the destination directory.
|
|
212
263
|
args.extend(("--backup", "--backup-dir", _format_path(target_backup_path)))
|
|
213
264
|
|
|
265
|
+
if cmd.target is not None and target_spec != cmd.target:
|
|
266
|
+
try:
|
|
267
|
+
cmp_dests = sorted(
|
|
268
|
+
_match_dirs(
|
|
269
|
+
rel_path_pattern=cmd.target, home=target_hpath, env=env
|
|
270
|
+
),
|
|
271
|
+
key=lambda d: d.datetime,
|
|
272
|
+
reverse=True,
|
|
273
|
+
)
|
|
274
|
+
except ValueError as err:
|
|
275
|
+
raise types.CommandBuildError(
|
|
276
|
+
"unsupported date specifiers in target"
|
|
277
|
+
) from err
|
|
278
|
+
# rsync restricts the number of `--compare-dest` specs to 20.
|
|
279
|
+
cmp_dests = [d.path.as_posix() for i, d in enumerate(cmp_dests) if i < 20]
|
|
280
|
+
log.info(
|
|
281
|
+
"Found reference targets `%s`.",
|
|
282
|
+
"`, `".join(d for d in cmp_dests),
|
|
283
|
+
)
|
|
284
|
+
args.extend(
|
|
285
|
+
it.chain.from_iterable(("--compare-dest", d) for d in cmp_dests)
|
|
286
|
+
)
|
|
287
|
+
|
|
214
288
|
args.extend(
|
|
215
289
|
f"--exclude={pattern}" for pattern in it.chain(cmd.exclude, cg.exclude)
|
|
216
290
|
)
|
|
@@ -35,7 +35,6 @@ __all__ = [
|
|
|
35
35
|
"list_available_configs",
|
|
36
36
|
"decode_from_stdout",
|
|
37
37
|
"decode_from_stderr",
|
|
38
|
-
"encode_error",
|
|
39
38
|
]
|
|
40
39
|
|
|
41
40
|
|
|
@@ -195,7 +194,3 @@ def decode_from_stderr(text: str, /) -> types.Transaction | str | None:
|
|
|
195
194
|
if not (stripped_text := text.strip()):
|
|
196
195
|
return None
|
|
197
196
|
return stripped_text
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def encode_error(text: str, /) -> str:
|
|
201
|
-
return text
|
|
@@ -70,10 +70,6 @@ class CommandGroup(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
|
|
70
70
|
src_home/target_home
|
|
71
71
|
Absolute path used to turn relative src/target path in individual commands into
|
|
72
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
73
|
mtp_target
|
|
78
74
|
Set True if communication with the target works with MTP. Due to
|
|
79
75
|
technical limitations of that protocol, synchronization needs to be done
|
|
@@ -84,7 +80,7 @@ class CommandGroup(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
|
|
84
80
|
Path to backup directory. Added to each command and if relative, then taken
|
|
85
81
|
as relative to target home. If None, a default relative path is used.
|
|
86
82
|
|
|
87
|
-
The `
|
|
83
|
+
The `backup` is passed to `datetime.strftime` via the `format` parameter, that
|
|
88
84
|
is, placeholders for date/time components like `%Y`, `%m`, etc., are replaced
|
|
89
85
|
with the actual values at runtime.
|
|
90
86
|
exclude
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|