Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ Don't forget to remove deprecated code on each major release!

## [Unreleased]

- Nothing (yet)!
### Fixed

- Fix compression when using `DjangoConnector`.
- Fix an issue with paramiko `os.stat` sizes were larger on the target compared to the source.

## [5.2.0] - 2026-02-10

Expand Down
12 changes: 8 additions & 4 deletions dbbackup/db/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
any Django-supported database backend.
"""

import codecs
import contextlib
import os
import tempfile
Expand Down Expand Up @@ -35,8 +36,11 @@ def _create_dump(self):
Returns a file-like object containing the serialized database data
in JSON format.
"""
# Create a SpooledTemporaryFile in text mode for direct use with dumpdata
dump_file = SpooledTemporaryFile(mode="w+t", encoding="utf-8")

binary_dump_file = SpooledTemporaryFile(mode="w+b")

# Wrap Binary SpooledTemporaryFile in text mode for direct use with dumpdata
dump_file = codecs.getwriter("utf-8")(binary_dump_file)

# Prepare arguments for dumpdata command
dump_args = []
Expand Down Expand Up @@ -108,8 +112,8 @@ def _create_dump(self):
call_command("dumpdata", *dump_args, **dump_kwargs)

# Reset file position to beginning for reading
dump_file.seek(0)
return dump_file
binary_dump_file.seek(0)
return binary_dump_file

def _restore_dump(self, dump):
"""
Expand Down
25 changes: 12 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,32 @@ keywords = [
]
license = "BSD-3-Clause"
authors = [{ name = "Mark Bakhit", email = "archiethemonger@gmail.com" }]
requires-python = ">=3.9"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Environment :: Console",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Database",
"Topic :: System :: Archiving",
"Topic :: System :: Archiving :: Backup",
"Topic :: System :: Archiving :: Compression",
]
dependencies = ["django>=4.2"]
dependencies = ["django>=5.0"]
dynamic = ["version"]
urls.Changelog = "https://archmonger.github.io/django-dbbackup/latest/changelog/"
urls.Documentation = "https://archmonger.github.io/django-dbbackup"
Expand Down Expand Up @@ -80,11 +80,6 @@ matrix-name-format = "{variable}-{value}"
[tool.hatch.envs.hatch-test.env-vars]
DJANGO_SETTINGS_MODULE = "tests.settings"

# Django 4.2
[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.9", "3.10", "3.11", "3.12"]
django = ["4.2"]

# Django 5.0
[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.10", "3.11", "3.12"]
Expand All @@ -100,11 +95,13 @@ django = ["5.1"]
python = ["3.10", "3.11", "3.12", "3.13"]
django = ["5.2"]

# Django 6.0
[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.12", "3.13", "3.14"]
django = ["6.0"]

[tool.hatch.envs.hatch-test.overrides]
matrix.django.dependencies = [
{ if = [
"4.2",
], value = "django>=4.2,<4.3" },
{ if = [
"5.0",
], value = "django>=5.0,<5.1" },
Expand All @@ -114,6 +111,9 @@ matrix.django.dependencies = [
{ if = [
"5.2",
], value = "django>=5.2,<5.3" },
{ if = [
"6.0",
], value = "django>=6.0,<6.1" },
]

# >>> Documentation Scripts <<<
Expand Down Expand Up @@ -191,7 +191,6 @@ postgres = ["python scripts/postgres_live_test.py {args}"]

[tool.ruff]
line-length = 120
target-version = "py39"
extend-exclude = [".eggs/*", ".tox/*", ".venv/*", "build/*", "*/migrations/*"]
format.preview = true
lint.extend-ignore = [
Expand Down
30 changes: 27 additions & 3 deletions tests/test_connectors/test_django.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def mock_dumpdata(*args, **kwargs):
# Verify dump content
assert isinstance(dump, SpooledTemporaryFile)
dump.seek(0)
content = dump.read() # Already a string in text mode
assert '"model": "auth.user"' in content
assert '"username": "test"' in content
content = dump.read()
assert b'"model": "auth.user"' in content
assert b'"username": "test"' in content

@patch("dbbackup.db.django.call_command")
def test_create_dump_with_exclude_app_model_format(self, mock_call_command):
Expand Down Expand Up @@ -243,3 +243,27 @@ def mock_dumpdata(*args, **kwargs):

# Verify loaddata was called
assert mock_call_command.call_count == 1

@patch("dbbackup.db.django.call_command")
def test_dump_is_binary(self, mock_call_command):
"""Test that the created dump is a binary file and can be compressed."""
# Mock the dumpdata command to write JSON to stdout
def mock_dumpdata(*args, **kwargs):
if "stdout" in kwargs:
kwargs["stdout"].write('[{"model": "auth.user", "pk": 1, "fields": {"username": "test"}}]')

mock_call_command.side_effect = mock_dumpdata

# Create the dump
dump_file = self.connector.create_dump()
dump_file.seek(0)

# Try to compress it (this will fail if it's not a binary file)
import gzip
compressed_file = SpooledTemporaryFile()
with gzip.GzipFile(fileobj=compressed_file, mode="wb") as gz_file:
gz_file.write(dump_file.read())

# Check that we have compressed data
compressed_file.seek(0)
assert len(compressed_file.read()) > 0
Loading