Count runs per flow, and page the run list by offset
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user