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:
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user