Skip to content

Commit 2bed725

Browse files
authored
v5.2.0 (#665)
1 parent 8bafbd4 commit 2bed725

11 files changed

Lines changed: 78 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Don't forget to remove deprecated code on each major release!
1414

1515
## [Unreleased]
1616

17+
- Nothing (yet)!
18+
19+
## [5.2.0] - 2026-02-10
20+
1721
### Added
1822

1923
- Added support for custom metadata writing and validation during operations via `DBBACKUP_BACKUP_METADATA_SETTER` and `DBBACKUP_RESTORE_METADATA_VALIDATOR` settings.
@@ -348,7 +352,8 @@ Don't forget to remove deprecated code on each major release!
348352

349353
- Miscellaneous maintenance and minor bug fixes.
350354

351-
[Unreleased]: https://github.com/Archmonger/django-dbbackup/compare/5.1.2...HEAD
355+
[Unreleased]: https://github.com/Archmonger/django-dbbackup/compare/5.2.0...HEAD
356+
[5.2.0]: https://github.com/Archmonger/django-dbbackup/compare/5.1.2...5.2.0
352357
[5.1.2]: https://github.com/Archmonger/django-dbbackup/compare/5.1.1...5.1.2
353358
[5.1.1]: https://github.com/Archmonger/django-dbbackup/compare/5.1.0...5.1.1
354359
[5.1.0]: https://github.com/Archmonger/django-dbbackup/compare/5.0.1...5.1.0

dbbackup/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Management commands to help backup and restore a project database and media"""
22

3-
__version__ = "5.1.2"
3+
__version__ = "5.2.0"

dbbackup/checks.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,45 @@
11
import re
22
from datetime import datetime
33

4-
from django.core.checks import Tags, Warning, register
4+
from django.core.checks import Tags, register
5+
from django.core.checks import Warning as DjangoWarning
56

67
from dbbackup import settings
78

8-
W001 = Warning(
9+
W001 = DjangoWarning(
910
"Invalid HOSTNAME parameter",
1011
hint="Set a non empty string to this settings.DBBACKUP_HOSTNAME",
1112
id="dbbackup.W001",
1213
)
13-
W002 = Warning(
14+
W002 = DjangoWarning(
1415
"Invalid STORAGE parameter",
1516
hint="Set a valid path to a storage in settings.DBBACKUP_STORAGE",
1617
id="dbbackup.W002",
1718
)
18-
W003 = Warning(
19+
W003 = DjangoWarning(
1920
"Invalid FILENAME_TEMPLATE parameter",
2021
hint="Include {datetime} to settings.DBBACKUP_FILENAME_TEMPLATE",
2122
id="dbbackup.W003",
2223
)
23-
W004 = Warning(
24+
W004 = DjangoWarning(
2425
"Invalid MEDIA_FILENAME_TEMPLATE parameter",
2526
hint="Include {datetime} to settings.DBBACKUP_MEDIA_FILENAME_TEMPLATE",
2627
id="dbbackup.W004",
2728
)
28-
W005 = Warning(
29+
W005 = DjangoWarning(
2930
"Invalid DATE_FORMAT parameter",
3031
hint="settings.DBBACKUP_DATE_FORMAT can contain only [A-Za-z0-9%_-]",
3132
id="dbbackup.W005",
3233
)
3334
# W006: Historical - "FAILURE_RECIPIENTS has been deprecated"
3435

35-
W007 = Warning(
36+
W007 = DjangoWarning(
3637
"Invalid FILENAME_TEMPLATE parameter",
3738
hint="settings.DBBACKUP_FILENAME_TEMPLATE must not contain slashes ('/'). "
3839
"Did you mean to change the value for 'location'?",
3940
id="dbbackup.W007",
4041
)
41-
W008 = Warning(
42+
W008 = DjangoWarning(
4243
"Invalid MEDIA_FILENAME_TEMPLATE parameter",
4344
hint="settings.DBBACKUP_MEDIA_FILENAME_TEMPLATE must not contain slashes ('/')"
4445
"Did you mean to change the value for 'location'?",

dbbackup/management/commands/_base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@
22
Abstract Command.
33
"""
44

5+
from __future__ import annotations
6+
57
import logging
68
import sys
79
from shutil import copyfileobj
10+
from typing import TYPE_CHECKING
811

912
from django.core.management.base import BaseCommand, CommandError
1013

1114
from dbbackup.storage import StorageError
1215

16+
if TYPE_CHECKING:
17+
from dbbackup.storage import Storage
18+
1319
USELESS_ARGS = ("callback", "callback_args", "callback_kwargs", "metavar")
1420
TYPES = {
1521
"string": str,
@@ -57,6 +63,14 @@ class BaseDbBackupCommand(BaseCommand):
5763
verbosity = 1
5864
quiet = False
5965
logger = logging.getLogger("dbbackup.command")
66+
storage: Storage
67+
path: str | None = None
68+
filename: str | None = None
69+
decrypt = False
70+
uncompress = False
71+
encrypt = False
72+
compress = False
73+
content_type = ""
6074

6175
def __init__(self, *args, **kwargs):
6276
self.option_list = self.base_option_list + self.option_list

dbbackup/management/commands/dbrestore.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Restore database.
33
"""
44

5+
from __future__ import annotations
6+
57
import io
68
import json
79
import os
@@ -67,7 +69,7 @@ class Command(BaseDbBackupCommand):
6769

6870
def handle(self, *args, **options):
6971
"""Django command handler."""
70-
self.verbosity = int(options.get("verbosity"))
72+
self.verbosity = int(options.get("verbosity", "1"))
7173
self.quiet = options.get("quiet")
7274
self._set_logger_level()
7375

@@ -90,7 +92,7 @@ def handle(self, *args, **options):
9092
except StorageError as err:
9193
raise CommandError(err) from err
9294

93-
def _get_database(self, database_name: str):
95+
def _get_database(self, database_name: str | None):
9496
"""Get the database to restore."""
9597
if not database_name:
9698
if len(settings.DATABASES) > 1:

dbbackup/management/commands/listbackups.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,11 @@ def handle(self, **options):
6262
def get_backup_attrs(self, options):
6363
filters = {k: v for k, v in options.items() if k in FILTER_KEYS}
6464
filenames = self.storage.list_backups(**filters)
65-
return [
66-
{
67-
"datetime": utils.filename_to_date(filename).strftime("%x %X"),
65+
backups = []
66+
for filename in filenames:
67+
file_date = utils.filename_to_date(filename)
68+
backups.append({
69+
"datetime": file_date.strftime("%x %X") if file_date is not None else "Unknown",
6870
"name": filename,
69-
}
70-
for filename in filenames
71-
]
71+
})
72+
return backups

dbbackup/management/commands/mediarestore.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class Command(BaseDbBackupCommand):
4747

4848
def handle(self, *args, **options):
4949
"""Django command handler."""
50-
self.verbosity = int(options.get("verbosity"))
50+
self.verbosity = int(options.get("verbosity", "1"))
5151
self.quiet = options.get("quiet")
5252
self._set_logger_level()
5353

dbbackup/storage.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import contextlib
66
import logging
7+
from datetime import datetime, timezone
78

89
from django.core.exceptions import ImproperlyConfigured
910

@@ -185,7 +186,7 @@ def get_latest_backup(
185186
if not files:
186187
msg = "There's no backup file available."
187188
raise StorageError(msg)
188-
return max(files, key=utils.filename_to_date)
189+
return max(files, key=self._filename_to_date_or_min)
189190

190191
def get_older_backup(
191192
self,
@@ -230,7 +231,7 @@ def get_older_backup(
230231
if not files:
231232
msg = "There's no backup file available."
232233
raise StorageError(msg)
233-
return min(files, key=utils.filename_to_date)
234+
return min(files, key=self._filename_to_date_or_min)
234235

235236
def clean_old_backups(
236237
self,
@@ -274,7 +275,7 @@ def clean_old_backups(
274275
database=database,
275276
servername=servername,
276277
)
277-
files = sorted(files, key=utils.filename_to_date, reverse=True)
278+
files = sorted(files, key=self._filename_to_date_or_min, reverse=True)
278279
files_to_delete = [fi for i, fi in enumerate(files) if i >= keep_number]
279280
for filename in files_to_delete:
280281
if keep_filter(filename):
@@ -285,6 +286,15 @@ def clean_old_backups(
285286
if self.storage.exists(metadata_filename):
286287
self.delete_file(metadata_filename)
287288

289+
@staticmethod
290+
def _filename_to_date_or_min(filename: str) -> datetime:
291+
file_date = utils.filename_to_date(filename)
292+
if file_date is None:
293+
return datetime.min.replace(tzinfo=timezone.utc)
294+
if file_date.tzinfo is None:
295+
return file_date.replace(tzinfo=timezone.utc)
296+
return file_date
297+
288298

289299
def get_storage_class(path=None):
290300
"""

dbbackup/utils.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def wrapper(*args, **kwargs):
123123
logger = logging.getLogger("dbbackup")
124124
exc_type, exc_value, tb = sys.exc_info()
125125
tb_str = "".join(traceback.format_tb(tb))
126-
msg = f"{exc_type.__name__}: {exc_value}\n{tb_str}"
126+
msg = f"{getattr(exc_type, '__name__', str(exc_type) or 'Exception')}: {exc_value}\n{tb_str}"
127127
logger.exception(msg)
128128
raise
129129
finally:
@@ -155,7 +155,19 @@ def create_spooled_temporary_file(filepath=None, fileobj=None):
155155
return spooled_file
156156

157157

158-
def encrypt_file(inputfile, filename):
158+
def _gpg_encrypt_file(inputfile, filepath, recipients, always_trust):
159+
import gnupg
160+
161+
g = gnupg.GPG()
162+
return g.encrypt_file(
163+
inputfile,
164+
output=filepath,
165+
recipients=recipients,
166+
always_trust=always_trust,
167+
)
168+
169+
170+
def encrypt_file(inputfile, filename): # sourcery skip: extract-method
159171
"""
160172
Encrypt input file using GPG and remove .gpg extension to its name.
161173
@@ -168,8 +180,6 @@ def encrypt_file(inputfile, filename):
168180
:returns: Tuple with file and new file's name
169181
:rtype: :class:`tempfile.SpooledTemporaryFile`, ``str``
170182
"""
171-
import gnupg
172-
173183
tempdir = tempfile.mkdtemp(dir=settings.TMP_DIR)
174184
try:
175185
filename = f"{filename}.gpg"
@@ -179,14 +189,8 @@ def encrypt_file(inputfile, filename):
179189
raise ValueError(msg)
180190
try:
181191
inputfile.seek(0)
182-
always_trust = settings.GPG_ALWAYS_TRUST
183-
g = gnupg.GPG()
184-
result = g.encrypt_file(
185-
inputfile,
186-
output=filepath,
187-
recipients=settings.GPG_RECIPIENT,
188-
always_trust=always_trust,
189-
)
192+
always_trust = bool(settings.GPG_ALWAYS_TRUST)
193+
result = _gpg_encrypt_file(inputfile, filepath, settings.GPG_RECIPIENT, always_trust)
190194
inputfile.close()
191195
if not result:
192196
msg = f"Encryption failed; status: {result.status}"

tests/test_connectors/test_sqlite.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,12 @@ def test_restore_dump_warns_only_for_serious_errors(self):
120120

121121
# Should warn about the serious error
122122
dbbackup_warnings = [w for w in warning_list if "dbbackup" in str(w.filename)]
123-
assert len(dbbackup_warnings) > 0, "Should warn about 'no such table' error"
123+
assert dbbackup_warnings, "Should warn about 'no such table' error"
124124

125125
warning_messages = [str(w.message) for w in dbbackup_warnings]
126-
assert any(
127-
"no such table" in msg.lower() for msg in warning_messages
128-
), f"Should warn about 'no such table', got: {warning_messages}"
126+
assert any("no such table" in msg.lower() for msg in warning_messages), (
127+
f"Should warn about 'no such table', got: {warning_messages}"
128+
)
129129

130130
def test_create_dump_with_virtual_tables(self):
131131
with connection.cursor() as c:
@@ -210,11 +210,11 @@ def test_restore_warns_about_already_exists_errors(self):
210210
dbbackup_warnings = [w for w in warning_list if "dbbackup" in str(w.filename)]
211211

212212
# Should warn about "already exists" errors now that filtering is removed
213-
assert len(dbbackup_warnings) > 0, "Should have warnings for 'already exists' errors"
213+
assert dbbackup_warnings, "Should have warnings for 'already exists' errors"
214214

215215
# Verify we get warnings about "already exists"
216216
already_exists_warnings = [w for w in dbbackup_warnings if "already exists" in str(w.message).lower()]
217-
assert len(already_exists_warnings) > 0, "Should warn about 'already exists' errors"
217+
assert already_exists_warnings, "Should warn about 'already exists' errors"
218218

219219

220220
@patch("dbbackup.db.sqlite.open", mock_open(read_data=b"foo"), create=True)

0 commit comments

Comments
 (0)