Skip to content

Commit 4f8de4d

Browse files
authored
Add backups APIs to client library (#18)
* Add backups APIs to client library * Add tests of options * ValueError not TypeError in post_init
1 parent 040a24b commit 4f8de4d

File tree

12 files changed

+699
-1
lines changed

12 files changed

+699
-1
lines changed

aiohasupervisor/backups.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Backups client for supervisor."""
2+
3+
from .client import _SupervisorComponentClient
4+
from .const import ResponseType
5+
from .models.backups import (
6+
Backup,
7+
BackupComplete,
8+
BackupJob,
9+
BackupList,
10+
BackupsInfo,
11+
BackupsOptions,
12+
FreezeOptions,
13+
FullBackupOptions,
14+
FullRestoreOptions,
15+
NewBackup,
16+
PartialBackupOptions,
17+
PartialRestoreOptions,
18+
)
19+
20+
21+
class BackupsClient(_SupervisorComponentClient):
22+
"""Handles backups access in Supervisor."""
23+
24+
async def list(self) -> list[Backup]:
25+
"""List backups."""
26+
result = await self._client.get("backups")
27+
return BackupList.from_dict(result.data).backups
28+
29+
async def info(self) -> BackupsInfo:
30+
"""Get backups info."""
31+
result = await self._client.get("backups/info")
32+
return BackupsInfo.from_dict(result.data)
33+
34+
async def options(self, options: BackupsOptions) -> None:
35+
"""Set options for backups."""
36+
await self._client.post("backups/options", json=options.to_dict())
37+
38+
async def reload(self) -> None:
39+
"""Reload backups cache."""
40+
await self._client.post("backups/reload")
41+
42+
async def freeze(self, options: FreezeOptions | None = None) -> None:
43+
"""Start a freeze for external snapshot process."""
44+
await self._client.post(
45+
"backups/freeze", json=options.to_dict() if options else None
46+
)
47+
48+
async def thaw(self) -> None:
49+
"""Thaw an active freeze when external snapshot process ends."""
50+
await self._client.post("backups/thaw")
51+
52+
async def full_backup(self, options: FullBackupOptions | None = None) -> NewBackup:
53+
"""Create a new full backup."""
54+
result = await self._client.post(
55+
"backups/new/full",
56+
json=options.to_dict() if options else None,
57+
response_type=ResponseType.JSON,
58+
)
59+
return NewBackup.from_dict(result.data)
60+
61+
async def partial_backup(self, options: PartialBackupOptions) -> NewBackup:
62+
"""Create a new partial backup."""
63+
result = await self._client.post(
64+
"backups/new/partial",
65+
json=options.to_dict(),
66+
response_type=ResponseType.JSON,
67+
)
68+
return NewBackup.from_dict(result.data)
69+
70+
async def backup_info(self, backup: str) -> BackupComplete:
71+
"""Get backup details."""
72+
result = await self._client.get(f"backups/{backup}/info")
73+
return BackupComplete.from_dict(result.data)
74+
75+
async def remove_backup(self, backup: str) -> None:
76+
"""Remove a backup."""
77+
await self._client.delete(f"backups/{backup}")
78+
79+
async def full_restore(
80+
self, backup: str, options: FullRestoreOptions | None = None
81+
) -> BackupJob:
82+
"""Start full restore from backup."""
83+
result = await self._client.post(
84+
f"backups/{backup}/restore/full",
85+
json=options.to_dict() if options else None,
86+
response_type=ResponseType.JSON,
87+
)
88+
return BackupJob.from_dict(result.data)
89+
90+
async def partial_restore(
91+
self, backup: str, options: PartialRestoreOptions
92+
) -> BackupJob:
93+
"""Start partial restore from backup."""
94+
result = await self._client.post(
95+
f"backups/{backup}/restore/partial",
96+
json=options.to_dict(),
97+
response_type=ResponseType.JSON,
98+
)
99+
return BackupJob.from_dict(result.data)
100+
101+
# Omitted for now - Upload and download backup

aiohasupervisor/models/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@
2323
StoreInfo,
2424
SupervisorRole,
2525
)
26+
from aiohasupervisor.models.backups import (
27+
Backup,
28+
BackupAddon,
29+
BackupComplete,
30+
BackupContent,
31+
BackupJob,
32+
BackupsInfo,
33+
BackupsOptions,
34+
BackupType,
35+
Folder,
36+
FreezeOptions,
37+
FullBackupOptions,
38+
FullRestoreOptions,
39+
NewBackup,
40+
PartialBackupOptions,
41+
PartialRestoreOptions,
42+
)
2643
from aiohasupervisor.models.homeassistant import (
2744
HomeAssistantInfo,
2845
HomeAssistantOptions,
@@ -138,4 +155,19 @@
138155
"GreenOptions",
139156
"YellowInfo",
140157
"YellowOptions",
158+
"Backup",
159+
"BackupAddon",
160+
"BackupComplete",
161+
"BackupContent",
162+
"BackupJob",
163+
"BackupsInfo",
164+
"BackupsOptions",
165+
"BackupType",
166+
"Folder",
167+
"FreezeOptions",
168+
"FullBackupOptions",
169+
"FullRestoreOptions",
170+
"NewBackup",
171+
"PartialBackupOptions",
172+
"PartialRestoreOptions",
141173
]

aiohasupervisor/models/backups.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""Models for Supervisor backups."""
2+
3+
from abc import ABC
4+
from dataclasses import dataclass
5+
from datetime import datetime
6+
from enum import StrEnum
7+
8+
from .base import Request, ResponseData
9+
10+
# --- ENUMS ----
11+
12+
13+
class BackupType(StrEnum):
14+
"""BackupType type."""
15+
16+
FULL = "full"
17+
PARTIAL = "partial"
18+
19+
20+
class Folder(StrEnum):
21+
"""Folder type."""
22+
23+
SHARE = "share"
24+
ADDONS = "addons/local"
25+
SSL = "ssl"
26+
MEDIA = "media"
27+
28+
29+
# --- OBJECTS ----
30+
31+
32+
@dataclass(frozen=True, slots=True)
33+
class BackupContent(ResponseData):
34+
"""BackupContent model."""
35+
36+
homeassistant: bool
37+
addons: list[str]
38+
folders: list[str]
39+
40+
41+
@dataclass(frozen=True)
42+
class BackupBaseFields(ABC):
43+
"""BackupBaseFields ABC type."""
44+
45+
slug: str
46+
name: str
47+
date: datetime
48+
type: BackupType
49+
size: float
50+
location: str | None
51+
protected: bool
52+
compressed: bool
53+
54+
55+
@dataclass(frozen=True, slots=True)
56+
class Backup(BackupBaseFields, ResponseData):
57+
"""Backup model."""
58+
59+
content: BackupContent
60+
61+
62+
@dataclass(frozen=True, slots=True)
63+
class BackupAddon(ResponseData):
64+
"""BackupAddon model."""
65+
66+
slug: str
67+
name: str
68+
version: str
69+
size: float
70+
71+
72+
@dataclass(frozen=True, slots=True)
73+
class BackupComplete(BackupBaseFields, ResponseData):
74+
"""BackupComplete model."""
75+
76+
supervisor_version: str
77+
homeassistant: str
78+
addons: list[BackupAddon]
79+
repositories: list[str]
80+
folders: list[str]
81+
homeassistant_exclude_database: bool | None
82+
83+
84+
@dataclass(frozen=True, slots=True)
85+
class BackupList(ResponseData):
86+
"""BackupList model."""
87+
88+
backups: list[Backup]
89+
90+
91+
@dataclass(frozen=True, slots=True)
92+
class BackupsInfo(BackupList):
93+
"""BackupsInfo model."""
94+
95+
days_until_stale: int
96+
97+
98+
@dataclass(frozen=True, slots=True)
99+
class BackupsOptions(Request):
100+
"""BackupsOptions model."""
101+
102+
days_until_stale: int
103+
104+
105+
@dataclass(frozen=True, slots=True)
106+
class FreezeOptions(Request):
107+
"""FreezeOptions model."""
108+
109+
timeout: int
110+
111+
112+
@dataclass(frozen=True)
113+
class PartialBackupRestoreOptions(ABC): # noqa: B024
114+
"""PartialBackupRestoreOptions ABC type."""
115+
116+
addons: set[str] | None = None
117+
folders: set[Folder] | None = None
118+
homeassistant: bool | None = None
119+
120+
def __post_init__(self) -> None:
121+
"""Validate at least one thing to backup/restore is included."""
122+
if not any((self.addons, self.folders, self.homeassistant)):
123+
raise ValueError(
124+
"At least one of addons, folders, or homeassistant must have a value"
125+
)
126+
127+
128+
@dataclass(frozen=True, slots=True)
129+
class FullBackupOptions(Request):
130+
"""FullBackupOptions model."""
131+
132+
name: str | None = None
133+
password: str | None = None
134+
compressed: bool | None = None
135+
location: str | None = None
136+
homeassistant_exclude_database: bool | None = None
137+
background: bool | None = None
138+
139+
140+
@dataclass(frozen=True, slots=True)
141+
class PartialBackupOptions(FullBackupOptions, PartialBackupRestoreOptions):
142+
"""PartialBackupOptions model."""
143+
144+
145+
@dataclass(frozen=True, slots=True)
146+
class BackupJob(ResponseData):
147+
"""BackupJob model."""
148+
149+
job_id: str
150+
151+
152+
@dataclass(frozen=True, slots=True)
153+
class NewBackup(BackupJob):
154+
"""NewBackup model."""
155+
156+
slug: str | None = None
157+
158+
159+
@dataclass(frozen=True, slots=True)
160+
class FullRestoreOptions(Request):
161+
"""FullRestoreOptions model."""
162+
163+
password: str | None = None
164+
background: bool | None = None
165+
166+
167+
@dataclass(frozen=True, slots=True)
168+
class PartialRestoreOptions(FullRestoreOptions, PartialBackupRestoreOptions):
169+
"""PartialRestoreOptions model."""

aiohasupervisor/models/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class Options(ABC, DataClassDictMixin):
5858
def __post_init__(self) -> None:
5959
"""Validate at least one field is present."""
6060
if not self.to_dict():
61-
raise TypeError("At least one field must have a value")
61+
raise ValueError("At least one field must have a value")
6262

6363
class Config(RequestConfig):
6464
"""Mashumaro config."""

aiohasupervisor/root.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from aiohttp import ClientSession
66

77
from .addons import AddonsClient
8+
from .backups import BackupsClient
89
from .client import _SupervisorClient
910
from .homeassistant import HomeAssistantClient
1011
from .models.root import AvailableUpdate, AvailableUpdates, RootInfo
@@ -28,6 +29,7 @@ def __init__(
2829
self._client = _SupervisorClient(api_host, token, request_timeout, session)
2930
self._addons = AddonsClient(self._client)
3031
self._os = OSClient(self._client)
32+
self._backups = BackupsClient(self._client)
3133
self._resolution = ResolutionClient(self._client)
3234
self._store = StoreClient(self._client)
3335
self._supervisor = SupervisorManagementClient(self._client)
@@ -48,6 +50,11 @@ def os(self) -> OSClient:
4850
"""Get OS component client."""
4951
return self._os
5052

53+
@property
54+
def backups(self) -> BackupsClient:
55+
"""Get backups component client."""
56+
return self._backups
57+
5158
@property
5259
def resolution(self) -> ResolutionClient:
5360
"""Get resolution center component client."""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"result": "ok",
3+
"data": { "job_id": "dc9dbc16f6ad4de592ffa72c807ca2bf" }
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"result": "ok",
3+
"data": { "job_id": "dc9dbc16f6ad4de592ffa72c807ca2bf", "slug": "9ecf0028" }
4+
}

tests/fixtures/backup_info.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"result": "ok",
3+
"data": {
4+
"slug": "69558789",
5+
"type": "partial",
6+
"name": "addon_core_mosquitto_6.4.0",
7+
"date": "2024-05-31T00:00:00.000000+00:00",
8+
"size": 0.01,
9+
"compressed": true,
10+
"protected": false,
11+
"supervisor_version": "2024.05.0",
12+
"homeassistant": null,
13+
"location": null,
14+
"addons": [
15+
{
16+
"slug": "core_mosquitto",
17+
"name": "Mosquitto broker",
18+
"version": "6.4.0",
19+
"size": 0.0
20+
}
21+
],
22+
"repositories": [
23+
"core",
24+
"local",
25+
"https://github.com/music-assistant/home-assistant-addon",
26+
"https://github.com/esphome/home-assistant-addon",
27+
"https://github.com/hassio-addons/repository"
28+
],
29+
"folders": [],
30+
"homeassistant_exclude_database": null
31+
}
32+
}

0 commit comments

Comments
 (0)