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/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
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@v3
Expand Down
14 changes: 14 additions & 0 deletions django_sqlite_backup/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,17 @@ def restore_db(self, date_str: str) -> None:

with open(db_name, "wb") as f:
f.write(response.get("Body").read())


class AwsListDb:
def list_db(self) -> list[str]:
bucket_name = settings.SQLITE_BACKUP.get("BUCKET_NAME")

if bucket_name is None:
raise ImproperlyConfigured("`BUCKET_NAME` is not defined")

response = s3().list_objects(
Bucket=bucket_name,
)

return [backup_date["Key"] for backup_date in response["Contents"]]
26 changes: 26 additions & 0 deletions django_sqlite_backup/list_backups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Protocol
from typing import runtime_checkable

from django.conf import settings
from django.utils.module_loading import import_string


@runtime_checkable
class SqliteList(Protocol):
def list_db(self) -> list[str]:
pass


DEFAULT_LIST_CLASS = "django_sqlite_backup.aws.AwsListDb"


def get_list_class() -> type[SqliteList]:
class_string = getattr(settings, "SQLITE_BACKUP", {}).get("LIST_CLASS") or DEFAULT_LIST_CLASS

return import_string(class_string)


def do_list() -> list[str]:
list_class = get_list_class()
list_class_instance = list_class()
return list_class_instance.list_db()
11 changes: 11 additions & 0 deletions django_sqlite_backup/management/commands/list_backups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.core.management.base import BaseCommand

from django_sqlite_backup import list_backups


class Command(BaseCommand):
def handle(self, *args, **options) -> None:
backups = list_backups.do_list()

for backup in backups:
self.stdout.write(backup)
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "django-sqlite-backup"
version = "0.2.0"
description = "A Django app to easily backup your sqlite database through an endpoint."
readme = "README.md"
requires-python = ">=3.9"
requires-python = ">=3.10"
license = { file = "LICENSE" }
authors = [
{name = "Ferran Jovell", email = "ferran.jovell+gh@gmail.com"}
Expand All @@ -12,10 +12,11 @@ keywords = ["django"]
classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.8",
"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",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries",
"Environment :: Web Environment",
Expand Down
5 changes: 5 additions & 0 deletions testing/dummy_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ def get_database_name(self) -> str:
class DummySqliteRestore:
def restore_db(self, date_str: str = "") -> None:
pass


class DummyListBackups:
def list_db(self) -> list[str]:
return []
33 changes: 33 additions & 0 deletions tests/test_aws_list_backups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import pytest
from django.conf import ImproperlyConfigured
from moto import mock_aws

from django_sqlite_backup.aws import AwsListDb
from testing.constants import TEST_DATE_STR
from testing.constants import TEST_DB_NAME


@pytest.fixture
def instance():
return AwsListDb()


@mock_aws
@pytest.mark.usefixtures("aws_credentials")
def test_aws_sqlite_list_db_raises_exception_if_bucket_name_not_defined(
instance, setup_test_bucket, test_settings
):
setup_test_bucket()
del test_settings.SQLITE_BACKUP["BUCKET_NAME"]
with pytest.raises(ImproperlyConfigured):
instance.list_db()


@mock_aws
@pytest.mark.usefixtures("aws_credentials")
def test_aws_sqlite_list_db_returns_list_of_backups(
instance, setup_sqlite_restore, default_settings, fake_db
):
setup_sqlite_restore()
default_settings.DATABASES["default"]["NAME"] = TEST_DB_NAME
assert instance.list_db() == [f"sqlite_backup/{TEST_DATE_STR}/fake.db"]
Empty file removed tests/test_decorators.py
Empty file.
27 changes: 27 additions & 0 deletions tests/test_list_backup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from unittest.mock import patch

import pytest

from django_sqlite_backup import list_backups
from django_sqlite_backup.aws import AwsListDb


def test_get_list_class_returns_default_value():
cls = list_backups.get_list_class()
assert cls is AwsListDb


def test_get_backup_class_with_wrong_settings_raises_exception(default_settings):
default_settings.SQLITE_BACKUP["LIST_CLASS"] = "foo.bar.Baz"

with pytest.raises(ModuleNotFoundError):
list_backups.get_list_class()


def test_do_list_backups_calls_do_list_method(default_settings):
default_settings.SQLITE_BACKUP["LIST_CLASS"] = "testing.dummy_backup.DummyListBackups"

with patch("testing.dummy_backup.DummyListBackups.list_db") as mocked:
list_backups.do_list()

mocked.assert_called_with()
28 changes: 28 additions & 0 deletions tests/test_list_backup_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from unittest.mock import patch

import pytest
from django.core.management import call_command


@pytest.fixture
def mock_backup():
with patch(
"django_sqlite_backup.list_backups.do_list",
return_value=["foo/bar/baz.db"],
) as mocked:
yield mocked


@pytest.fixture
def list_run(mock_backup):
def _(*args, **kwatgs):
call_command("list_backups")

return _


def test_list_backups_command_writes_to_stdout_on_success(list_run, capsys):
list_run()
out, err = capsys.readouterr()
assert out == "foo/bar/baz.db\n"
assert err == ""