Runs export: a truncated one counts the selection into X-Total-Count; the SDK and fluksio export warn how many runs were left out
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
@@ -512,15 +512,22 @@ def _selected(
|
||||
ids: str,
|
||||
since: datetime | None,
|
||||
until: datetime | None,
|
||||
) -> list[Run]:
|
||||
"""The runs an export covers, newest first — the filters the list takes.
|
||||
) -> tuple[list[Run], int]:
|
||||
"""The runs an export covers, newest first, and how many the filters match.
|
||||
|
||||
Capped: the filters bound a *sensible* request and nothing bounded an
|
||||
unfiltered one, so asking for everything read every row of the table into
|
||||
memory before a byte was sent. A truncated export says so in a header.
|
||||
memory before a byte was sent. A truncated export says so in a header, and
|
||||
only then is the selection counted, since otherwise the rows are the count.
|
||||
"""
|
||||
statement = _selection(flow, status, group, ids, since, until)
|
||||
return list(session.exec(statement.limit(EXPORT_CAP + 1)))
|
||||
runs = list(session.exec(statement.limit(EXPORT_CAP + 1)))
|
||||
if len(runs) <= EXPORT_CAP:
|
||||
return runs, len(runs)
|
||||
total = session.exec(
|
||||
select(func.count()).select_from(statement.order_by(None).subquery())
|
||||
).one()
|
||||
return runs[:EXPORT_CAP], total
|
||||
|
||||
|
||||
def _cell(value: Any) -> Any:
|
||||
@@ -613,7 +620,7 @@ def _stream(
|
||||
name: str,
|
||||
columns: list[str],
|
||||
chunks: Iterator[list[dict[str, Any]]],
|
||||
truncated: bool = False,
|
||||
total: int = 0,
|
||||
) -> StreamingResponse:
|
||||
"""The rows out, a chunk at a time rather than a list.
|
||||
|
||||
@@ -629,11 +636,12 @@ def _stream(
|
||||
body = _csv(columns, chunks)
|
||||
media = "text/csv; charset=utf-8"
|
||||
headers = {"Content-Disposition": f'attachment; filename="{name}.{fmt}"'}
|
||||
if truncated:
|
||||
if total > EXPORT_CAP:
|
||||
# The body is otherwise indistinguishable from a complete one, and an
|
||||
# export quietly missing its oldest runs is the wrong thing to hand
|
||||
# somebody who is about to plot it.
|
||||
# somebody who is about to plot it. The total says how many it missed.
|
||||
headers["X-Truncated"] = str(EXPORT_CAP)
|
||||
headers["X-Total-Count"] = str(total)
|
||||
return StreamingResponse(body, media_type=media, headers=headers)
|
||||
|
||||
|
||||
@@ -656,9 +664,7 @@ def export_metrics(
|
||||
the metrics it lists; ``stride`` thins each curve — per series, so asking
|
||||
for every tenth point of two metrics gives every tenth point of both.
|
||||
"""
|
||||
runs = _selected(session, flow, status, group, ids, since, until)
|
||||
truncated = len(runs) > EXPORT_CAP
|
||||
runs = runs[:EXPORT_CAP]
|
||||
runs, total = _selected(session, flow, status, group, ids, since, until)
|
||||
wanted = [part for part in name.split(",") if part]
|
||||
step = max(1, stride)
|
||||
|
||||
@@ -682,7 +688,7 @@ def export_metrics(
|
||||
)
|
||||
yield rows
|
||||
|
||||
return _stream(format, "metrics", list(METRIC_COLUMNS), chunks(), truncated)
|
||||
return _stream(format, "metrics", list(METRIC_COLUMNS), chunks(), total)
|
||||
|
||||
|
||||
@router.get("/export/runs")
|
||||
@@ -710,9 +716,7 @@ def export_runs(
|
||||
three fields rather than two blobs, and the defaults reach the same
|
||||
depth.
|
||||
"""
|
||||
runs = _selected(session, flow, status, group, ids, since, until)
|
||||
truncated = len(runs) > EXPORT_CAP
|
||||
runs = runs[:EXPORT_CAP]
|
||||
runs, total = _selected(session, flow, status, group, ids, since, until)
|
||||
inputs = [part for part in params.split(",") if part] or sorted(
|
||||
{path for run in runs for path, _ in _leaves(run.params)}
|
||||
)
|
||||
@@ -730,7 +734,7 @@ def export_runs(
|
||||
row.update((f"metric.{k}", _cell(_dig(run.result, k))) for k in scores)
|
||||
yield [row]
|
||||
|
||||
return _stream(format, "runs", columns, chunks(), truncated)
|
||||
return _stream(format, "runs", columns, chunks(), total)
|
||||
|
||||
|
||||
@router.get("/metrics/names", response_model=list[str])
|
||||
|
||||
Reference in New Issue
Block a user