"""Single source for JobRunState reads/writes. Mirrors ``settings_store``: ``record_finish`` never commits — the caller owns the transaction — and reads are batched so the admin listing stays one query. """ from __future__ import annotations import logging from collections.abc import Iterable from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.job_run_state import JobRunState logger = logging.getLogger(__name__) def _as_datetime(value: object) -> datetime | None: """Runtime snapshots carry ISO strings; the column wants a datetime.""" if isinstance(value, datetime): return value if isinstance(value, str) and value: try: return datetime.fromisoformat(value) except ValueError: return None return None async def get_map(db: AsyncSession, job_names: Iterable[str]) -> dict[str, JobRunState]: """Return {job_name: row} for the given jobs that have ever finished.""" result = await db.execute( select(JobRunState).where(JobRunState.job_name.in_(list(job_names))) ) return {row.job_name: row for row in result.scalars().all()} async def record_finish(db: AsyncSession, job_name: str, runtime: dict) -> JobRunState: """Upsert the last-run row from a scheduler runtime snapshot. Select-then-update-or-insert rather than a dialect-specific upsert, matching ``settings_store.upsert_setting`` — tests run on SQLite, prod on Postgres. """ existing = await db.execute( select(JobRunState).where(JobRunState.job_name == job_name) ) row = existing.scalar_one_or_none() finished_at = _as_datetime(runtime.get("finished_at")) or datetime.now(timezone.utc) status = str(runtime.get("status") or "completed") message = runtime.get("message") values = { "status": status, "started_at": _as_datetime(runtime.get("started_at")), "finished_at": finished_at, "processed": runtime.get("processed"), "total": runtime.get("total"), "message": str(message)[:4000] if message else None, } if row is None: row = JobRunState(job_name=job_name, **values) db.add(row) else: for field, value in values.items(): setattr(row, field, value) return row