declarative-data-backup 0.3.0__tar.gz → 0.3.1__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.
Files changed (27) hide show
  1. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/PKG-INFO +7 -2
  2. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/README.md +6 -1
  3. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/pyproject.toml +1 -1
  4. declarative_data_backup-0.3.1/src/declarative_data_backup/borg/borg_cli.py +164 -0
  5. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/PKG-INFO +7 -2
  6. declarative_data_backup-0.3.0/src/declarative_data_backup/borg/borg_cli.py +0 -69
  7. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/setup.cfg +0 -0
  8. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/__init__.py +0 -0
  9. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/__main__.py +0 -0
  10. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/backup.py +0 -0
  11. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/borg/__init__.py +0 -0
  12. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/borg/archive_naming.py +0 -0
  13. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/borg/borg.py +0 -0
  14. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/__init__.py +0 -0
  15. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/__init__.py +0 -0
  16. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/models.py +0 -0
  17. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/utils.py +0 -0
  18. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/yaml_parser.py +0 -0
  19. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/yaml_raw_loader.py +0 -0
  20. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/logging.py +0 -0
  21. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/environment.py +0 -0
  22. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/errors.py +0 -0
  23. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/SOURCES.txt +0 -0
  24. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/dependency_links.txt +0 -0
  25. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/requires.txt +0 -0
  26. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/top_level.txt +0 -0
  27. {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/tests/test_backup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: declarative-data-backup
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Declarative Borg-based data backups
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -149,7 +149,7 @@ Optional variables:
149
149
  | --- | --- |
150
150
  | `BORG_REMOTE_PATH` | Remote Borg executable path, passed as Borg's `--remote-path`. Leave unset to use Borg's default. |
151
151
  | `BORG_EXIT_CODES` | Borg exit-code mode: `legacy` or `modern`. |
152
- | `BACKUP_PROGRESS` | Set to `true` to enable Borg's live progress output during archive creation. Defaults to `false` to limit log volume. |
152
+ | `BACKUP_PROGRESS` | Set to `true` to log the latest Borg progress every 30 seconds during archive creation. Defaults to `false`. |
153
153
  | `BACKUP_VERBOSE` | Set to `true` to show Borg's per-file create and per-archive prune output. Defaults to `false` for concise logs. |
154
154
 
155
155
  Provide credentials through a secret manager or protected files; do not put
@@ -167,6 +167,11 @@ the run fails and pruning is skipped. Logs include group and source start/end
167
167
  messages, while Borg's per-file and per-archive output is disabled by default.
168
168
  Set `BACKUP_VERBOSE=true` for detailed Borg output or `BACKUP_PROGRESS=true`
169
169
  for live progress during archive creation; either can increase log volume.
170
+ Progress is logged as plain status lines, not a terminal progress bar. Each
171
+ line includes the group, source, elapsed time, and latest reported file/byte
172
+ counters or Borg phase. Unchanged counters do not prove forward progress;
173
+ before Borg reports a phase, the status only indicates that the process is
174
+ running. Warnings and statistics are not throttled.
170
175
 
171
176
  ## Configuration reference
172
177
 
@@ -141,7 +141,7 @@ Optional variables:
141
141
  | --- | --- |
142
142
  | `BORG_REMOTE_PATH` | Remote Borg executable path, passed as Borg's `--remote-path`. Leave unset to use Borg's default. |
143
143
  | `BORG_EXIT_CODES` | Borg exit-code mode: `legacy` or `modern`. |
144
- | `BACKUP_PROGRESS` | Set to `true` to enable Borg's live progress output during archive creation. Defaults to `false` to limit log volume. |
144
+ | `BACKUP_PROGRESS` | Set to `true` to log the latest Borg progress every 30 seconds during archive creation. Defaults to `false`. |
145
145
  | `BACKUP_VERBOSE` | Set to `true` to show Borg's per-file create and per-archive prune output. Defaults to `false` for concise logs. |
146
146
 
147
147
  Provide credentials through a secret manager or protected files; do not put
@@ -159,6 +159,11 @@ the run fails and pruning is skipped. Logs include group and source start/end
159
159
  messages, while Borg's per-file and per-archive output is disabled by default.
160
160
  Set `BACKUP_VERBOSE=true` for detailed Borg output or `BACKUP_PROGRESS=true`
161
161
  for live progress during archive creation; either can increase log volume.
162
+ Progress is logged as plain status lines, not a terminal progress bar. Each
163
+ line includes the group, source, elapsed time, and latest reported file/byte
164
+ counters or Borg phase. Unchanged counters do not prove forward progress;
165
+ before Borg reports a phase, the status only indicates that the process is
166
+ running. Warnings and statistics are not throttled.
162
167
 
163
168
  ## Configuration reference
164
169
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "declarative-data-backup"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Declarative Borg-based data backups"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,164 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import selectors
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ import time
9
+
10
+ from ..environment import Environment
11
+ from ..errors import BorgWarningError, fail
12
+
13
+ logger = logging.getLogger(__name__)
14
+ PROGRESS_INTERVAL_SECONDS = 30
15
+
16
+
17
+ def run_with_progress(
18
+ command: list[str], environment: dict[str, str], context: str
19
+ ) -> int:
20
+ started_at = time.monotonic()
21
+ next_report = started_at + PROGRESS_INTERVAL_SECONDS
22
+ latest_progress = "Waiting for Borg progress (process running)."
23
+
24
+ def handle_line(line: bytes) -> None:
25
+ nonlocal latest_progress
26
+ text = line.decode("utf-8", errors="replace")
27
+ try:
28
+ event = json.loads(text)
29
+ except json.JSONDecodeError:
30
+ print(text, file=sys.stderr, flush=True)
31
+ return
32
+ if not isinstance(event, dict):
33
+ print(text, file=sys.stderr, flush=True)
34
+ return
35
+ kind = event.get("type")
36
+ if kind in ("archive_progress", "progress_message", "progress_percent"):
37
+ if event.get("finished"):
38
+ return
39
+ if kind == "archive_progress":
40
+ latest_progress = (
41
+ f"files={event.get('nfiles')} "
42
+ f"original_bytes={event.get('original_size')} "
43
+ f"compressed_bytes={event.get('compressed_size')} "
44
+ f"deduplicated_bytes={event.get('deduplicated_size')}"
45
+ )
46
+ else:
47
+ latest_progress = str(event.get("message", text))
48
+ return
49
+ if kind == "log_message":
50
+ print(event.get("message", text), file=sys.stderr, flush=True)
51
+ else:
52
+ print(text, file=sys.stderr, flush=True)
53
+
54
+ with subprocess.Popen(
55
+ command,
56
+ env=environment,
57
+ stderr=subprocess.PIPE,
58
+ ) as process:
59
+ assert process.stderr is not None
60
+ try:
61
+ with selectors.DefaultSelector() as selector:
62
+ selector.register(process.stderr, selectors.EVENT_READ)
63
+ pending = b""
64
+ while selector.get_map():
65
+ for key, _ in selector.select(
66
+ max(0, next_report - time.monotonic())
67
+ ):
68
+ chunk = os.read(key.fd, 65536)
69
+ if not chunk:
70
+ selector.unregister(key.fileobj)
71
+ if pending:
72
+ handle_line(pending)
73
+ break
74
+ pending += chunk
75
+ while b"\n" in pending:
76
+ line, pending = pending.split(b"\n", 1)
77
+ handle_line(line)
78
+ now = time.monotonic()
79
+ if now >= next_report and selector.get_map():
80
+ logger.info(
81
+ "Borg progress for %s (elapsed_seconds=%.0f; "
82
+ "latest reported: %s).",
83
+ context,
84
+ now - started_at,
85
+ latest_progress,
86
+ )
87
+ next_report = now + PROGRESS_INTERVAL_SECONDS
88
+ return process.wait()
89
+ except BaseException:
90
+ process.terminate()
91
+ try:
92
+ process.wait(timeout=10)
93
+ except subprocess.TimeoutExpired:
94
+ process.kill()
95
+ process.wait()
96
+ raise
97
+
98
+
99
+ def ensure_borg_binary() -> None:
100
+ if shutil.which("borg") is None:
101
+ fail("Required executable 'borg' was not found on PATH.")
102
+
103
+
104
+ def borg_environment(settings: Environment) -> dict[str, str]:
105
+ environment = os.environ.copy()
106
+ environment.update(
107
+ {
108
+ "BORG_REPO": settings.borg_repo,
109
+ "BORG_BASE_DIR": str(settings.borg_base_dir),
110
+ "BORG_KEY_FILE": str(settings.key_file),
111
+ "BORG_RSH": settings.borg_rsh,
112
+ "BORG_PASSCOMMAND": settings.borg_passcommand,
113
+ }
114
+ )
115
+ if settings.borg_exit_codes is not None:
116
+ environment["BORG_EXIT_CODES"] = settings.borg_exit_codes
117
+ return environment
118
+
119
+
120
+ def run_borg(
121
+ args: list[str],
122
+ settings: Environment,
123
+ operation: str,
124
+ context: str,
125
+ ) -> None:
126
+ logger.info("Running Borg %s for %s.", operation, context)
127
+ command = ["borg"]
128
+ remote_path = settings.borg_remote_path
129
+ if remote_path is not None:
130
+ if not remote_path.strip() or "\0" in remote_path:
131
+ fail("BORG_REMOTE_PATH must be a nonempty Borg executable path.")
132
+ command.extend(("--remote-path", remote_path))
133
+ command.extend(args)
134
+
135
+ try:
136
+ if settings.backup_progress and operation == "create":
137
+ command.insert(1, "--log-json")
138
+ returncode = run_with_progress(
139
+ command,
140
+ borg_environment(settings),
141
+ context,
142
+ )
143
+ else:
144
+ returncode = subprocess.run(
145
+ command,
146
+ env=borg_environment(settings),
147
+ check=False,
148
+ ).returncode
149
+ except FileNotFoundError:
150
+ fail("Required executable 'borg' could not be started.")
151
+ except OSError as exc:
152
+ fail(
153
+ f"Unable to start Borg {operation} for {context}: {exc.strerror or 'I/O error'}."
154
+ )
155
+
156
+ if returncode == 0:
157
+ return
158
+
159
+ if returncode == 1:
160
+ message = f"Borg {operation} reported warnings for {context} (exit code 1)."
161
+ logger.warning("%s", message)
162
+ raise BorgWarningError(message)
163
+
164
+ fail(f"Borg {operation} failed for {context} (exit code {returncode}).")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: declarative-data-backup
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Declarative Borg-based data backups
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -149,7 +149,7 @@ Optional variables:
149
149
  | --- | --- |
150
150
  | `BORG_REMOTE_PATH` | Remote Borg executable path, passed as Borg's `--remote-path`. Leave unset to use Borg's default. |
151
151
  | `BORG_EXIT_CODES` | Borg exit-code mode: `legacy` or `modern`. |
152
- | `BACKUP_PROGRESS` | Set to `true` to enable Borg's live progress output during archive creation. Defaults to `false` to limit log volume. |
152
+ | `BACKUP_PROGRESS` | Set to `true` to log the latest Borg progress every 30 seconds during archive creation. Defaults to `false`. |
153
153
  | `BACKUP_VERBOSE` | Set to `true` to show Borg's per-file create and per-archive prune output. Defaults to `false` for concise logs. |
154
154
 
155
155
  Provide credentials through a secret manager or protected files; do not put
@@ -167,6 +167,11 @@ the run fails and pruning is skipped. Logs include group and source start/end
167
167
  messages, while Borg's per-file and per-archive output is disabled by default.
168
168
  Set `BACKUP_VERBOSE=true` for detailed Borg output or `BACKUP_PROGRESS=true`
169
169
  for live progress during archive creation; either can increase log volume.
170
+ Progress is logged as plain status lines, not a terminal progress bar. Each
171
+ line includes the group, source, elapsed time, and latest reported file/byte
172
+ counters or Borg phase. Unchanged counters do not prove forward progress;
173
+ before Borg reports a phase, the status only indicates that the process is
174
+ running. Warnings and statistics are not throttled.
170
175
 
171
176
  ## Configuration reference
172
177
 
@@ -1,69 +0,0 @@
1
- import logging
2
- import os
3
- import shutil
4
- import subprocess
5
-
6
- from ..environment import Environment
7
- from ..errors import BorgWarningError, fail
8
-
9
- logger = logging.getLogger(__name__)
10
-
11
-
12
- def ensure_borg_binary() -> None:
13
- if shutil.which("borg") is None:
14
- fail("Required executable 'borg' was not found on PATH.")
15
-
16
-
17
- def borg_environment(settings: Environment) -> dict[str, str]:
18
- environment = os.environ.copy()
19
- environment.update(
20
- {
21
- "BORG_REPO": settings.borg_repo,
22
- "BORG_BASE_DIR": str(settings.borg_base_dir),
23
- "BORG_KEY_FILE": str(settings.key_file),
24
- "BORG_RSH": settings.borg_rsh,
25
- "BORG_PASSCOMMAND": settings.borg_passcommand,
26
- }
27
- )
28
- if settings.borg_exit_codes is not None:
29
- environment["BORG_EXIT_CODES"] = settings.borg_exit_codes
30
- return environment
31
-
32
-
33
- def run_borg(
34
- args: list[str],
35
- settings: Environment,
36
- operation: str,
37
- context: str,
38
- ) -> None:
39
- logger.info("Running Borg %s for %s.", operation, context)
40
- command = ["borg"]
41
- remote_path = settings.borg_remote_path
42
- if remote_path is not None:
43
- if not remote_path.strip() or "\0" in remote_path:
44
- fail("BORG_REMOTE_PATH must be a nonempty Borg executable path.")
45
- command.extend(("--remote-path", remote_path))
46
- command.extend(args)
47
-
48
- try:
49
- result = subprocess.run(
50
- command,
51
- env=borg_environment(settings),
52
- check=False,
53
- )
54
- except FileNotFoundError:
55
- fail("Required executable 'borg' could not be started.")
56
- except OSError as exc:
57
- fail(
58
- f"Unable to start Borg {operation} for {context}: {exc.strerror or 'I/O error'}."
59
- )
60
-
61
- if result.returncode == 0:
62
- return
63
-
64
- if result.returncode == 1:
65
- message = f"Borg {operation} reported warnings for {context} (exit code 1)."
66
- logger.warning("%s", message)
67
- raise BorgWarningError(message)
68
-
69
- fail(f"Borg {operation} failed for {context} (exit code {result.returncode}).")