From a14cb131947dc40db41e8da747b6cf97bb3bc89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 15 Oct 2024 12:31:12 +0200 Subject: [PATCH] Add BaseBackupManager as a common interface for backup managers (#126611) * Add BaseBackupManager as a common interface for backup managers * Document the key * Update homeassistant/components/backup/manager.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/backup/__init__.py | 2 +- homeassistant/components/backup/http.py | 6 +- homeassistant/components/backup/manager.py | 60 +++++++++++++++----- homeassistant/components/backup/websocket.py | 10 ++-- tests/components/backup/test_http.py | 2 +- tests/components/backup/test_init.py | 2 +- tests/components/backup/test_manager.py | 26 ++++----- tests/components/backup/test_websocket.py | 14 ++--- 8 files changed, 78 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index ac37ef4ec59069..59f1e0c7fb5565 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -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) diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py index 793192aa6234c1..4cc4e61c9e4ccd 100644 --- a/homeassistant/components/backup/http.py +++ b/homeassistant/components/backup/http.py @@ -12,7 +12,7 @@ from homeassistant.util import slugify from .const import DOMAIN -from .manager import BackupManager +from .manager import BaseBackupManager @callback @@ -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) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index e33318362029a4..8ac36f220bbff7 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -2,6 +2,7 @@ from __future__ import annotations +import abc import asyncio from dataclasses import asdict, dataclass import hashlib @@ -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 @@ -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() @@ -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() @@ -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() @@ -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) @@ -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, diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index dd42fe06afc3a3..be833edbce5dbe 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -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"], { @@ -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"]) @@ -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) @@ -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 @@ -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 diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py index b4d9c52d0558fb..93ecb27bc9770b 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -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), diff --git a/tests/components/backup/test_init.py b/tests/components/backup/test_init.py index 0472111e33e200..e064939d618d10 100644 --- a/tests/components/backup/test_init.py +++ b/tests/components/backup/test_init.py @@ -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, diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 41749298819d64..1bf801a0fcf795 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -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] @@ -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} @@ -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 == {} @@ -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 @@ -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 @@ -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 ( @@ -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: @@ -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: @@ -302,7 +302,7 @@ 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 @@ -310,7 +310,7 @@ async def test_loading_platforms_when_running_pre_backup_actions( 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: @@ -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 diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 388aba6bc04f50..33e997d15e498e 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -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"}) @@ -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() @@ -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"}) @@ -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() @@ -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() @@ -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"}) @@ -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"})