Export runs and their curves as tables an analysis reads
Docs / docs (push) Successful in 35s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m11s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m17s
pre-commit / pre-commit (push) Failing after 2m44s
Test Backend / test-backend (push) Successful in 3m0s
Compose Smoke Test / test-compose (push) Successful in 41s
Playwright Tests / merge-reports (push) Successful in 8m14s
Docs / docs (push) Successful in 35s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m11s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m17s
pre-commit / pre-commit (push) Failing after 2m44s
Test Backend / test-backend (push) Successful in 3m0s
Compose Smoke Test / test-compose (push) Successful in 41s
Playwright Tests / merge-reports (push) Successful in 8m14s
`fluksio export metrics` is the long table — a row per run, metric and step —
and `fluksio export runs` the wide one, a row per run with the inputs that
*vary* across the selection as columns beside its final numbers, status,
duration and the commit and digest of the code it ran. Both carry the run id
on every row, which is the join back to the run page and what makes an
exported file auditable. `Client.export_metrics`/`export_runs` answer the same
rows to a notebook.
The engine streams csv or jsonl from two routes declared above `/{run_id}`;
parquet is a client-side conversion behind the new `fluksio[parquet]` extra,
so nobody pays for pyarrow who does not want dtypes kept. The long export
reads each run through `_series`, so a cached node's curve comes with it, and
`--stride` thins each series rather than the concatenation of all of them.
Two things they needed on the way: `GET /runs` takes `?since=` and `?before=`,
so a long history pages by the last row's own timestamp instead of an offset
that shifts under it; and a read that reaches no engine now says so in half a
second rather than seven, because `runs`, `flavors`, `export` and an unwatched
`status` pass `retries=0`. Everything that submits keeps them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9Hdrmf2cwNABCnE5x9UJa
This commit is contained in:
@@ -397,6 +397,75 @@ class Client:
|
||||
params={"ids": ",".join(ids), "metric": metric},
|
||||
)
|
||||
|
||||
def export_metrics(
|
||||
self,
|
||||
flow: str = "",
|
||||
ids: Iterable[str] = (),
|
||||
name: str = "",
|
||||
stride: int = 1,
|
||||
**filters: Any,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Runs' series as long rows: run, name, step, ts, value.
|
||||
|
||||
``pd.DataFrame(client.export_metrics(flow="train"))`` is every loss
|
||||
curve of that flow with the run id on each row. ``name`` keeps the
|
||||
metrics it lists, ``stride`` thins each curve, and the selection is
|
||||
narrowed the way :meth:`runs` is — ``status=``, ``group=``,
|
||||
``since=``, ``until=``.
|
||||
"""
|
||||
query: dict[str, Any] = {"stride": stride}
|
||||
if name:
|
||||
query["name"] = name
|
||||
return self._export("/runs/export/metrics", flow, ids, query, filters)
|
||||
|
||||
def export_runs(
|
||||
self,
|
||||
flow: str = "",
|
||||
ids: Iterable[str] = (),
|
||||
params: str = "",
|
||||
metrics: str = "",
|
||||
**filters: Any,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""One row per run: its inputs as columns, its final numbers, its code.
|
||||
|
||||
The arm-comparison table. The inputs kept are the ones that vary
|
||||
across the selection unless ``params`` names them, which is the axis a
|
||||
sweep is read along.
|
||||
"""
|
||||
query: dict[str, Any] = {}
|
||||
if params:
|
||||
query["params"] = params
|
||||
if metrics:
|
||||
query["metrics"] = metrics
|
||||
return self._export("/runs/export/runs", flow, ids, query, filters)
|
||||
|
||||
def _export(
|
||||
self,
|
||||
path: str,
|
||||
flow: str,
|
||||
ids: Iterable[str],
|
||||
query: dict[str, Any],
|
||||
filters: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""An export as parsed rows.
|
||||
|
||||
jsonl on the wire rather than csv: it keeps the types the engine has,
|
||||
and it is the format the CLI converts to parquet from. Read whole —
|
||||
the streaming is the engine's side, and a notebook wants a list.
|
||||
"""
|
||||
if flow:
|
||||
query["flow"] = flow
|
||||
named = ",".join(ids)
|
||||
if named:
|
||||
query["ids"] = named
|
||||
for key, value in filters.items():
|
||||
query[key] = value.isoformat() if hasattr(value, "isoformat") else value
|
||||
query["format"] = "jsonl"
|
||||
response = self._request("GET", path, idempotent=True, params=query)
|
||||
if response.status_code >= 400:
|
||||
raise ApiError(response.status_code, _detail(response))
|
||||
return [json.loads(line) for line in response.text.splitlines() if line]
|
||||
|
||||
def cancel(self, run_id: str) -> Any:
|
||||
# Cancelling a cancelled run is cancelled.
|
||||
return self._call("POST", f"/runs/{run_id}/cancel", idempotent=True)
|
||||
|
||||
Reference in New Issue
Block a user