mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-17 22:26:30 +00:00
Sort jobs by creation in API (#5545)
* Sort jobs by creation in API * Fix tests missing new field * Fix sorting logic around child jobs
This commit is contained in:
parent
da6bdfa795
commit
600bf91c4f
@ -31,9 +31,16 @@ class APIJobs(CoreSysAttributes):
|
|||||||
raise APINotFound("Job does not exist") from None
|
raise APINotFound("Job does not exist") from None
|
||||||
|
|
||||||
def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]:
|
def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]:
|
||||||
"""Return current job tree."""
|
"""Return current job tree.
|
||||||
|
|
||||||
|
Jobs are added to cache as they are created so by default they are in oldest to newest.
|
||||||
|
This is correct ordering for child jobs as it makes logical sense to present those in
|
||||||
|
the order they occurred within the parent. For the list as a whole, sort from newest
|
||||||
|
to oldest as its likely any client is most interested in the newer ones.
|
||||||
|
"""
|
||||||
|
# Initially sort oldest to newest so all child lists end up in correct order
|
||||||
jobs_by_parent: dict[str | None, list[SupervisorJob]] = {}
|
jobs_by_parent: dict[str | None, list[SupervisorJob]] = {}
|
||||||
for job in self.sys_jobs.jobs:
|
for job in sorted(self.sys_jobs.jobs):
|
||||||
if job.internal:
|
if job.internal:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -42,11 +49,15 @@ class APIJobs(CoreSysAttributes):
|
|||||||
else:
|
else:
|
||||||
jobs_by_parent[job.parent_id].append(job)
|
jobs_by_parent[job.parent_id].append(job)
|
||||||
|
|
||||||
|
# After parent-child organization, sort the root jobs only from newest to oldest
|
||||||
job_list: list[dict[str, Any]] = []
|
job_list: list[dict[str, Any]] = []
|
||||||
queue: list[tuple[list[dict[str, Any]], SupervisorJob]] = (
|
queue: list[tuple[list[dict[str, Any]], SupervisorJob]] = (
|
||||||
[(job_list, start)]
|
[(job_list, start)]
|
||||||
if start
|
if start
|
||||||
else [(job_list, job) for job in jobs_by_parent.get(None, [])]
|
else [
|
||||||
|
(job_list, job)
|
||||||
|
for job in sorted(jobs_by_parent.get(None, []), reverse=True)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
while queue:
|
while queue:
|
||||||
|
@ -19,6 +19,7 @@ from ..coresys import CoreSys, CoreSysAttributes
|
|||||||
from ..exceptions import HassioError, JobNotFound, JobStartException
|
from ..exceptions import HassioError, JobNotFound, JobStartException
|
||||||
from ..homeassistant.const import WSEvent
|
from ..homeassistant.const import WSEvent
|
||||||
from ..utils.common import FileConfiguration
|
from ..utils.common import FileConfiguration
|
||||||
|
from ..utils.dt import utcnow
|
||||||
from ..utils.sentry import capture_exception
|
from ..utils.sentry import capture_exception
|
||||||
from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition
|
from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition
|
||||||
from .validate import SCHEMA_JOBS_CONFIG
|
from .validate import SCHEMA_JOBS_CONFIG
|
||||||
@ -79,10 +80,12 @@ class SupervisorJobError:
|
|||||||
return {"type": self.type_.__name__, "message": self.message}
|
return {"type": self.type_.__name__, "message": self.message}
|
||||||
|
|
||||||
|
|
||||||
@define
|
@define(order=True)
|
||||||
class SupervisorJob:
|
class SupervisorJob:
|
||||||
"""Representation of a job running in supervisor."""
|
"""Representation of a job running in supervisor."""
|
||||||
|
|
||||||
|
created: datetime = field(init=False, factory=utcnow, on_setattr=frozen)
|
||||||
|
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
|
||||||
name: str | None = field(default=None, validator=[_invalid_if_started])
|
name: str | None = field(default=None, validator=[_invalid_if_started])
|
||||||
reference: str | None = field(default=None, on_setattr=_on_change)
|
reference: str | None = field(default=None, on_setattr=_on_change)
|
||||||
progress: float = field(
|
progress: float = field(
|
||||||
@ -94,7 +97,6 @@ class SupervisorJob:
|
|||||||
stage: str | None = field(
|
stage: str | None = field(
|
||||||
default=None, validator=[_invalid_if_done], on_setattr=_on_change
|
default=None, validator=[_invalid_if_done], on_setattr=_on_change
|
||||||
)
|
)
|
||||||
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
|
|
||||||
parent_id: UUID | None = field(
|
parent_id: UUID | None = field(
|
||||||
factory=lambda: _CURRENT_JOB.get(None), on_setattr=frozen
|
factory=lambda: _CURRENT_JOB.get(None), on_setattr=frozen
|
||||||
)
|
)
|
||||||
@ -119,6 +121,7 @@ class SupervisorJob:
|
|||||||
"done": self.done,
|
"done": self.done,
|
||||||
"parent_id": self.parent_id,
|
"parent_id": self.parent_id,
|
||||||
"errors": [err.as_dict() for err in self.errors],
|
"errors": [err.as_dict() for err in self.errors],
|
||||||
|
"created": self.created.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def capture_error(self, err: HassioError | None = None) -> None:
|
def capture_error(self, err: HassioError | None = None) -> None:
|
||||||
|
@ -102,6 +102,18 @@ async def test_jobs_tree_representation(api_client: TestClient, coresys: CoreSys
|
|||||||
result = await resp.json()
|
result = await resp.json()
|
||||||
assert result["data"]["jobs"] == [
|
assert result["data"]["jobs"] == [
|
||||||
{
|
{
|
||||||
|
"created": ANY,
|
||||||
|
"name": "test_jobs_tree_alt",
|
||||||
|
"reference": None,
|
||||||
|
"uuid": ANY,
|
||||||
|
"progress": 0,
|
||||||
|
"stage": "init",
|
||||||
|
"done": False,
|
||||||
|
"child_jobs": [],
|
||||||
|
"errors": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": ANY,
|
||||||
"name": "test_jobs_tree_outer",
|
"name": "test_jobs_tree_outer",
|
||||||
"reference": None,
|
"reference": None,
|
||||||
"uuid": ANY,
|
"uuid": ANY,
|
||||||
@ -111,6 +123,7 @@ async def test_jobs_tree_representation(api_client: TestClient, coresys: CoreSys
|
|||||||
"errors": [],
|
"errors": [],
|
||||||
"child_jobs": [
|
"child_jobs": [
|
||||||
{
|
{
|
||||||
|
"created": ANY,
|
||||||
"name": "test_jobs_tree_inner",
|
"name": "test_jobs_tree_inner",
|
||||||
"reference": None,
|
"reference": None,
|
||||||
"uuid": ANY,
|
"uuid": ANY,
|
||||||
@ -122,16 +135,6 @@ async def test_jobs_tree_representation(api_client: TestClient, coresys: CoreSys
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "test_jobs_tree_alt",
|
|
||||||
"reference": None,
|
|
||||||
"uuid": ANY,
|
|
||||||
"progress": 0,
|
|
||||||
"stage": "init",
|
|
||||||
"done": False,
|
|
||||||
"child_jobs": [],
|
|
||||||
"errors": [],
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
test.event.set()
|
test.event.set()
|
||||||
@ -141,6 +144,7 @@ async def test_jobs_tree_representation(api_client: TestClient, coresys: CoreSys
|
|||||||
result = await resp.json()
|
result = await resp.json()
|
||||||
assert result["data"]["jobs"] == [
|
assert result["data"]["jobs"] == [
|
||||||
{
|
{
|
||||||
|
"created": ANY,
|
||||||
"name": "test_jobs_tree_alt",
|
"name": "test_jobs_tree_alt",
|
||||||
"reference": None,
|
"reference": None,
|
||||||
"uuid": ANY,
|
"uuid": ANY,
|
||||||
@ -182,6 +186,7 @@ async def test_job_manual_cleanup(api_client: TestClient, coresys: CoreSys):
|
|||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
result = await resp.json()
|
result = await resp.json()
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
|
"created": ANY,
|
||||||
"name": "test_job_manual_cleanup",
|
"name": "test_job_manual_cleanup",
|
||||||
"reference": None,
|
"reference": None,
|
||||||
"uuid": test.job_id,
|
"uuid": test.job_id,
|
||||||
@ -229,3 +234,86 @@ async def test_job_not_found(api_client: TestClient, method: str, url: str):
|
|||||||
assert resp.status == 404
|
assert resp.status == 404
|
||||||
body = await resp.json()
|
body = await resp.json()
|
||||||
assert body["message"] == "Job does not exist"
|
assert body["message"] == "Job does not exist"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_jobs_sorted(api_client: TestClient, coresys: CoreSys):
|
||||||
|
"""Test jobs are sorted by datetime in results."""
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
"""Test class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize the test class."""
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
@Job(name="test_jobs_sorted_1", cleanup=False)
|
||||||
|
async def test_jobs_sorted_1(self):
|
||||||
|
"""Sorted test method 1."""
|
||||||
|
await self.test_jobs_sorted_inner_1()
|
||||||
|
await self.test_jobs_sorted_inner_2()
|
||||||
|
|
||||||
|
@Job(name="test_jobs_sorted_inner_1", cleanup=False)
|
||||||
|
async def test_jobs_sorted_inner_1(self):
|
||||||
|
"""Sorted test inner method 1."""
|
||||||
|
|
||||||
|
@Job(name="test_jobs_sorted_inner_2", cleanup=False)
|
||||||
|
async def test_jobs_sorted_inner_2(self):
|
||||||
|
"""Sorted test inner method 2."""
|
||||||
|
|
||||||
|
@Job(name="test_jobs_sorted_2", cleanup=False)
|
||||||
|
async def test_jobs_sorted_2(self):
|
||||||
|
"""Sorted test method 2."""
|
||||||
|
|
||||||
|
test = TestClass(coresys)
|
||||||
|
await test.test_jobs_sorted_1()
|
||||||
|
await test.test_jobs_sorted_2()
|
||||||
|
|
||||||
|
resp = await api_client.get("/jobs/info")
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["data"]["jobs"] == [
|
||||||
|
{
|
||||||
|
"created": ANY,
|
||||||
|
"name": "test_jobs_sorted_2",
|
||||||
|
"reference": None,
|
||||||
|
"uuid": ANY,
|
||||||
|
"progress": 0,
|
||||||
|
"stage": None,
|
||||||
|
"done": True,
|
||||||
|
"errors": [],
|
||||||
|
"child_jobs": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": ANY,
|
||||||
|
"name": "test_jobs_sorted_1",
|
||||||
|
"reference": None,
|
||||||
|
"uuid": ANY,
|
||||||
|
"progress": 0,
|
||||||
|
"stage": None,
|
||||||
|
"done": True,
|
||||||
|
"errors": [],
|
||||||
|
"child_jobs": [
|
||||||
|
{
|
||||||
|
"created": ANY,
|
||||||
|
"name": "test_jobs_sorted_inner_1",
|
||||||
|
"reference": None,
|
||||||
|
"uuid": ANY,
|
||||||
|
"progress": 0,
|
||||||
|
"stage": None,
|
||||||
|
"done": True,
|
||||||
|
"errors": [],
|
||||||
|
"child_jobs": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": ANY,
|
||||||
|
"name": "test_jobs_sorted_inner_2",
|
||||||
|
"reference": None,
|
||||||
|
"uuid": ANY,
|
||||||
|
"progress": 0,
|
||||||
|
"stage": None,
|
||||||
|
"done": True,
|
||||||
|
"errors": [],
|
||||||
|
"child_jobs": [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
@ -1066,6 +1066,7 @@ def _make_backup_message_for_assert(
|
|||||||
"done": done,
|
"done": done,
|
||||||
"parent_id": None,
|
"parent_id": None,
|
||||||
"errors": [],
|
"errors": [],
|
||||||
|
"created": ANY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -997,6 +997,7 @@ async def test_internal_jobs_no_notify(coresys: CoreSys):
|
|||||||
"done": True,
|
"done": True,
|
||||||
"parent_id": None,
|
"parent_id": None,
|
||||||
"errors": [],
|
"errors": [],
|
||||||
|
"created": ANY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,7 @@ async def test_notify_on_change(coresys: CoreSys):
|
|||||||
"done": None,
|
"done": None,
|
||||||
"parent_id": None,
|
"parent_id": None,
|
||||||
"errors": [],
|
"errors": [],
|
||||||
|
"created": ANY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -125,6 +126,7 @@ async def test_notify_on_change(coresys: CoreSys):
|
|||||||
"done": None,
|
"done": None,
|
||||||
"parent_id": None,
|
"parent_id": None,
|
||||||
"errors": [],
|
"errors": [],
|
||||||
|
"created": ANY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -146,6 +148,7 @@ async def test_notify_on_change(coresys: CoreSys):
|
|||||||
"done": None,
|
"done": None,
|
||||||
"parent_id": None,
|
"parent_id": None,
|
||||||
"errors": [],
|
"errors": [],
|
||||||
|
"created": ANY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -167,6 +170,7 @@ async def test_notify_on_change(coresys: CoreSys):
|
|||||||
"done": False,
|
"done": False,
|
||||||
"parent_id": None,
|
"parent_id": None,
|
||||||
"errors": [],
|
"errors": [],
|
||||||
|
"created": ANY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -193,6 +197,7 @@ async def test_notify_on_change(coresys: CoreSys):
|
|||||||
"message": "Unknown error, see supervisor logs",
|
"message": "Unknown error, see supervisor logs",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"created": ANY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -218,6 +223,7 @@ async def test_notify_on_change(coresys: CoreSys):
|
|||||||
"message": "Unknown error, see supervisor logs",
|
"message": "Unknown error, see supervisor logs",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"created": ANY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user