Add POST /runs/import for runs executed elsewhere

A history recorded before the move (MLflow experiments) had nowhere to
go, since runs were only made by running a flow. Each entry is stored as
it ended: parameters completed and checked like a submit, cause
"import", empty commit and code digest, metrics and artifacts written
as given, nothing queued or announced. Run.external_id (new migration)
makes a second import a no-op and is an export column so a row joins
back. The request is all or nothing and capped at 200 entries;
Client.import_runs pages a longer history.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QgG6TJw5qBZzaLghQSRVrq
This commit is contained in:
2026-09-16 17:01:07 +02:00
co-authored by Claude Opus 5
parent eeaa46d4ae
commit d9630f66f5
12 changed files with 848 additions and 32 deletions
+129
View File
@@ -4,6 +4,7 @@ The pipeline half — what a hit restores and what a key is made of — is in
`tests/flow/test_runs.py`, which runs without one.
"""
import base64
import csv
import json
from datetime import UTC, datetime, timedelta
@@ -494,6 +495,134 @@ def test_the_seed_is_recorded_the_same_way_however_it_arrived():
session.commit()
def test_a_run_made_elsewhere_is_imported_as_it_ended(tmp_path):
"""History from before the move, completed like a submit and borrowing
nothing from today: no commit, no code digest, nothing queued."""
flow = FlowDef(
name="history",
mode="batch",
inputs=[
FlowInput(spec=MessageSpec(name="lr", dtype=DType.FLOAT), initial=0.01),
FlowInput(spec=MessageSpec(name="epochs", dtype=DType.INT)),
],
)
store = ArtifactStore(tmp_path)
queue = _Collect()
service = RunService(controller=_OneFlow(flow), queue=queue, artifacts=store)
started = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC)
entry = {
"flow": "history",
"external_id": "mlflow:1/abc",
"params": {"epochs": 3},
"seed": 1001,
"status": "ok",
"created_at": started,
"started_at": started,
"finished_at": started + timedelta(seconds=90),
"result": {"final": {"mse": 7.0}},
"metrics": [
{"name": "history.mse", "step": 0, "ts": 1.0, "value": 9.0},
{"name": "history.mse", "step": 1, "ts": 2.0, "value": float("nan")},
{"name": "history.mse", "step": 1, "ts": 3.0, "value": 7.0},
],
"artifacts": [
{
"name": "model",
"filename": "model.txt",
"media_type": "text/plain",
"content_base64": base64.b64encode(b"weights").decode(),
}
],
"labels": ["imported"],
"group_id": "mlflow:1",
}
made: list[str] = []
try:
[answer] = service.import_runs([entry], actor="me@example.com")
made.append(answer["id"])
assert answer["created"] is True
with Session(db_engine) as session:
run = session.get(Run, answer["id"])
assert (run.cause, run.status, run.external_id) == (
"import",
"ok",
"mlflow:1/abc",
)
# The declared default is folded in, as a submit would have.
assert run.params == {"lr": 0.01, "epochs": 3}
assert run.params_digest
assert run.duration_ms == 90_000
assert (run.commit, run.origin_commit, run.code_digest) == ("", "", "")
points = session.exec(
select(RunMetric).where(col(RunMetric.run_id) == run.id)
).all()
assert sorted((point.step, point.value) for point in points) == [
(0, 9.0),
(1, 7.0),
]
[artifact] = session.exec(
select(RunArtifact).where(col(RunArtifact.run_id) == run.id)
).all()
assert artifact.filename == "model.txt"
assert b"".join(store.read(artifact.digest)) == b"weights"
assert queue.items == []
# The same history again adds nothing, and says so.
assert service.import_runs([entry]) == [{**answer, "created": False}]
# One refused entry takes the good one beside it down with it.
good = {**entry, "external_id": "mlflow:1/def", "artifacts": []}
bad = {**good, "external_id": "mlflow:1/ghi", "params": {"epochs": "three"}}
with pytest.raises(RunRejected, match="mlflow:1/ghi"):
service.import_runs([good, bad])
assert service._by_external("mlflow:1/def") is None
finally:
with Session(db_engine) as session:
for table in (RunMetric, RunArtifact, Run):
column = Run.id if table is Run else table.run_id
for row in session.exec(select(table).where(col(column).in_(made))):
session.delete(row)
session.commit()
def test_an_import_answers_what_it_added(client, superuser_token_headers, monkeypatch):
"""So a client can report what it actually added, and pages the rest."""
seen: dict[str, object] = {}
class Recorder:
def import_runs(self, entries, actor=""):
seen.update(entries=entries, actor=actor)
return [
{
"external_id": one["external_id"],
"id": f"run-{index}",
"created": True,
}
for index, one in enumerate(entries)
]
monkeypatch.setattr(client.app.state, "run_service", Recorder())
url = f"{settings.API_V1_STR}/runs/import"
entry = {
"flow": "history",
"external_id": "mlflow:1/abc",
"created_at": "2026-01-02T03:04:05Z",
}
answer = client.post(url, headers=superuser_token_headers, json={"runs": [entry]})
assert answer.status_code == 202
assert answer.json() == [
{"external_id": "mlflow:1/abc", "id": "run-0", "created": True}
]
assert seen["entries"][0]["status"] == "ok"
assert seen["actor"] == settings.FIRST_SUPERUSER
too_many = client.post(
url, headers=superuser_token_headers, json={"runs": [entry] * 201}
)
assert too_many.status_code == 422
DRAFT_SOURCE = "def process():\n return {'answer': 'draft'}\n"
PUBLISHED_SOURCE = "def process():\n return {'answer': 'published'}\n"