backup-docker-to-local 1.3.0__py3-none-any.whl → 1.5.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backup-docker-to-local
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Backup Docker volumes to local with rsync and optional DB dumps.
5
5
  Author: Kevin Veen-Birkenbach
6
6
  License: AGPL-3.0-or-later
@@ -1,10 +1,10 @@
1
- backup_docker_to_local-1.3.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
1
+ backup_docker_to_local-1.5.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
2
2
  baudolo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  baudolo/backup/__init__.py,sha256=5BfF8JBXB2j6sAptcmswtbjlajNWxOho6_CjwIamO7k,30
4
4
  baudolo/backup/__main__.py,sha256=118gZ0wij9_PAtR-jlG7LizrhrxwhHlAcAPW1eFeJtU,140
5
- baudolo/backup/app.py,sha256=Msx4DLi9URo9KWiT73riRD9csAQKvqZC6R2uRuEFqhk,6551
5
+ baudolo/backup/app.py,sha256=eUJajqvXudc5B3cGDofx9NvoBSE-Nc0hx1pAb7aUN_I,7872
6
6
  baudolo/backup/cli.py,sha256=CIckfVsU03ajZTI7zj3TB-AuvaOeJMbTB6rNCOKymXE,2473
7
- baudolo/backup/compose.py,sha256=dJiZnHlBZdeXTVqLR5zIbAtwR97YWxbULVn6VL16Hqg,1136
7
+ baudolo/backup/compose.py,sha256=b-qKimuCcNhvHvDNDyNhmtDPcBpN2pmlJNBgy2fiPg0,3637
8
8
  baudolo/backup/db.py,sha256=_7_Vy8nMql1vaaL1g0kbLtRHhGx65T560DsckZFRJ-k,4462
9
9
  baudolo/backup/docker.py,sha256=6Sj9fpf1bm-CoqoeerQaq059VyDnF1Pj2ketzIt1Nkk,1364
10
10
  baudolo/backup/shell.py,sha256=guMHWcRb0Qlrz79gqFqmJLXVQK8cJEvNkhkMe5Tpgwc,738
@@ -18,8 +18,8 @@ baudolo/restore/db/__init__.py,sha256=C4K_YAB0F9p8QhZRZIRyV2naMb54Qf_1O2cDxwwVQA
18
18
  baudolo/restore/db/mariadb.py,sha256=_JEQFuF4GhTl8lKmgk2gsHYcXHcpCKrf32tigMSeTKI,2782
19
19
  baudolo/restore/db/postgres.py,sha256=_NIlcxdpcQIUZ8SNBWfWIaH1oGIkmZy9emFMgjGznhk,1446
20
20
  baudolo/seed/__main__.py,sha256=BnN0Ckdjr30r6yxORq-ctrdmkT0yfeRIuXnyhKSe8Us,3430
21
- backup_docker_to_local-1.3.0.dist-info/METADATA,sha256=-LXJPQPpZmk8IMnlfJcNzEWzMOjfr7S0yXkkPC82n8Q,7200
22
- backup_docker_to_local-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- backup_docker_to_local-1.3.0.dist-info/entry_points.txt,sha256=92f5jPSjW__9-u5gzwmWkdiHKt4ysEFCVmwMtorQCv4,147
24
- backup_docker_to_local-1.3.0.dist-info/top_level.txt,sha256=y_5iNpF7EdLzqWWXIDfcTJpqijyy2hvrYgNiJXrN4r4,8
25
- backup_docker_to_local-1.3.0.dist-info/RECORD,,
21
+ backup_docker_to_local-1.5.0.dist-info/METADATA,sha256=yEIbwvxjC2sEdIkBCJjFVw4ESBRtYK7cbRl7DknMxWo,7200
22
+ backup_docker_to_local-1.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
23
+ backup_docker_to_local-1.5.0.dist-info/entry_points.txt,sha256=92f5jPSjW__9-u5gzwmWkdiHKt4ysEFCVmwMtorQCv4,147
24
+ backup_docker_to_local-1.5.0.dist-info/top_level.txt,sha256=y_5iNpF7EdLzqWWXIDfcTJpqijyy2hvrYgNiJXrN4r4,8
25
+ backup_docker_to_local-1.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
baudolo/backup/app.py CHANGED
@@ -2,10 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import pathlib
5
+ import sys
5
6
  from datetime import datetime
6
7
 
7
8
  import pandas
8
9
  from dirval import create_stamp_file
10
+ from pandas.errors import EmptyDataError
9
11
 
10
12
  from .cli import parse_args
11
13
  from .compose import handle_docker_compose_services
@@ -96,6 +98,42 @@ def backup_mariadb_or_postgres(
96
98
  return False, False
97
99
 
98
100
 
101
+ def _empty_databases_df() -> "pandas.DataFrame":
102
+ """
103
+ Create an empty DataFrame with the expected schema for databases.csv.
104
+
105
+ This allows the backup to continue without DB dumps when the CSV is missing
106
+ or empty (pandas EmptyDataError).
107
+ """
108
+ return pandas.DataFrame(columns=["instance", "database", "username", "password"])
109
+
110
+
111
+ def _load_databases_df(csv_path: str) -> "pandas.DataFrame":
112
+ """
113
+ Load databases.csv robustly.
114
+
115
+ - Missing file -> warn, continue with empty df
116
+ - Empty file -> warn, continue with empty df
117
+ - Valid CSV -> return dataframe
118
+ """
119
+ try:
120
+ return pandas.read_csv(csv_path, sep=";", keep_default_na=False, dtype=str)
121
+ except FileNotFoundError:
122
+ print(
123
+ f"WARNING: databases.csv not found: {csv_path}. Continuing without database dumps.",
124
+ file=sys.stderr,
125
+ flush=True,
126
+ )
127
+ return _empty_databases_df()
128
+ except EmptyDataError:
129
+ print(
130
+ f"WARNING: databases.csv exists but is empty: {csv_path}. Continuing without database dumps.",
131
+ file=sys.stderr,
132
+ flush=True,
133
+ )
134
+ return _empty_databases_df()
135
+
136
+
99
137
  def _backup_dumps_for_volume(
100
138
  *,
101
139
  containers: list[str],
@@ -136,9 +174,10 @@ def main() -> int:
136
174
  # IMPORTANT:
137
175
  # - keep_default_na=False prevents empty fields from turning into NaN
138
176
  # - dtype=str keeps all columns stable for comparisons/validation
139
- databases_df = pandas.read_csv(
140
- args.databases_csv, sep=";", keep_default_na=False, dtype=str
141
- )
177
+ #
178
+ # Robust behavior:
179
+ # - if the file is missing or empty, we continue without DB dumps.
180
+ databases_df = _load_databases_df(args.databases_csv)
142
181
 
143
182
  print("💾 Start volume backups...", flush=True)
144
183
 
@@ -168,7 +207,8 @@ def main() -> int:
168
207
  if found_db:
169
208
  if not dumped_any:
170
209
  print(
171
- f"WARNING: dump-only-sql requested but no DB dump was produced for DB volume '{volume_name}'. Falling back to file backup.",
210
+ f"WARNING: dump-only-sql requested but no DB dump was produced for DB volume '{volume_name}'. "
211
+ "Falling back to file backup.",
172
212
  flush=True,
173
213
  )
174
214
  # fall through to file backup below
baudolo/backup/compose.py CHANGED
@@ -1,13 +1,104 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import shutil
4
5
  import subprocess
6
+ from pathlib import Path
7
+ from typing import List, Optional
8
+
9
+
10
+ def _detect_env_file(project_dir: Path) -> Optional[Path]:
11
+ """
12
+ Detect Compose env file in a directory.
13
+ Preference (same as Infinito.Nexus wrapper):
14
+ 1) <dir>/.env (file)
15
+ 2) <dir>/.env/env (file) (legacy layout)
16
+ """
17
+ c1 = project_dir / ".env"
18
+ if c1.is_file():
19
+ return c1
20
+
21
+ c2 = project_dir / ".env" / "env"
22
+ if c2.is_file():
23
+ return c2
24
+
25
+ return None
26
+
27
+
28
+ def _detect_compose_files(project_dir: Path) -> List[Path]:
29
+ """
30
+ Detect Compose file stack in a directory (same as Infinito.Nexus wrapper).
31
+ Always requires docker-compose.yml.
32
+ Optionals:
33
+ - docker-compose.override.yml
34
+ - docker-compose.ca.override.yml
35
+ """
36
+ base = project_dir / "docker-compose.yml"
37
+ if not base.is_file():
38
+ raise FileNotFoundError(f"Missing docker-compose.yml in: {project_dir}")
39
+
40
+ files = [base]
41
+
42
+ override = project_dir / "docker-compose.override.yml"
43
+ if override.is_file():
44
+ files.append(override)
45
+
46
+ ca_override = project_dir / "docker-compose.ca.override.yml"
47
+ if ca_override.is_file():
48
+ files.append(ca_override)
49
+
50
+ return files
51
+
52
+
53
+ def _compose_wrapper_path() -> Optional[str]:
54
+ """
55
+ Prefer the Infinito.Nexus compose wrapper if present.
56
+ Equivalent to: `which compose`
57
+ """
58
+ return shutil.which("compose")
59
+
60
+
61
+ def _build_compose_cmd(project_dir: str, passthrough: List[str]) -> List[str]:
62
+ """
63
+ Build the compose command for this project directory.
64
+
65
+ Behavior:
66
+ - If `compose` wrapper exists: use it with --chdir (so it resolves -f/--env-file itself)
67
+ - Else: use `docker compose` and replicate wrapper's file/env detection.
68
+ """
69
+ pdir = Path(project_dir).resolve()
70
+
71
+ wrapper = _compose_wrapper_path()
72
+ if wrapper:
73
+ # Wrapper defaults project name to basename of --chdir.
74
+ # "--" ensures wrapper stops parsing its own args.
75
+ return [wrapper, "--chdir", str(pdir), "--", *passthrough]
76
+
77
+ # Fallback: pure docker compose, but mirror wrapper behavior.
78
+ files = _detect_compose_files(pdir)
79
+ env_file = _detect_env_file(pdir)
80
+
81
+ cmd: List[str] = ["docker", "compose"]
82
+ for f in files:
83
+ cmd += ["-f", str(f)]
84
+ if env_file:
85
+ cmd += ["--env-file", str(env_file)]
86
+
87
+ cmd += passthrough
88
+ return cmd
5
89
 
6
90
 
7
91
  def hard_restart_docker_services(dir_path: str) -> None:
8
- print(f"Hard restart docker-compose services in: {dir_path}", flush=True)
9
- subprocess.run(["docker-compose", "down"], cwd=dir_path, check=True)
10
- subprocess.run(["docker-compose", "up", "-d"], cwd=dir_path, check=True)
92
+ print(f"Hard restart compose services in: {dir_path}", flush=True)
93
+
94
+ down_cmd = _build_compose_cmd(dir_path, ["down"])
95
+ up_cmd = _build_compose_cmd(dir_path, ["up", "-d"])
96
+
97
+ print(">>> " + " ".join(down_cmd), flush=True)
98
+ subprocess.run(down_cmd, check=True)
99
+
100
+ print(">>> " + " ".join(up_cmd), flush=True)
101
+ subprocess.run(up_cmd, check=True)
11
102
 
12
103
 
13
104
  def handle_docker_compose_services(