From 33677373d79247131403830d9590b2d715ef78d6 Mon Sep 17 00:00:00 2001 From: Alexander Diana Date: Mon, 6 Apr 2026 20:27:55 -0400 Subject: [PATCH 1/6] return binary temporary file in DjangoConnector _create_dump to match other connectors. --- dbbackup/db/django.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dbbackup/db/django.py b/dbbackup/db/django.py index a0144dae..4532707b 100644 --- a/dbbackup/db/django.py +++ b/dbbackup/db/django.py @@ -6,6 +6,7 @@ any Django-supported database backend. """ +import codecs import contextlib import os import tempfile @@ -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", encoding="utf-8") + + # 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 = [] @@ -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): """ From ceaa21cdcdc229c24af73b300614d6f8dfbc4fe6 Mon Sep 17 00:00:00 2001 From: Alexander Diana Date: Tue, 7 Apr 2026 22:11:19 -0400 Subject: [PATCH 2/6] fix tests --- tests/test_connectors/test_django.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_connectors/test_django.py b/tests/test_connectors/test_django.py index 3e5b65f9..9ed3d957 100644 --- a/tests/test_connectors/test_django.py +++ b/tests/test_connectors/test_django.py @@ -48,8 +48,8 @@ def mock_dumpdata(*args, **kwargs): 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 + 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): From 3d8e05afa5445bfa380dbf6c2c5d32972789be99 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:14:13 -0700 Subject: [PATCH 3/6] Bump minimum Python/Django versions --- .github/workflows/ci.yml | 2 +- dbbackup/db/django.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 087b6931..367ca7c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/dbbackup/db/django.py b/dbbackup/db/django.py index 4532707b..33dc7cf1 100644 --- a/dbbackup/db/django.py +++ b/dbbackup/db/django.py @@ -37,7 +37,7 @@ def _create_dump(self): in JSON format. """ - binary_dump_file = SpooledTemporaryFile(mode="w+b", 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) From d412eccd9fca8cf9f90f9798c84b14517c7afcff Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:16:28 -0700 Subject: [PATCH 4/6] Add binary output regression test --- tests/test_connectors/test_django.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_connectors/test_django.py b/tests/test_connectors/test_django.py index 9ed3d957..739fef22 100644 --- a/tests/test_connectors/test_django.py +++ b/tests/test_connectors/test_django.py @@ -47,7 +47,7 @@ def mock_dumpdata(*args, **kwargs): # Verify dump content assert isinstance(dump, SpooledTemporaryFile) dump.seek(0) - content = dump.read() # Already a string in text mode + content = dump.read() assert b'"model": "auth.user"' in content assert b'"username": "test"' in content @@ -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 From 427aa0e726ec35339eac3c234414139f74feb298 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:17:07 -0700 Subject: [PATCH 5/6] Add Django 6 to matrix --- pyproject.toml | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 25c36550..628d0e9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" @@ -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"] @@ -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" }, @@ -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 <<< @@ -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 = [ From d6c29fad92444d9e94e51fd632f6c8289ed73b90 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:21:39 -0700 Subject: [PATCH 6/6] Add changelog entry --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e95421..570a588a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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