Skip to content

Commit

Permalink
Add BaseBackupManager as a common interface for backup managers (home…
Browse files Browse the repository at this point in the history
…-assistant#126611)

* Add BaseBackupManager as a common interface for backup managers

* Document the key

* Update homeassistant/components/backup/manager.py

Co-authored-by: Martin Hjelmare <[email protected]>

---------

Co-authored-by: Martin Hjelmare <[email protected]>
  • Loading branch information
ludeeus and MartinHjelmare authored Oct 15, 2024
1 parent 78fce90 commit a14cb13
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 44 deletions.
2 changes: 1 addition & 1 deletion homeassistant/components/backup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:

async def async_handle_create_service(call: ServiceCall) -> None:
"""Service handler for creating backups."""
await backup_manager.generate_backup()
await backup_manager.async_create_backup()

hass.services.async_register(DOMAIN, "create", async_handle_create_service)

Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/backup/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from homeassistant.util import slugify

from .const import DOMAIN
from .manager import BackupManager
from .manager import BaseBackupManager


@callback
Expand All @@ -36,8 +36,8 @@ async def get(
if not request["hass_user"].is_admin:
return Response(status=HTTPStatus.UNAUTHORIZED)

manager: BackupManager = request.app[KEY_HASS].data[DOMAIN]
backup = await manager.get_backup(slug)
manager: BaseBackupManager = request.app[KEY_HASS].data[DOMAIN]
backup = await manager.async_get_backup(slug=slug)

if backup is None or not backup.path.exists():
return Response(status=HTTPStatus.NOT_FOUND)
Expand Down
60 changes: 47 additions & 13 deletions homeassistant/components/backup/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import abc
import asyncio
from dataclasses import asdict, dataclass
import hashlib
Expand Down Expand Up @@ -53,15 +54,48 @@ async def async_post_backup(self, hass: HomeAssistant) -> None:
"""Perform operations after a backup finishes."""


class BackupManager:
"""Backup manager for the Backup integration."""
class BaseBackupManager(abc.ABC):
"""Define the format that backup managers can have."""

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the backup manager."""
self.hass = hass
self.backup_dir = Path(hass.config.path("backups"))
self.backing_up = False
self.backups: dict[str, Backup] = {}
self.backing_up = False

async def async_post_backup_actions(self, **kwargs: Any) -> None:
"""Post backup actions."""

async def async_pre_backup_actions(self, **kwargs: Any) -> None:
"""Pre backup actions."""

@abc.abstractmethod
async def async_create_backup(self, **kwargs: Any) -> Backup:
"""Generate a backup."""

@abc.abstractmethod
async def async_get_backups(self, **kwargs: Any) -> dict[str, Backup]:
"""Get backups.
Return a dictionary of Backup instances keyed by their slug.
"""

@abc.abstractmethod
async def async_get_backup(self, *, slug: str, **kwargs: Any) -> Backup | None:
"""Get a backup."""

@abc.abstractmethod
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
"""Remove a backup."""


class BackupManager(BaseBackupManager):
"""Backup manager for the Backup integration."""

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the backup manager."""
super().__init__(hass=hass)
self.backup_dir = Path(hass.config.path("backups"))
self.platforms: dict[str, BackupPlatformProtocol] = {}
self.loaded_backups = False
self.loaded_platforms = False
Expand All @@ -84,7 +118,7 @@ def _add_platform(
return
self.platforms[integration_domain] = platform

async def pre_backup_actions(self) -> None:
async def async_pre_backup_actions(self, **kwargs: Any) -> None:
"""Perform pre backup actions."""
if not self.loaded_platforms:
await self.load_platforms()
Expand All @@ -100,7 +134,7 @@ async def pre_backup_actions(self) -> None:
if isinstance(result, Exception):
raise result

async def post_backup_actions(self) -> None:
async def async_post_backup_actions(self, **kwargs: Any) -> None:
"""Perform post backup actions."""
if not self.loaded_platforms:
await self.load_platforms()
Expand Down Expand Up @@ -151,14 +185,14 @@ def _read_backups(self) -> dict[str, Backup]:
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
return backups

async def get_backups(self) -> dict[str, Backup]:
async def async_get_backups(self, **kwargs: Any) -> dict[str, Backup]:
"""Return backups."""
if not self.loaded_backups:
await self.load_backups()

return self.backups

async def get_backup(self, slug: str) -> Backup | None:
async def async_get_backup(self, *, slug: str, **kwargs: Any) -> Backup | None:
"""Return a backup."""
if not self.loaded_backups:
await self.load_backups()
Expand All @@ -180,23 +214,23 @@ async def get_backup(self, slug: str) -> Backup | None:

return backup

async def remove_backup(self, slug: str) -> None:
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
"""Remove a backup."""
if (backup := await self.get_backup(slug)) is None:
if (backup := await self.async_get_backup(slug=slug)) is None:
return

await self.hass.async_add_executor_job(backup.path.unlink, True)
LOGGER.debug("Removed backup located at %s", backup.path)
self.backups.pop(slug)

async def generate_backup(self) -> Backup:
async def async_create_backup(self, **kwargs: Any) -> Backup:
"""Generate a backup."""
if self.backing_up:
raise HomeAssistantError("Backup already in progress")

try:
self.backing_up = True
await self.pre_backup_actions()
await self.async_pre_backup_actions()
backup_name = f"Core {HAVERSION}"
date_str = dt_util.now().isoformat()
slug = _generate_slug(date_str, backup_name)
Expand Down Expand Up @@ -229,7 +263,7 @@ async def generate_backup(self) -> Backup:
return backup
finally:
self.backing_up = False
await self.post_backup_actions()
await self.async_post_backup_actions()

def _mkdir_and_generate_backup_contents(
self,
Expand Down
10 changes: 5 additions & 5 deletions homeassistant/components/backup/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async def handle_info(
) -> None:
"""List all stored backups."""
manager = hass.data[DATA_MANAGER]
backups = await manager.get_backups()
backups = await manager.async_get_backups()
connection.send_result(
msg["id"],
{
Expand All @@ -57,7 +57,7 @@ async def handle_remove(
msg: dict[str, Any],
) -> None:
"""Remove a backup."""
await hass.data[DATA_MANAGER].remove_backup(msg["slug"])
await hass.data[DATA_MANAGER].async_remove_backup(slug=msg["slug"])
connection.send_result(msg["id"])


Expand All @@ -70,7 +70,7 @@ async def handle_create(
msg: dict[str, Any],
) -> None:
"""Generate a backup."""
backup = await hass.data[DATA_MANAGER].generate_backup()
backup = await hass.data[DATA_MANAGER].async_create_backup()
connection.send_result(msg["id"], backup)


Expand All @@ -88,7 +88,7 @@ async def handle_backup_start(
LOGGER.debug("Backup start notification")

try:
await manager.pre_backup_actions()
await manager.async_pre_backup_actions()
except Exception as err: # noqa: BLE001
connection.send_error(msg["id"], "pre_backup_actions_failed", str(err))
return
Expand All @@ -110,7 +110,7 @@ async def handle_backup_end(
LOGGER.debug("Backup end notification")

try:
await manager.post_backup_actions()
await manager.async_post_backup_actions()
except Exception as err: # noqa: BLE001
connection.send_error(msg["id"], "post_backup_actions_failed", str(err))
return
Expand Down
2 changes: 1 addition & 1 deletion tests/components/backup/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def test_downloading_backup(

with (
patch(
"homeassistant.components.backup.manager.BackupManager.get_backup",
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
return_value=TEST_BACKUP,
),
patch("pathlib.Path.exists", return_value=True),
Expand Down
2 changes: 1 addition & 1 deletion tests/components/backup/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async def test_create_service(
await setup_backup_integration(hass)

with patch(
"homeassistant.components.backup.manager.BackupManager.generate_backup",
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
) as generate_backup:
await hass.services.async_call(
DOMAIN,
Expand Down
26 changes: 13 additions & 13 deletions tests/components/backup/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def _mock_iterdir(path: Path) -> list[Path]:
"2025.1.0",
),
):
await manager.generate_backup()
await manager.async_create_backup()

assert mocked_json_bytes.call_count == 1
backup_json_dict = mocked_json_bytes.call_args[0][0]
Expand Down Expand Up @@ -108,7 +108,7 @@ async def test_load_backups(hass: HomeAssistant) -> None:
),
):
await manager.load_backups()
backups = await manager.get_backups()
backups = await manager.async_get_backups()
assert backups == {TEST_BACKUP.slug: TEST_BACKUP}


Expand All @@ -123,7 +123,7 @@ async def test_load_backups_with_exception(
patch("tarfile.open", side_effect=OSError("Test exception")),
):
await manager.load_backups()
backups = await manager.get_backups()
backups = await manager.async_get_backups()
assert f"Unable to read backup {TEST_BACKUP.path}: Test exception" in caplog.text
assert backups == {}

Expand All @@ -138,7 +138,7 @@ async def test_removing_backup(
manager.loaded_backups = True

with patch("pathlib.Path.exists", return_value=True):
await manager.remove_backup(TEST_BACKUP.slug)
await manager.async_remove_backup(slug=TEST_BACKUP.slug)
assert "Removed backup located at" in caplog.text


Expand All @@ -149,7 +149,7 @@ async def test_removing_non_existing_backup(
"""Test removing not existing backup."""
manager = BackupManager(hass)

await manager.remove_backup("non_existing")
await manager.async_remove_backup(slug="non_existing")
assert "Removed backup located at" not in caplog.text


Expand All @@ -163,7 +163,7 @@ async def test_getting_backup_that_does_not_exist(
manager.loaded_backups = True

with patch("pathlib.Path.exists", return_value=False):
backup = await manager.get_backup(TEST_BACKUP.slug)
backup = await manager.async_get_backup(slug=TEST_BACKUP.slug)
assert backup is None

assert (
Expand All @@ -172,15 +172,15 @@ async def test_getting_backup_that_does_not_exist(
) in caplog.text


async def test_generate_backup_when_backing_up(hass: HomeAssistant) -> None:
async def test_async_create_backup_when_backing_up(hass: HomeAssistant) -> None:
"""Test generate backup."""
manager = BackupManager(hass)
manager.backing_up = True
with pytest.raises(HomeAssistantError, match="Backup already in progress"):
await manager.generate_backup()
await manager.async_create_backup()


async def test_generate_backup(
async def test_async_create_backup(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
Expand Down Expand Up @@ -285,7 +285,7 @@ async def _mock_step(hass: HomeAssistant) -> None:
await _mock_backup_generation(manager)


async def test_loading_platforms_when_running_pre_backup_actions(
async def test_loading_platforms_when_running_async_pre_backup_actions(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
Expand All @@ -302,15 +302,15 @@ async def test_loading_platforms_when_running_pre_backup_actions(
async_post_backup=AsyncMock(),
),
)
await manager.pre_backup_actions()
await manager.async_pre_backup_actions()

assert manager.loaded_platforms
assert len(manager.platforms) == 1

assert "Loaded 1 platforms" in caplog.text


async def test_loading_platforms_when_running_post_backup_actions(
async def test_loading_platforms_when_running_async_post_backup_actions(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
Expand All @@ -327,7 +327,7 @@ async def test_loading_platforms_when_running_post_backup_actions(
async_post_backup=AsyncMock(),
),
)
await manager.post_backup_actions()
await manager.async_post_backup_actions()

assert manager.loaded_platforms
assert len(manager.platforms) == 1
Expand Down
14 changes: 7 additions & 7 deletions tests/components/backup/test_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async def test_info(
await hass.async_block_till_done()

with patch(
"homeassistant.components.backup.manager.BackupManager.get_backups",
"homeassistant.components.backup.manager.BackupManager.async_get_backups",
return_value={TEST_BACKUP.slug: TEST_BACKUP},
):
await client.send_json_auto_id({"type": "backup/info"})
Expand All @@ -72,7 +72,7 @@ async def test_remove(
await hass.async_block_till_done()

with patch(
"homeassistant.components.backup.manager.BackupManager.remove_backup",
"homeassistant.components.backup.manager.BackupManager.async_remove_backup",
):
await client.send_json_auto_id({"type": "backup/remove", "slug": "abc123"})
assert snapshot == await client.receive_json()
Expand All @@ -98,7 +98,7 @@ async def test_generate(
await hass.async_block_till_done()

with patch(
"homeassistant.components.backup.manager.BackupManager.generate_backup",
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
return_value=TEST_BACKUP,
):
await client.send_json_auto_id({"type": "backup/generate"})
Expand Down Expand Up @@ -132,7 +132,7 @@ async def test_backup_end(
await hass.async_block_till_done()

with patch(
"homeassistant.components.backup.manager.BackupManager.post_backup_actions",
"homeassistant.components.backup.manager.BackupManager.async_post_backup_actions",
):
await client.send_json_auto_id({"type": "backup/end"})
assert snapshot == await client.receive_json()
Expand Down Expand Up @@ -165,7 +165,7 @@ async def test_backup_start(
await hass.async_block_till_done()

with patch(
"homeassistant.components.backup.manager.BackupManager.pre_backup_actions",
"homeassistant.components.backup.manager.BackupManager.async_pre_backup_actions",
):
await client.send_json_auto_id({"type": "backup/start"})
assert snapshot == await client.receive_json()
Expand Down Expand Up @@ -193,7 +193,7 @@ async def test_backup_end_excepion(
await hass.async_block_till_done()

with patch(
"homeassistant.components.backup.manager.BackupManager.post_backup_actions",
"homeassistant.components.backup.manager.BackupManager.async_post_backup_actions",
side_effect=exception,
):
await client.send_json_auto_id({"type": "backup/end"})
Expand Down Expand Up @@ -222,7 +222,7 @@ async def test_backup_start_excepion(
await hass.async_block_till_done()

with patch(
"homeassistant.components.backup.manager.BackupManager.pre_backup_actions",
"homeassistant.components.backup.manager.BackupManager.async_pre_backup_actions",
side_effect=exception,
):
await client.send_json_auto_id({"type": "backup/start"})
Expand Down

0 comments on commit a14cb13

Please sign in to comment.