Count runs per flow, and page the run list by offset

This commit is contained in:
2026-08-25 11:34:07 +02:00
parent 60757fa7fa
commit 7e422c0047
5 changed files with 139 additions and 3 deletions
+42 -1
View File
@@ -10,6 +10,8 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel, Field
from sqlalchemy import func
from sqlalchemy import select as sa_select
from sqlmodel import col, select
from fluksio.api.deps import CurrentUser, SessionDep, get_current_user
@@ -98,6 +100,16 @@ class RunDetail(RunRow):
artifacts: list[ArtifactRow] = Field(default_factory=list)
class FlowRunsRow(BaseModel):
"""How much a flow has been run, for the screen's list of flows."""
flow: str
runs: int
running: int
queued: int
last_created_at: Any = None
class MetricPoint(BaseModel):
step: int
ts: float
@@ -197,6 +209,7 @@ def read_runs(
group: str | None = None,
digest: str | None = None,
limit: int = 50,
offset: int = 0,
) -> Any:
"""Runs, newest first. The queryable table an experiment log needs."""
statement = select(Run).order_by(col(Run.created_at).desc())
@@ -208,7 +221,35 @@ def read_runs(
statement = statement.where(col(Run.group_id) == group)
if digest:
statement = statement.where(col(Run.params_digest) == digest)
return list(session.exec(statement.limit(min(limit, 500))))
statement = statement.offset(max(0, offset)).limit(min(limit, 500))
return list(session.exec(statement))
@router.get("/overview", response_model=list[FlowRunsRow])
def read_overview(session: SessionDep) -> Any:
"""One row per flow that has ever run, busiest-recent first.
The list caps at 500 newest runs, so counting flows on the client goes
wrong the moment a history outgrows one page. The database counts instead.
"""
statement = sa_select(
col(Run.flow),
col(Run.status),
func.count(col(Run.id)),
func.max(col(Run.created_at)),
).group_by(col(Run.flow), col(Run.status))
rows: dict[str, FlowRunsRow] = {}
for flow, status, count, latest in session.execute(statement):
row = rows.setdefault(flow, FlowRunsRow(flow=flow, runs=0, running=0, queued=0))
row.runs += count
if status == "running":
row.running += count
elif status == "queued":
row.queued += count
if row.last_created_at is None or latest > row.last_created_at:
row.last_created_at = latest
return sorted(rows.values(), key=lambda row: row.last_created_at, reverse=True)
@router.get("/{run_id}", response_model=RunDetail)
+35
View File
@@ -10,6 +10,7 @@ from datetime import UTC, datetime
import pytest
from sqlmodel import Session
from fluksio.core.config import settings
from fluksio.core.db import engine as db_engine
from fluksio.flow.artifacts import ArtifactStore
from fluksio.flow.messages import DType, MessageSpec
@@ -196,3 +197,37 @@ def test_a_reference_passed_whole_is_left_alone(made_artifact):
assert resolve_references(artifact_flow(), {"dataset": reference}) == {
"dataset": reference
}
def test_the_overview_counts_a_flow_the_list_page_would_not_reach(
client, superuser_token_headers
):
"""The list caps at 500 newest; the flow rail needs whole counts."""
with Session(db_engine) as session:
for index in range(3):
session.add(
Run(
id=f"ov-{index}",
flow="overviewed",
status="ok" if index else "running",
created_at=datetime(2026, 1, 1 + index, tzinfo=UTC),
)
)
session.commit()
rows = client.get(
f"{settings.API_V1_STR}/runs/overview", headers=superuser_token_headers
).json()
row = next(r for r in rows if r["flow"] == "overviewed")
assert (row["runs"], row["running"], row["queued"]) == (3, 1, 0)
def test_overview_is_not_read_as_a_run_id(client, superuser_token_headers):
"""`/overview` is declared before `/{run_id}`, which would swallow it."""
answer = client.get(
f"{settings.API_V1_STR}/runs/overview", headers=superuser_token_headers
)
assert answer.status_code == 200
assert isinstance(answer.json(), list)