Skip to content

Commit 72f7e7e

Browse files
committed
Add jobs API to library
1 parent 51a8a78 commit 72f7e7e

File tree

7 files changed

+327
-0
lines changed

7 files changed

+327
-0
lines changed

aiohasupervisor/jobs.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Jobs client for supervisor."""
2+
3+
from uuid import UUID
4+
5+
from .client import _SupervisorComponentClient
6+
from .models.jobs import Job, JobsInfo, JobsOptions
7+
8+
9+
class JobsClient(_SupervisorComponentClient):
10+
"""Handles Jobs access in Supervisor."""
11+
12+
async def info(self) -> JobsInfo:
13+
"""Get Jobs info."""
14+
result = await self._client.get("jobs/info")
15+
return JobsInfo.from_dict(result.data)
16+
17+
async def options(self, options: JobsOptions) -> None:
18+
"""Set Jobs options."""
19+
await self._client.post("jobs/options", json=options.to_dict())
20+
21+
async def reset(self) -> None:
22+
"""Reset Jobs options (primarily clears previously ignored job conditions)."""
23+
await self._client.post("jobs/reset")
24+
25+
async def get_job(self, job: UUID) -> Job:
26+
"""Get details of a job."""
27+
result = await self._client.get(f"jobs/{job.hex}")
28+
return Job.from_dict(result.data)
29+
30+
async def delete_job(self, job: UUID) -> None:
31+
"""Remove a done job from Supervisor's cache."""
32+
await self._client.delete(f"jobs/{job.hex}")

aiohasupervisor/models/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@
6666
ServiceState,
6767
ShutdownOptions,
6868
)
69+
from aiohasupervisor.models.jobs import (
70+
Job,
71+
JobCondition,
72+
JobError,
73+
JobsInfo,
74+
JobsOptions,
75+
)
6976
from aiohasupervisor.models.mounts import (
7077
CIFSMountRequest,
7178
CIFSMountResponse,
@@ -248,6 +255,11 @@
248255
"Service",
249256
"ServiceState",
250257
"ShutdownOptions",
258+
"Job",
259+
"JobCondition",
260+
"JobError",
261+
"JobsInfo",
262+
"JobsOptions",
251263
"CIFSMountRequest",
252264
"CIFSMountResponse",
253265
"MountCifsVersion",

aiohasupervisor/models/jobs.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Models for Supervisor jobs."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from enum import StrEnum
7+
from uuid import UUID # noqa: TCH003
8+
9+
from .base import Request, ResponseData
10+
11+
# --- ENUMS ----
12+
13+
14+
class JobCondition(StrEnum):
15+
"""JobCondition type.
16+
17+
This is an incomplete list. Supervisor regularly adds support for new
18+
job conditions as they are found to be needed. Therefore when returning
19+
a list of job conditions, there may be some which are not in
20+
this list parsed as strings on older versions of the client.
21+
"""
22+
23+
AUTO_UPDATE = "auto_update"
24+
FREE_SPACE = "free_space"
25+
FROZEN = "frozen"
26+
HAOS = "haos"
27+
HEALTHY = "healthy"
28+
HOST_NETWORK = "host_network"
29+
INTERNET_HOST = "internet_host"
30+
INTERNET_SYSTEM = "internet_system"
31+
MOUNT_AVAILABLE = "mount_available"
32+
OS_AGENT = "os_agent"
33+
PLUGINS_UPDATED = "plugins_updated"
34+
RUNNING = "running"
35+
SUPERVISOR_UPDATED = "supervisor_updated"
36+
37+
38+
# --- OBJECTS ----
39+
40+
41+
@dataclass(slots=True, frozen=True)
42+
class JobError(ResponseData):
43+
"""JobError model."""
44+
45+
type: str
46+
message: str
47+
48+
49+
@dataclass(slots=True, frozen=True)
50+
class Job(ResponseData):
51+
"""Job model."""
52+
53+
name: str | None
54+
reference: str | None
55+
uuid: UUID
56+
progress: float
57+
stage: str | None
58+
done: bool | None
59+
errors: list[JobError]
60+
child_jobs: list[Job]
61+
62+
63+
@dataclass(slots=True, frozen=True)
64+
class JobsInfo(ResponseData):
65+
"""JobsInfo model."""
66+
67+
ignore_conditions: list[JobCondition | str]
68+
jobs: list[Job]
69+
70+
71+
@dataclass(slots=True, frozen=True)
72+
class JobsOptions(Request):
73+
"""JobsOptions model."""
74+
75+
# We only do `| str` in responses since we can't control what supervisor returns
76+
# Support for ignoring new job conditions will wait for a new version of library
77+
ignore_conditions: list[JobCondition]

aiohasupervisor/root.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .discovery import DiscoveryClient
1111
from .homeassistant import HomeAssistantClient
1212
from .host import HostClient
13+
from .jobs import JobsClient
1314
from .models.root import AvailableUpdate, AvailableUpdates, RootInfo
1415
from .mounts import MountsClient
1516
from .network import NetworkClient
@@ -34,6 +35,7 @@ def __init__(
3435
self._os = OSClient(self._client)
3536
self._backups = BackupsClient(self._client)
3637
self._discovery = DiscoveryClient(self._client)
38+
self._jobs = JobsClient(self._client)
3739
self._mounts = MountsClient(self._client)
3840
self._network = NetworkClient(self._client)
3941
self._host = HostClient(self._client)
@@ -67,6 +69,11 @@ def discovery(self) -> DiscoveryClient:
6769
"""Get discovery component client."""
6870
return self._discovery
6971

72+
@property
73+
def jobs(self) -> JobsClient:
74+
"""Get jobs component client."""
75+
return self._jobs
76+
7077
@property
7178
def mounts(self) -> MountsClient:
7279
"""Get mounts component client."""

tests/fixtures/jobs_get_job.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"result": "ok",
3+
"data": {
4+
"name": "backup_manager_partial_backup",
5+
"reference": "89cafa67",
6+
"uuid": "2febe59311f94d6ba36f6f9f73357ca8",
7+
"progress": 0,
8+
"stage": "finishing_file",
9+
"done": true,
10+
"errors": [],
11+
"child_jobs": [
12+
{
13+
"name": "backup_store_folders",
14+
"reference": "89cafa67",
15+
"uuid": "f4bac7d9240f434a9f6a21aff7f7ade3",
16+
"progress": 0,
17+
"stage": null,
18+
"done": true,
19+
"errors": [],
20+
"child_jobs": [
21+
{
22+
"name": "backup_folder_save",
23+
"reference": "ssl",
24+
"uuid": "fb328a1a048d4b9b982bf6b9070c03a6",
25+
"progress": 0,
26+
"stage": null,
27+
"done": true,
28+
"errors": [],
29+
"child_jobs": []
30+
}
31+
]
32+
}
33+
]
34+
}
35+
}

tests/fixtures/jobs_info.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"result": "ok",
3+
"data": {
4+
"ignore_conditions": ["free_space"],
5+
"jobs": [
6+
{
7+
"name": "backup_manager_partial_backup",
8+
"reference": "89cafa67",
9+
"uuid": "2febe59311f94d6ba36f6f9f73357ca8",
10+
"progress": 0,
11+
"stage": "finishing_file",
12+
"done": true,
13+
"errors": [],
14+
"child_jobs": [
15+
{
16+
"name": "backup_store_folders",
17+
"reference": "89cafa67",
18+
"uuid": "f4bac7d9240f434a9f6a21aff7f7ade3",
19+
"progress": 0,
20+
"stage": null,
21+
"done": true,
22+
"errors": [],
23+
"child_jobs": [
24+
{
25+
"name": "backup_folder_save",
26+
"reference": "ssl",
27+
"uuid": "fb328a1a048d4b9b982bf6b9070c03a6",
28+
"progress": 0,
29+
"stage": null,
30+
"done": true,
31+
"errors": [],
32+
"child_jobs": []
33+
}
34+
]
35+
}
36+
]
37+
},
38+
{
39+
"name": "backup_manager_partial_restore",
40+
"reference": "cfddca18",
41+
"uuid": "6e61e9629669499981780b700ec730ec",
42+
"progress": 0,
43+
"stage": null,
44+
"done": true,
45+
"errors": [
46+
{
47+
"type": "BackupInvalidError",
48+
"message": "Invalid password for backup cfddca18"
49+
}
50+
],
51+
"child_jobs": []
52+
}
53+
]
54+
}
55+
}

tests/test_jobs.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Test jobs supervisor client."""
2+
3+
from uuid import UUID
4+
5+
from aioresponses import aioresponses
6+
from yarl import URL
7+
8+
from aiohasupervisor import SupervisorClient
9+
from aiohasupervisor.models import JobCondition, JobsOptions
10+
11+
from . import load_fixture
12+
from .const import SUPERVISOR_URL
13+
14+
15+
async def test_jobs_info(
16+
responses: aioresponses, supervisor_client: SupervisorClient
17+
) -> None:
18+
"""Test jobs info API."""
19+
responses.get(
20+
f"{SUPERVISOR_URL}/jobs/info", status=200, body=load_fixture("jobs_info.json")
21+
)
22+
info = await supervisor_client.jobs.info()
23+
assert info.ignore_conditions == [JobCondition.FREE_SPACE]
24+
25+
assert info.jobs[0].name == "backup_manager_partial_backup"
26+
assert info.jobs[0].reference == "89cafa67"
27+
assert info.jobs[0].uuid.hex == "2febe59311f94d6ba36f6f9f73357ca8"
28+
assert info.jobs[0].progress == 0
29+
assert info.jobs[0].stage == "finishing_file"
30+
assert info.jobs[0].done is True
31+
assert info.jobs[0].errors == []
32+
assert info.jobs[0].child_jobs[0].name == "backup_store_folders"
33+
assert info.jobs[0].child_jobs[0].child_jobs[0].name == "backup_folder_save"
34+
assert info.jobs[0].child_jobs[0].child_jobs[0].reference == "ssl"
35+
assert info.jobs[0].child_jobs[0].child_jobs[0].child_jobs == []
36+
37+
assert info.jobs[1].name == "backup_manager_partial_restore"
38+
assert info.jobs[1].reference == "cfddca18"
39+
assert info.jobs[1].errors[0].type == "BackupInvalidError"
40+
assert info.jobs[1].errors[0].message == "Invalid password for backup cfddca18"
41+
42+
43+
async def test_jobs_options(
44+
responses: aioresponses, supervisor_client: SupervisorClient
45+
) -> None:
46+
"""Test jobs options API."""
47+
responses.post(f"{SUPERVISOR_URL}/jobs/options", status=200)
48+
assert (
49+
await supervisor_client.jobs.options(
50+
JobsOptions(ignore_conditions=[JobCondition.FREE_SPACE])
51+
)
52+
is None
53+
)
54+
assert responses.requests.keys() == {
55+
("POST", URL(f"{SUPERVISOR_URL}/jobs/options"))
56+
}
57+
58+
59+
async def test_jobs_reset(
60+
responses: aioresponses, supervisor_client: SupervisorClient
61+
) -> None:
62+
"""Test jobs reset API."""
63+
responses.post(f"{SUPERVISOR_URL}/jobs/reset", status=200)
64+
assert await supervisor_client.jobs.reset() is None
65+
assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/jobs/reset"))}
66+
67+
68+
async def test_jobs_get_job(
69+
responses: aioresponses, supervisor_client: SupervisorClient
70+
) -> None:
71+
"""Test jobs get job API."""
72+
responses.get(
73+
f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8",
74+
status=200,
75+
body=load_fixture("jobs_get_job.json"),
76+
)
77+
info = await supervisor_client.jobs.get_job(
78+
UUID("2febe59311f94d6ba36f6f9f73357ca8")
79+
)
80+
81+
assert info.name == "backup_manager_partial_backup"
82+
assert info.reference == "89cafa67"
83+
assert info.uuid.hex == "2febe59311f94d6ba36f6f9f73357ca8"
84+
assert info.progress == 0
85+
assert info.stage == "finishing_file"
86+
assert info.done is True
87+
assert info.errors == []
88+
assert info.child_jobs[0].name == "backup_store_folders"
89+
assert info.child_jobs[0].child_jobs[0].name == "backup_folder_save"
90+
assert info.child_jobs[0].child_jobs[0].reference == "ssl"
91+
assert info.child_jobs[0].child_jobs[0].child_jobs == []
92+
93+
94+
async def test_jobs_delete_job(
95+
responses: aioresponses, supervisor_client: SupervisorClient
96+
) -> None:
97+
"""Test jobs delete job API."""
98+
responses.delete(
99+
f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8", status=200
100+
)
101+
assert (
102+
await supervisor_client.jobs.delete_job(
103+
UUID("2febe59311f94d6ba36f6f9f73357ca8")
104+
)
105+
is None
106+
)
107+
assert responses.requests.keys() == {
108+
("DELETE", URL(f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8"))
109+
}

0 commit comments

Comments
 (0)