Skip to content

Commit 8b45edd

Browse files
authored
[DOP-31843] add last run for /jobs response (#387)
1 parent 6175606 commit 8b45edd

File tree

11 files changed

+83
-5
lines changed

11 files changed

+83
-5
lines changed

data_rentgen/db/models/job.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# SPDX-FileCopyrightText: 2024-present MTS PJSC
22
# SPDX-License-Identifier: Apache-2.0
33

4+
from typing import TYPE_CHECKING
5+
46
from sqlalchemy import BigInteger, Column, Computed, ForeignKey, Index, String, Table, column, func, select
57
from sqlalchemy.dialects.postgresql import TSVECTOR
68
from sqlalchemy.orm import Mapped, column_property, mapped_column, relationship
@@ -10,6 +12,9 @@
1012
from data_rentgen.db.models.location import Location
1113
from data_rentgen.db.models.tag_value import TagValue
1214

15+
if TYPE_CHECKING:
16+
from data_rentgen.db.models.run import Run
17+
1318
JobTagValue: Table = Table(
1419
"job_tag_value",
1520
Base.metadata,
@@ -60,6 +65,19 @@ class Job(Base):
6065
doc="Job tag values",
6166
)
6267

68+
last_run: Mapped["Run | None"] = relationship(
69+
"Run",
70+
primaryjoin=(
71+
"and_(Job.id == foreign(Run.job_id), "
72+
"Run.id == select(Run.id).where(Run.job_id == Job.id)"
73+
".order_by(Run.created_at.desc(), Run.id.desc())"
74+
".limit(1).correlate_except(Run).scalar_subquery())"
75+
),
76+
lazy="noload",
77+
viewonly=True,
78+
uselist=False,
79+
)
80+
6381
search_vector: Mapped[str] = mapped_column(
6482
TSVECTOR,
6583
Computed(

data_rentgen/db/repositories/job.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from sqlalchemy.dialects.postgresql import insert
2828
from sqlalchemy.orm import selectinload
2929

30-
from data_rentgen.db.models import Address, Job, JobTagValue, Location, TagValue
30+
from data_rentgen.db.models import Address, Job, JobTagValue, Location, Run, TagValue
3131
from data_rentgen.db.repositories.base import Repository
3232
from data_rentgen.db.utils.search import make_tsquery, ts_match, ts_rank
3333
from data_rentgen.dto import JobDTO, PaginationDTO
@@ -165,6 +165,7 @@ async def paginate(
165165
options = [
166166
selectinload(Job.location).selectinload(Location.addresses),
167167
selectinload(Job.tag_values).selectinload(TagValue.tag),
168+
selectinload(Job.last_run).selectinload(Run.started_by_user),
168169
]
169170
return await self._paginate_by_query(
170171
query=query,

data_rentgen/server/schemas/v1/job.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from data_rentgen.server.schemas.v1.location import LocationResponseV1
88
from data_rentgen.server.schemas.v1.pagination import PaginateQueryV1
9+
from data_rentgen.server.schemas.v1.run import RunResponseV1
910
from data_rentgen.server.schemas.v1.tag import TagResponseV1
1011

1112

@@ -24,6 +25,7 @@ class JobDetailedResponseV1(BaseModel):
2425
id: str = Field(description="Job id", coerce_numbers_to_str=True)
2526
data: JobResponseV1 = Field(description="Job data")
2627
tags: list[TagResponseV1] = Field(default_factory=list, description="Job tags")
28+
last_run: RunResponseV1 | None = Field(description="Last run of the job", default=None)
2729

2830
model_config = ConfigDict(from_attributes=True)
2931

data_rentgen/server/services/job.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from fastapi import Depends
99

10-
from data_rentgen.db.models import Location
10+
from data_rentgen.db.models import Location, Run
1111
from data_rentgen.dto.pagination import PaginationDTO
1212
from data_rentgen.server.services.tag import TagData, TagValueData
1313
from data_rentgen.services.uow import UnitOfWork
@@ -26,6 +26,7 @@ class JobServiceResult:
2626
id: int
2727
data: JobData
2828
tags: list[TagData]
29+
last_run: Run | None
2930

3031

3132
class JobServicePaginatedResult(PaginationDTO[JobServiceResult]):
@@ -71,6 +72,7 @@ async def paginate(
7172
type=job.type,
7273
location=job.location,
7374
),
75+
last_run=job.last_run,
7476
tags=[
7577
TagData(
7678
id=tag.id,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added ``last_run`` field to ``GET /v1/jobs`` endpoint response, showing the most recently started run for each job.

tests/test_server/test_jobs/test_get_jobs.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from httpx import AsyncClient
55
from sqlalchemy.ext.asyncio import AsyncSession
66

7-
from data_rentgen.db.models import Job
7+
from data_rentgen.db.models import Job, Run
88
from tests.fixtures.mocks import MockedUser
9-
from tests.test_server.utils.convert_to_json import job_to_json, tag_values_to_json
10-
from tests.test_server.utils.enrich import enrich_jobs
9+
from tests.test_server.utils.convert_to_json import job_to_json, run_to_json, tag_values_to_json
10+
from tests.test_server.utils.enrich import enrich_jobs, enrich_runs
1111

1212
pytestmark = [pytest.mark.server, pytest.mark.asyncio]
1313

@@ -41,12 +41,50 @@ async def test_get_jobs_no_filters(
4141
"id": str(job.id),
4242
"data": job_to_json(job),
4343
"tags": tag_values_to_json(job.tag_values) if job.tag_values else [],
44+
"last_run": None,
4445
}
4546
for job in sorted(jobs, key=lambda x: x.name)
4647
],
4748
}
4849

4950

51+
async def test_get_jobs_with_last_run(
52+
test_client: AsyncClient,
53+
runs_with_same_job: list[Run],
54+
async_session: AsyncSession,
55+
mocked_user: MockedUser,
56+
):
57+
last_run = max(runs_with_same_job, key=lambda x: x.created_at)
58+
[last_run] = await enrich_runs([last_run], async_session)
59+
[job] = await enrich_jobs([last_run.job], async_session)
60+
response = await test_client.get(
61+
"v1/jobs",
62+
headers={"Authorization": f"Bearer {mocked_user.access_token}"},
63+
)
64+
65+
assert response.status_code == HTTPStatus.OK, response.json()
66+
assert response.json() == {
67+
"meta": {
68+
"page": 1,
69+
"page_size": 20,
70+
"total_count": 1,
71+
"pages_count": 1,
72+
"has_next": False,
73+
"has_previous": False,
74+
"next_page": None,
75+
"previous_page": None,
76+
},
77+
"items": [
78+
{
79+
"id": str(job.id),
80+
"data": job_to_json(job),
81+
"tags": tag_values_to_json(job.tag_values) if job.tag_values else [],
82+
"last_run": run_to_json(last_run),
83+
}
84+
],
85+
}
86+
87+
5088
async def test_get_jobs_unauthorized(
5189
test_client: AsyncClient,
5290
):

tests/test_server/test_jobs/test_get_jobs_by_id.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ async def test_get_jobs_by_one_id(
7171
"id": str(job.id),
7272
"data": job_to_json(job),
7373
"tags": tag_values_to_json(job.tag_values) if job.tag_values else [],
74+
"last_run": None,
7475
},
7576
],
7677
}
@@ -108,6 +109,7 @@ async def test_get_jobs_by_multiple_ids(
108109
"id": str(job.id),
109110
"data": job_to_json(job),
110111
"tags": tag_values_to_json(job.tag_values) if job.tag_values else [],
112+
"last_run": None,
111113
}
112114
for job in sorted(selected_jobs, key=lambda x: x.name)
113115
],

tests/test_server/test_jobs/test_get_jobs_by_location.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ async def test_get_jobs_by_location_id(
4646
"id": str(job.id),
4747
"data": job_to_json(job),
4848
"tags": [],
49+
"last_run": None,
4950
}
5051
for job in [dag_job, task_job]
5152
],
@@ -78,6 +79,7 @@ async def test_get_jobs_by_location_id(
7879
"id": str(dag_job.id),
7980
"data": job_to_json(dag_job),
8081
"tags": tag_values_to_json(dag_job.tag_values) if dag_job.tag_values else [],
82+
"last_run": None,
8183
},
8284
],
8385
}
@@ -145,6 +147,7 @@ async def test_get_jobs_by_location_type(
145147
"id": str(job.id),
146148
"data": job_to_json(job),
147149
"tags": tag_values_to_json(job.tag_values) if job.tag_values else [],
150+
"last_run": None,
148151
}
149152
for job in [dag_job, task_job]
150153
],
@@ -177,6 +180,7 @@ async def test_get_jobs_by_location_type(
177180
"id": str(dag_job.id),
178181
"data": job_to_json(dag_job),
179182
"tags": tag_values_to_json(dag_job.tag_values) if dag_job.tag_values else [],
183+
"last_run": None,
180184
},
181185
],
182186
}

tests/test_server/test_jobs/test_get_jobs_by_tag_value.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ async def test_get_jobs_by_one_id_with_tags(
4747
"id": str(job.id),
4848
"data": job_to_json(job),
4949
"tags": tag_values_to_json(tag_values),
50+
"last_run": None,
5051
},
5152
],
5253
}
@@ -87,6 +88,7 @@ async def test_get_jobs_by_tag_value_id(
8788
"id": str(job.id),
8889
"data": job_to_json(job),
8990
"tags": tag_values_to_json(job.tag_values) if job.tag_values else [],
91+
"last_run": None,
9092
},
9193
],
9294
}
@@ -129,6 +131,7 @@ async def test_get_jobs_by_multiple_tag_value_ids(
129131
"id": str(wanted_job.id),
130132
"data": job_to_json(wanted_job),
131133
"tags": tag_values_to_json(wanted_job.tag_values),
134+
"last_run": None,
132135
},
133136
],
134137
}

tests/test_server/test_jobs/test_get_jobs_by_type.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ async def test_get_jobs_by_job_type(
5959
"id": str(job.id),
6060
"data": job_to_json(job),
6161
"tags": tag_values_to_json(job.tag_values) if job.tag_values else [],
62+
"last_run": None,
6263
}
6364
for job in (dag_job, task_job)
6465
],
@@ -91,6 +92,7 @@ async def test_get_jobs_by_job_type(
9192
"id": str(dag_job.id),
9293
"data": job_to_json(dag_job),
9394
"tags": tag_values_to_json(dag_job.tag_values) if dag_job.tag_values else [],
95+
"last_run": None,
9496
},
9597
],
9698
}

0 commit comments

Comments
 (0)