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

from uuid import UUID

from .client import _SupervisorComponentClient
from .models.jobs import Job, JobsInfo, JobsOptions


class JobsClient(_SupervisorComponentClient):
"""Handles Jobs access in Supervisor."""

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

async def set_options(self, options: JobsOptions) -> None:
"""Set Jobs options."""
await self._client.post("jobs/options", json=options.to_dict())

async def reset(self) -> None:
"""Reset Jobs options (primarily clears previously ignored job conditions)."""
await self._client.post("jobs/reset")

async def get_job(self, job: UUID) -> Job:
"""Get details of a job."""
result = await self._client.get(f"jobs/{job.hex}")
return Job.from_dict(result.data)

async def delete_job(self, job: UUID) -> None:
"""Remove a done job from Supervisor's cache."""
await self._client.delete(f"jobs/{job.hex}")
12 changes: 12 additions & 0 deletions aiohasupervisor/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@
ServiceState,
ShutdownOptions,
)
from aiohasupervisor.models.jobs import (
Job,
JobCondition,
JobError,
JobsInfo,
JobsOptions,
)
from aiohasupervisor.models.mounts import (
CIFSMountRequest,
CIFSMountResponse,
Expand Down Expand Up @@ -248,6 +255,11 @@
"Service",
"ServiceState",
"ShutdownOptions",
"Job",
"JobCondition",
"JobError",
"JobsInfo",
"JobsOptions",
"CIFSMountRequest",
"CIFSMountResponse",
"MountCifsVersion",
Expand Down
77 changes: 77 additions & 0 deletions aiohasupervisor/models/jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Models for Supervisor jobs."""

from __future__ import annotations

from dataclasses import dataclass
from enum import StrEnum
from uuid import UUID # noqa: TCH003

from .base import Request, ResponseData

# --- ENUMS ----


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

This is an incomplete list. Supervisor regularly adds support for new
job conditions as they are found to be needed. Therefore when returning
a list of job conditions, there may be some which are not in
this list parsed as strings on older versions of the client.
"""

AUTO_UPDATE = "auto_update"
FREE_SPACE = "free_space"
FROZEN = "frozen"
HAOS = "haos"
HEALTHY = "healthy"
HOST_NETWORK = "host_network"
INTERNET_HOST = "internet_host"
INTERNET_SYSTEM = "internet_system"
MOUNT_AVAILABLE = "mount_available"
OS_AGENT = "os_agent"
PLUGINS_UPDATED = "plugins_updated"
RUNNING = "running"
SUPERVISOR_UPDATED = "supervisor_updated"


# --- OBJECTS ----


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

type: str
message: str


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

name: str | None
reference: str | None
uuid: UUID
progress: float
stage: str | None
done: bool | None
errors: list[JobError]
child_jobs: list[Job]


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

ignore_conditions: list[JobCondition | str]
jobs: list[Job]


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

# We only do `| str` in responses since we can't control what supervisor returns
# Support for ignoring new job conditions will wait for a new version of library
ignore_conditions: list[JobCondition]
7 changes: 7 additions & 0 deletions aiohasupervisor/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .discovery import DiscoveryClient
from .homeassistant import HomeAssistantClient
from .host import HostClient
from .jobs import JobsClient
from .models.root import AvailableUpdate, AvailableUpdates, RootInfo
from .mounts import MountsClient
from .network import NetworkClient
Expand All @@ -34,6 +35,7 @@ def __init__(
self._os = OSClient(self._client)
self._backups = BackupsClient(self._client)
self._discovery = DiscoveryClient(self._client)
self._jobs = JobsClient(self._client)
self._mounts = MountsClient(self._client)
self._network = NetworkClient(self._client)
self._host = HostClient(self._client)
Expand Down Expand Up @@ -67,6 +69,11 @@ def discovery(self) -> DiscoveryClient:
"""Get discovery component client."""
return self._discovery

@property
def jobs(self) -> JobsClient:
"""Get jobs component client."""
return self._jobs

@property
def mounts(self) -> MountsClient:
"""Get mounts component client."""
Expand Down
35 changes: 35 additions & 0 deletions tests/fixtures/jobs_get_job.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"result": "ok",
"data": {
"name": "backup_manager_partial_backup",
"reference": "89cafa67",
"uuid": "2febe59311f94d6ba36f6f9f73357ca8",
"progress": 0,
"stage": "finishing_file",
"done": true,
"errors": [],
"child_jobs": [
{
"name": "backup_store_folders",
"reference": "89cafa67",
"uuid": "f4bac7d9240f434a9f6a21aff7f7ade3",
"progress": 0,
"stage": null,
"done": true,
"errors": [],
"child_jobs": [
{
"name": "backup_folder_save",
"reference": "ssl",
"uuid": "fb328a1a048d4b9b982bf6b9070c03a6",
"progress": 0,
"stage": null,
"done": true,
"errors": [],
"child_jobs": []
}
]
}
]
}
}
55 changes: 55 additions & 0 deletions tests/fixtures/jobs_info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"result": "ok",
"data": {
"ignore_conditions": ["free_space"],
"jobs": [
{
"name": "backup_manager_partial_backup",
"reference": "89cafa67",
"uuid": "2febe59311f94d6ba36f6f9f73357ca8",
"progress": 0,
"stage": "finishing_file",
"done": true,
"errors": [],
"child_jobs": [
{
"name": "backup_store_folders",
"reference": "89cafa67",
"uuid": "f4bac7d9240f434a9f6a21aff7f7ade3",
"progress": 0,
"stage": null,
"done": true,
"errors": [],
"child_jobs": [
{
"name": "backup_folder_save",
"reference": "ssl",
"uuid": "fb328a1a048d4b9b982bf6b9070c03a6",
"progress": 0,
"stage": null,
"done": true,
"errors": [],
"child_jobs": []
}
]
}
]
},
{
"name": "backup_manager_partial_restore",
"reference": "cfddca18",
"uuid": "6e61e9629669499981780b700ec730ec",
"progress": 0,
"stage": null,
"done": true,
"errors": [
{
"type": "BackupInvalidError",
"message": "Invalid password for backup cfddca18"
}
],
"child_jobs": []
}
]
}
}
109 changes: 109 additions & 0 deletions tests/test_jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Test jobs supervisor client."""

from uuid import UUID

from aioresponses import aioresponses
from yarl import URL

from aiohasupervisor import SupervisorClient
from aiohasupervisor.models import JobCondition, JobsOptions

from . import load_fixture
from .const import SUPERVISOR_URL


async def test_jobs_info(
responses: aioresponses, supervisor_client: SupervisorClient
) -> None:
"""Test jobs info API."""
responses.get(
f"{SUPERVISOR_URL}/jobs/info", status=200, body=load_fixture("jobs_info.json")
)
info = await supervisor_client.jobs.info()
assert info.ignore_conditions == [JobCondition.FREE_SPACE]

assert info.jobs[0].name == "backup_manager_partial_backup"
assert info.jobs[0].reference == "89cafa67"
assert info.jobs[0].uuid.hex == "2febe59311f94d6ba36f6f9f73357ca8"
assert info.jobs[0].progress == 0
assert info.jobs[0].stage == "finishing_file"
assert info.jobs[0].done is True
assert info.jobs[0].errors == []
assert info.jobs[0].child_jobs[0].name == "backup_store_folders"
assert info.jobs[0].child_jobs[0].child_jobs[0].name == "backup_folder_save"
assert info.jobs[0].child_jobs[0].child_jobs[0].reference == "ssl"
assert info.jobs[0].child_jobs[0].child_jobs[0].child_jobs == []

assert info.jobs[1].name == "backup_manager_partial_restore"
assert info.jobs[1].reference == "cfddca18"
assert info.jobs[1].errors[0].type == "BackupInvalidError"
assert info.jobs[1].errors[0].message == "Invalid password for backup cfddca18"


async def test_jobs_set_options(
responses: aioresponses, supervisor_client: SupervisorClient
) -> None:
"""Test jobs set options API."""
responses.post(f"{SUPERVISOR_URL}/jobs/options", status=200)
assert (
await supervisor_client.jobs.set_options(
JobsOptions(ignore_conditions=[JobCondition.FREE_SPACE])
)
is None
)
assert responses.requests.keys() == {
("POST", URL(f"{SUPERVISOR_URL}/jobs/options"))
}


async def test_jobs_reset(
responses: aioresponses, supervisor_client: SupervisorClient
) -> None:
"""Test jobs reset API."""
responses.post(f"{SUPERVISOR_URL}/jobs/reset", status=200)
assert await supervisor_client.jobs.reset() is None
assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/jobs/reset"))}


async def test_jobs_get_job(
responses: aioresponses, supervisor_client: SupervisorClient
) -> None:
"""Test jobs get job API."""
responses.get(
f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8",
status=200,
body=load_fixture("jobs_get_job.json"),
)
info = await supervisor_client.jobs.get_job(
UUID("2febe59311f94d6ba36f6f9f73357ca8")
)

assert info.name == "backup_manager_partial_backup"
assert info.reference == "89cafa67"
assert info.uuid.hex == "2febe59311f94d6ba36f6f9f73357ca8"
assert info.progress == 0
assert info.stage == "finishing_file"
assert info.done is True
assert info.errors == []
assert info.child_jobs[0].name == "backup_store_folders"
assert info.child_jobs[0].child_jobs[0].name == "backup_folder_save"
assert info.child_jobs[0].child_jobs[0].reference == "ssl"
assert info.child_jobs[0].child_jobs[0].child_jobs == []


async def test_jobs_delete_job(
responses: aioresponses, supervisor_client: SupervisorClient
) -> None:
"""Test jobs delete job API."""
responses.delete(
f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8", status=200
)
assert (
await supervisor_client.jobs.delete_job(
UUID("2febe59311f94d6ba36f6f9f73357ca8")
)
is None
)
assert responses.requests.keys() == {
("DELETE", URL(f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8"))
}
Loading