Skip to content

Commit 6e21f0c

Browse files
committed
Sort jobs by creation in API
1 parent b07236b commit 6e21f0c

File tree

3 files changed

+102
-5
lines changed

3 files changed

+102
-5
lines changed

supervisor/api/jobs.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@ def _extract_job(self, request: web.Request) -> SupervisorJob:
3131
raise APINotFound("Job does not exist") from None
3232

3333
def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]:
34-
"""Return current job tree."""
34+
"""Return current job tree.
35+
36+
Jobs are added to cache as they are created so by default they are in oldest to newest.
37+
This is correct ordering for child jobs as it makes logical sense to present those in
38+
the order they occurred within the parent. For the list as a whole, sort from newest
39+
to oldest as its likely any client is most interested in the newer ones.
40+
"""
3541
jobs_by_parent: dict[str | None, list[SupervisorJob]] = {}
36-
for job in self.sys_jobs.jobs:
42+
for job in sorted(self.sys_jobs.jobs):
3743
if job.internal:
3844
continue
3945

@@ -56,7 +62,7 @@ def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]
5662
# We remove parent_id and instead use that info to represent jobs as a tree
5763
job_dict = current_job.as_dict() | {"child_jobs": child_jobs}
5864
job_dict.pop("parent_id")
59-
current_list.append(job_dict)
65+
current_list.insert(0, job_dict)
6066

6167
if current_job.uuid in jobs_by_parent:
6268
queue.extend(

supervisor/jobs/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ..exceptions import HassioError, JobNotFound, JobStartException
2020
from ..homeassistant.const import WSEvent
2121
from ..utils.common import FileConfiguration
22+
from ..utils.dt import utcnow
2223
from ..utils.sentry import capture_exception
2324
from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition
2425
from .validate import SCHEMA_JOBS_CONFIG
@@ -79,10 +80,12 @@ def as_dict(self) -> dict[str, str]:
7980
return {"type": self.type_.__name__, "message": self.message}
8081

8182

82-
@define
83+
@define(order=True)
8384
class SupervisorJob:
8485
"""Representation of a job running in supervisor."""
8586

87+
created: datetime = field(init=False, factory=utcnow, on_setattr=frozen)
88+
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
8689
name: str | None = field(default=None, validator=[_invalid_if_started])
8790
reference: str | None = field(default=None, on_setattr=_on_change)
8891
progress: float = field(
@@ -94,7 +97,6 @@ class SupervisorJob:
9497
stage: str | None = field(
9598
default=None, validator=[_invalid_if_done], on_setattr=_on_change
9699
)
97-
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
98100
parent_id: UUID | None = field(
99101
factory=lambda: _CURRENT_JOB.get(None), on_setattr=frozen
100102
)
@@ -119,6 +121,7 @@ def as_dict(self) -> dict[str, Any]:
119121
"done": self.done,
120122
"parent_id": self.parent_id,
121123
"errors": [err.as_dict() for err in self.errors],
124+
"created": self.created.isoformat(),
122125
}
123126

124127
def capture_error(self, err: HassioError | None = None) -> None:

tests/api/test_jobs.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ async def test_jobs_tree_internal(self):
102102
result = await resp.json()
103103
assert result["data"]["jobs"] == [
104104
{
105+
"created": ANY,
105106
"name": "test_jobs_tree_outer",
106107
"reference": None,
107108
"uuid": ANY,
@@ -111,6 +112,7 @@ async def test_jobs_tree_internal(self):
111112
"errors": [],
112113
"child_jobs": [
113114
{
115+
"created": ANY,
114116
"name": "test_jobs_tree_inner",
115117
"reference": None,
116118
"uuid": ANY,
@@ -123,6 +125,7 @@ async def test_jobs_tree_internal(self):
123125
],
124126
},
125127
{
128+
"created": ANY,
126129
"name": "test_jobs_tree_alt",
127130
"reference": None,
128131
"uuid": ANY,
@@ -141,6 +144,7 @@ async def test_jobs_tree_internal(self):
141144
result = await resp.json()
142145
assert result["data"]["jobs"] == [
143146
{
147+
"created": ANY,
144148
"name": "test_jobs_tree_alt",
145149
"reference": None,
146150
"uuid": ANY,
@@ -182,6 +186,7 @@ async def test_job_manual_cleanup(self) -> None:
182186
assert resp.status == 200
183187
result = await resp.json()
184188
assert result["data"] == {
189+
"created": ANY,
185190
"name": "test_job_manual_cleanup",
186191
"reference": None,
187192
"uuid": test.job_id,
@@ -229,3 +234,86 @@ async def test_job_not_found(api_client: TestClient, method: str, url: str):
229234
assert resp.status == 404
230235
body = await resp.json()
231236
assert body["message"] == "Job does not exist"
237+
238+
239+
async def test_jobs_sorted(api_client: TestClient, coresys: CoreSys):
240+
"""Test jobs are sorted by datetime in results."""
241+
242+
class TestClass:
243+
"""Test class."""
244+
245+
def __init__(self, coresys: CoreSys):
246+
"""Initialize the test class."""
247+
self.coresys = coresys
248+
249+
@Job(name="test_jobs_sorted_1", cleanup=False)
250+
async def test_jobs_sorted_1(self):
251+
"""Sorted test method 1."""
252+
await self.test_jobs_sorted_inner_1()
253+
await self.test_jobs_sorted_inner_2()
254+
255+
@Job(name="test_jobs_sorted_inner_1", cleanup=False)
256+
async def test_jobs_sorted_inner_1(self):
257+
"""Sorted test inner method 1."""
258+
259+
@Job(name="test_jobs_sorted_inner_2", cleanup=False)
260+
async def test_jobs_sorted_inner_2(self):
261+
"""Sorted test inner method 2."""
262+
263+
@Job(name="test_jobs_sorted_2", cleanup=False)
264+
async def test_jobs_sorted_2(self):
265+
"""Sorted test method 2."""
266+
267+
test = TestClass(coresys)
268+
await test.test_jobs_sorted_1()
269+
await test.test_jobs_sorted_2()
270+
271+
resp = await api_client.get("/jobs/info")
272+
result = await resp.json()
273+
assert result["data"]["jobs"] == [
274+
{
275+
"created": ANY,
276+
"name": "test_jobs_sorted_2",
277+
"reference": None,
278+
"uuid": ANY,
279+
"progress": 0,
280+
"stage": None,
281+
"done": True,
282+
"errors": [],
283+
"child_jobs": [],
284+
},
285+
{
286+
"created": ANY,
287+
"name": "test_jobs_sorted_1",
288+
"reference": None,
289+
"uuid": ANY,
290+
"progress": 0,
291+
"stage": None,
292+
"done": True,
293+
"errors": [],
294+
"child_jobs": [
295+
{
296+
"created": ANY,
297+
"name": "test_jobs_sorted_inner_1",
298+
"reference": None,
299+
"uuid": ANY,
300+
"progress": 0,
301+
"stage": None,
302+
"done": True,
303+
"errors": [],
304+
"child_jobs": [],
305+
},
306+
{
307+
"created": ANY,
308+
"name": "test_jobs_sorted_inner_2",
309+
"reference": None,
310+
"uuid": ANY,
311+
"progress": 0,
312+
"stage": None,
313+
"done": True,
314+
"errors": [],
315+
"child_jobs": [],
316+
},
317+
],
318+
},
319+
]

0 commit comments

Comments
 (0)