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
101 changes: 101 additions & 0 deletions aiohasupervisor/backups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Backups client for supervisor."""

from .client import _SupervisorComponentClient
from .const import ResponseType
from .models.backups import (
Backup,
BackupComplete,
BackupJob,
BackupList,
BackupsInfo,
BackupsOptions,
FreezeOptions,
FullBackupOptions,
FullRestoreOptions,
NewBackup,
PartialBackupOptions,
PartialRestoreOptions,
)


class BackupsClient(_SupervisorComponentClient):
"""Handles backups access in Supervisor."""

async def list(self) -> list[Backup]:
"""List backups."""
result = await self._client.get("backups")
return BackupList.from_dict(result.data).backups

async def info(self) -> BackupsInfo:
"""Get backups info."""
result = await self._client.get("backups/info")
return BackupsInfo.from_dict(result.data)

async def options(self, options: BackupsOptions) -> None:
"""Set options for backups."""
await self._client.post("backups/options", json=options.to_dict())

async def reload(self) -> None:
"""Reload backups cache."""
await self._client.post("backups/reload")

async def freeze(self, options: FreezeOptions | None = None) -> None:
"""Start a freeze for external snapshot process."""
await self._client.post(
"backups/freeze", json=options.to_dict() if options else None
)

async def thaw(self) -> None:
"""Thaw an active freeze when external snapshot process ends."""
await self._client.post("backups/thaw")

async def full_backup(self, options: FullBackupOptions | None = None) -> NewBackup:
"""Create a new full backup."""
result = await self._client.post(
"backups/new/full",
json=options.to_dict() if options else None,
response_type=ResponseType.JSON,
)
return NewBackup.from_dict(result.data)

async def partial_backup(self, options: PartialBackupOptions) -> NewBackup:
"""Create a new partial backup."""
result = await self._client.post(
"backups/new/partial",
json=options.to_dict(),
response_type=ResponseType.JSON,
)
return NewBackup.from_dict(result.data)

async def backup_info(self, backup: str) -> BackupComplete:
"""Get backup details."""
result = await self._client.get(f"backups/{backup}/info")
return BackupComplete.from_dict(result.data)

async def remove_backup(self, backup: str) -> None:
"""Remove a backup."""
await self._client.delete(f"backups/{backup}")

async def full_restore(
self, backup: str, options: FullRestoreOptions | None = None
) -> BackupJob:
"""Start full restore from backup."""
result = await self._client.post(
f"backups/{backup}/restore/full",
json=options.to_dict() if options else None,
response_type=ResponseType.JSON,
)
return BackupJob.from_dict(result.data)

async def partial_restore(
self, backup: str, options: PartialRestoreOptions
) -> BackupJob:
"""Start partial restore from backup."""
result = await self._client.post(
f"backups/{backup}/restore/partial",
json=options.to_dict(),
response_type=ResponseType.JSON,
)
return BackupJob.from_dict(result.data)

# Omitted for now - Upload and download backup
32 changes: 32 additions & 0 deletions aiohasupervisor/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@
StoreInfo,
SupervisorRole,
)
from aiohasupervisor.models.backups import (
Backup,
BackupAddon,
BackupComplete,
BackupContent,
BackupJob,
BackupsInfo,
BackupsOptions,
BackupType,
Folder,
FreezeOptions,
FullBackupOptions,
FullRestoreOptions,
NewBackup,
PartialBackupOptions,
PartialRestoreOptions,
)
from aiohasupervisor.models.homeassistant import (
HomeAssistantInfo,
HomeAssistantOptions,
Expand Down Expand Up @@ -138,4 +155,19 @@
"GreenOptions",
"YellowInfo",
"YellowOptions",
"Backup",
"BackupAddon",
"BackupComplete",
"BackupContent",
"BackupJob",
"BackupsInfo",
"BackupsOptions",
"BackupType",
"Folder",
"FreezeOptions",
"FullBackupOptions",
"FullRestoreOptions",
"NewBackup",
"PartialBackupOptions",
"PartialRestoreOptions",
]
169 changes: 169 additions & 0 deletions aiohasupervisor/models/backups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""Models for Supervisor backups."""

from abc import ABC
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum

from .base import Request, ResponseData

# --- ENUMS ----


class BackupType(StrEnum):
"""BackupType type."""

FULL = "full"
PARTIAL = "partial"


class Folder(StrEnum):
"""Folder type."""

SHARE = "share"
ADDONS = "addons/local"
SSL = "ssl"
MEDIA = "media"


# --- OBJECTS ----


@dataclass(frozen=True, slots=True)
class BackupContent(ResponseData):
"""BackupContent model."""

homeassistant: bool
addons: list[str]
folders: list[str]


@dataclass(frozen=True)
class BackupBaseFields(ABC):
"""BackupBaseFields ABC type."""

slug: str
name: str
date: datetime
type: BackupType
size: float
location: str | None
protected: bool
compressed: bool


@dataclass(frozen=True, slots=True)
class Backup(BackupBaseFields, ResponseData):
"""Backup model."""

content: BackupContent


@dataclass(frozen=True, slots=True)
class BackupAddon(ResponseData):
"""BackupAddon model."""

slug: str
name: str
version: str
size: float


@dataclass(frozen=True, slots=True)
class BackupComplete(BackupBaseFields, ResponseData):
"""BackupComplete model."""

supervisor_version: str
homeassistant: str
addons: list[BackupAddon]
repositories: list[str]
folders: list[str]
homeassistant_exclude_database: bool | None


@dataclass(frozen=True, slots=True)
class BackupList(ResponseData):
"""BackupList model."""

backups: list[Backup]


@dataclass(frozen=True, slots=True)
class BackupsInfo(BackupList):
"""BackupsInfo model."""

days_until_stale: int


@dataclass(frozen=True, slots=True)
class BackupsOptions(Request):
"""BackupsOptions model."""

days_until_stale: int


@dataclass(frozen=True, slots=True)
class FreezeOptions(Request):
"""FreezeOptions model."""

timeout: int


@dataclass(frozen=True)
class PartialBackupRestoreOptions(ABC): # noqa: B024
"""PartialBackupRestoreOptions ABC type."""

addons: set[str] | None = None
folders: set[Folder] | None = None
homeassistant: bool | None = None

def __post_init__(self) -> None:
"""Validate at least one thing to backup/restore is included."""
if not any((self.addons, self.folders, self.homeassistant)):
raise ValueError(
"At least one of addons, folders, or homeassistant must have a value"
)


@dataclass(frozen=True, slots=True)
class FullBackupOptions(Request):
"""FullBackupOptions model."""

name: str | None = None
password: str | None = None
compressed: bool | None = None
location: str | None = None
homeassistant_exclude_database: bool | None = None
background: bool | None = None


@dataclass(frozen=True, slots=True)
class PartialBackupOptions(FullBackupOptions, PartialBackupRestoreOptions):
"""PartialBackupOptions model."""


@dataclass(frozen=True, slots=True)
class BackupJob(ResponseData):
"""BackupJob model."""

job_id: str


@dataclass(frozen=True, slots=True)
class NewBackup(BackupJob):
"""NewBackup model."""

slug: str | None = None


@dataclass(frozen=True, slots=True)
class FullRestoreOptions(Request):
"""FullRestoreOptions model."""

password: str | None = None
background: bool | None = None


@dataclass(frozen=True, slots=True)
class PartialRestoreOptions(FullRestoreOptions, PartialBackupRestoreOptions):
"""PartialRestoreOptions model."""
2 changes: 1 addition & 1 deletion aiohasupervisor/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Options(ABC, DataClassDictMixin):
def __post_init__(self) -> None:
"""Validate at least one field is present."""
if not self.to_dict():
raise TypeError("At least one field must have a value")
raise ValueError("At least one field must have a value")

class Config(RequestConfig):
"""Mashumaro config."""
Expand Down
7 changes: 7 additions & 0 deletions aiohasupervisor/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from aiohttp import ClientSession

from .addons import AddonsClient
from .backups import BackupsClient
from .client import _SupervisorClient
from .homeassistant import HomeAssistantClient
from .models.root import AvailableUpdate, AvailableUpdates, RootInfo
Expand All @@ -28,6 +29,7 @@ def __init__(
self._client = _SupervisorClient(api_host, token, request_timeout, session)
self._addons = AddonsClient(self._client)
self._os = OSClient(self._client)
self._backups = BackupsClient(self._client)
self._resolution = ResolutionClient(self._client)
self._store = StoreClient(self._client)
self._supervisor = SupervisorManagementClient(self._client)
Expand All @@ -48,6 +50,11 @@ def os(self) -> OSClient:
"""Get OS component client."""
return self._os

@property
def backups(self) -> BackupsClient:
"""Get backups component client."""
return self._backups

@property
def resolution(self) -> ResolutionClient:
"""Get resolution center component client."""
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/backup_background.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"result": "ok",
"data": { "job_id": "dc9dbc16f6ad4de592ffa72c807ca2bf" }
}
4 changes: 4 additions & 0 deletions tests/fixtures/backup_foreground.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"result": "ok",
"data": { "job_id": "dc9dbc16f6ad4de592ffa72c807ca2bf", "slug": "9ecf0028" }
}
32 changes: 32 additions & 0 deletions tests/fixtures/backup_info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"result": "ok",
"data": {
"slug": "69558789",
"type": "partial",
"name": "addon_core_mosquitto_6.4.0",
"date": "2024-05-31T00:00:00.000000+00:00",
"size": 0.01,
"compressed": true,
"protected": false,
"supervisor_version": "2024.05.0",
"homeassistant": null,
"location": null,
"addons": [
{
"slug": "core_mosquitto",
"name": "Mosquitto broker",
"version": "6.4.0",
"size": 0.0
}
],
"repositories": [
"core",
"local",
"https://github.com/music-assistant/home-assistant-addon",
"https://github.com/esphome/home-assistant-addon",
"https://github.com/hassio-addons/repository"
],
"folders": [],
"homeassistant_exclude_database": null
}
}
Loading
Loading