Upload 8 files
Browse files- src/bucket.py +112 -27
src/bucket.py
CHANGED
|
@@ -133,11 +133,56 @@ def write_json(path: str, payload: dict[str, Any], token: str | None = None) ->
|
|
| 133 |
|
| 134 |
|
| 135 |
def write_launch_metadata(run_id: str, *, bucket_source: str, payload: dict[str, Any], token: str | None = None) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
paths = RunPaths(run_id, bucket_source=bucket_source)
|
| 137 |
launch = dict(payload)
|
| 138 |
launch.setdefault("run_id", run_id)
|
| 139 |
launch.setdefault("bucket_source", bucket_source)
|
|
|
|
| 140 |
write_json(f"{paths.root}/launch.json", launch, token=token)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
|
| 143 |
def read_events(run_id: str, *, bucket_source: str, token: str | None = None) -> list[dict[str, Any]]:
|
|
@@ -234,25 +279,26 @@ def _list_run_files(
|
|
| 234 |
|
| 235 |
|
| 236 |
def summarize_run_bundle(run_id: str, bundle: dict[str, Any], *, bucket_source: str) -> dict[str, Any]:
|
|
|
|
| 237 |
state = bundle.get("state") or {}
|
| 238 |
launch = bundle.get("launch") or {}
|
| 239 |
gate = bundle.get("inference_gate") or {}
|
| 240 |
smoke = bundle.get("generation_smoke") or {}
|
| 241 |
hardware = bundle.get("hardware_strategy") or {}
|
| 242 |
-
target_space = state.get("target_space") or launch.get("target_space") or ""
|
| 243 |
-
status = state.get("status") or launch.get("status") or gate.get("status") or smoke.get("status") or "unknown"
|
| 244 |
return {
|
| 245 |
"run_id": run_id,
|
| 246 |
-
"kind": state.get("kind") or launch.get("kind") or "unknown",
|
| 247 |
"status": status,
|
| 248 |
-
"model_id": state.get("model_id") or launch.get("model_id") or state.get("model") or "",
|
| 249 |
"target_space": target_space,
|
| 250 |
-
"target_space_url": state.get("target_space_url") or launch.get("target_space_url") or (f"https://huggingface.co/spaces/{target_space}" if target_space else ""),
|
| 251 |
-
"job_id":
|
| 252 |
-
"job_url": _job_url_from_launch_or_state(launch=launch, state=state),
|
| 253 |
-
"created_at": state.get("created_at") or launch.get("created_at") or "",
|
| 254 |
-
"updated_at": state.get("updated_at") or state.get("created_at") or launch.get("updated_at") or launch.get("created_at") or "",
|
| 255 |
-
"selected_hardware": hardware.get("selected_hardware") or state.get("selected_hardware") or launch.get("preferred_space_hardware") or state.get("hardware") or "",
|
| 256 |
"manual_hardware_required": bool(gate.get("manual_hardware_required") or hardware.get("manual_action_required") or launch.get("manual_hardware_required")),
|
| 257 |
"health_passed": bool((gate.get("implementation_signals") or {}).get("health_passed") or smoke.get("health_passed")),
|
| 258 |
"smoke_test_passed": bool(smoke.get("ok") or smoke.get("status") == "success"),
|
|
@@ -271,6 +317,7 @@ def read_run_bundle(run_id: str, *, bucket_source: str, token: str | None = None
|
|
| 271 |
"events": paths.events,
|
| 272 |
"report": paths.report,
|
| 273 |
},
|
|
|
|
| 274 |
"launch": _safe_read_json(f"{paths.root}/launch.json", token=token),
|
| 275 |
"state": read_json(paths.state, token=token) or {},
|
| 276 |
"events": read_events(run_id, bucket_source=bucket_source, token=token),
|
|
@@ -292,6 +339,45 @@ def _run_id_from_path(path: str) -> str:
|
|
| 292 |
return path.rstrip('/').split('/')[-1]
|
| 293 |
|
| 294 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
def list_recent_runs(
|
| 296 |
*,
|
| 297 |
bucket_source: str,
|
|
@@ -302,35 +388,34 @@ def list_recent_runs(
|
|
| 302 |
) -> list[dict[str, Any]]:
|
| 303 |
"""List recent run summaries from a user's bucket.
|
| 304 |
|
| 305 |
-
Best effort:
|
| 306 |
-
|
|
|
|
| 307 |
"""
|
| 308 |
-
fs = _fs(token)
|
| 309 |
root = f"{bucket_uri_from_source(bucket_source)}/runs"
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
except FileNotFoundError:
|
| 313 |
-
return []
|
| 314 |
-
except Exception:
|
| 315 |
return []
|
| 316 |
|
| 317 |
runs: list[dict[str, Any]] = []
|
| 318 |
-
for
|
| 319 |
-
|
| 320 |
-
if not name:
|
| 321 |
-
continue
|
| 322 |
-
run_id = _run_id_from_path(name)
|
| 323 |
-
if run_id in {"runs", ""}:
|
| 324 |
-
continue
|
| 325 |
launch = read_json(f"{root}/{run_id}/launch.json", token=token) or {}
|
| 326 |
state = read_json(f"{root}/{run_id}/state.json", token=token) or {}
|
| 327 |
gate = read_json(f"{root}/{run_id}/inference_gate.json", token=token) or {}
|
| 328 |
smoke = read_json(f"{root}/{run_id}/tests/generation_smoke.json", token=token) or read_json(f"{root}/{run_id}/generation_smoke.json", token=token) or {}
|
| 329 |
hardware = read_json(f"{root}/{run_id}/hardware_strategy.json", token=token) or {}
|
| 330 |
-
partial_bundle = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
item = summarize_run_bundle(run_id, partial_bundle, bucket_source=bucket_source)
|
| 332 |
item["bucket_source"] = bucket_source
|
| 333 |
-
haystack = " ".join(str(item.get(k, "")) for k in ["run_id", "model_id", "target_space", "status", "kind"]).lower()
|
| 334 |
if query and query.lower() not in haystack:
|
| 335 |
continue
|
| 336 |
if status and status not in {"all", ""} and item["status"] != status:
|
|
|
|
| 133 |
|
| 134 |
|
| 135 |
def write_launch_metadata(run_id: str, *, bucket_source: str, payload: dict[str, Any], token: str | None = None) -> None:
|
| 136 |
+
"""Persist enough metadata immediately for the Run Explorer.
|
| 137 |
+
|
| 138 |
+
Workers can take time before writing state.json/events.jsonl. The custom UI
|
| 139 |
+
should still be able to show a just-launched run, open its Job, and resume
|
| 140 |
+
polling after a page refresh. Store launch.json plus a lightweight
|
| 141 |
+
summary.json and minimal state.json fallback. The worker may overwrite
|
| 142 |
+
state.json later with richer information.
|
| 143 |
+
"""
|
| 144 |
paths = RunPaths(run_id, bucket_source=bucket_source)
|
| 145 |
launch = dict(payload)
|
| 146 |
launch.setdefault("run_id", run_id)
|
| 147 |
launch.setdefault("bucket_source", bucket_source)
|
| 148 |
+
launch.setdefault("status", "running")
|
| 149 |
write_json(f"{paths.root}/launch.json", launch, token=token)
|
| 150 |
+
summary = {
|
| 151 |
+
"run_id": run_id,
|
| 152 |
+
"bucket_source": bucket_source,
|
| 153 |
+
"kind": launch.get("kind") or "unknown",
|
| 154 |
+
"status": launch.get("status") or "running",
|
| 155 |
+
"model_id": launch.get("model_id") or "",
|
| 156 |
+
"target_space": launch.get("target_space") or "",
|
| 157 |
+
"target_space_url": launch.get("target_space_url") or (f"https://huggingface.co/spaces/{launch.get('target_space')}" if launch.get("target_space") else ""),
|
| 158 |
+
"job_id": launch.get("job_id") or "",
|
| 159 |
+
"job_url": launch.get("job_url") or "",
|
| 160 |
+
"created_by": launch.get("created_by") or launch.get("username") or "",
|
| 161 |
+
"created_at": launch.get("created_at") or "",
|
| 162 |
+
"updated_at": launch.get("updated_at") or launch.get("created_at") or "",
|
| 163 |
+
"selected_hardware": launch.get("preferred_space_hardware") or launch.get("selected_hardware") or "",
|
| 164 |
+
"artifacts_url": f"https://huggingface.co/buckets/{bucket_source}/tree/main/runs/{run_id}",
|
| 165 |
+
}
|
| 166 |
+
write_json(f"{paths.root}/summary.json", summary, token=token)
|
| 167 |
+
# Minimal state fallback: progress polling can treat it as running until
|
| 168 |
+
# the worker writes a definitive state.json.
|
| 169 |
+
write_json(
|
| 170 |
+
f"{paths.root}/state.json",
|
| 171 |
+
{
|
| 172 |
+
"run_id": run_id,
|
| 173 |
+
"kind": summary["kind"],
|
| 174 |
+
"status": summary["status"],
|
| 175 |
+
"model_id": summary["model_id"],
|
| 176 |
+
"target_space": summary["target_space"],
|
| 177 |
+
"target_space_url": summary["target_space_url"],
|
| 178 |
+
"job_id": summary["job_id"],
|
| 179 |
+
"job_url": summary["job_url"],
|
| 180 |
+
"created_by": summary["created_by"],
|
| 181 |
+
"created_at": summary["created_at"],
|
| 182 |
+
"updated_at": summary["updated_at"],
|
| 183 |
+
},
|
| 184 |
+
token=token,
|
| 185 |
+
)
|
| 186 |
|
| 187 |
|
| 188 |
def read_events(run_id: str, *, bucket_source: str, token: str | None = None) -> list[dict[str, Any]]:
|
|
|
|
| 279 |
|
| 280 |
|
| 281 |
def summarize_run_bundle(run_id: str, bundle: dict[str, Any], *, bucket_source: str) -> dict[str, Any]:
|
| 282 |
+
summary_file = bundle.get("summary_file") or {}
|
| 283 |
state = bundle.get("state") or {}
|
| 284 |
launch = bundle.get("launch") or {}
|
| 285 |
gate = bundle.get("inference_gate") or {}
|
| 286 |
smoke = bundle.get("generation_smoke") or {}
|
| 287 |
hardware = bundle.get("hardware_strategy") or {}
|
| 288 |
+
target_space = state.get("target_space") or launch.get("target_space") or summary_file.get("target_space") or ""
|
| 289 |
+
status = state.get("status") or launch.get("status") or summary_file.get("status") or gate.get("status") or smoke.get("status") or "unknown"
|
| 290 |
return {
|
| 291 |
"run_id": run_id,
|
| 292 |
+
"kind": state.get("kind") or launch.get("kind") or summary_file.get("kind") or "unknown",
|
| 293 |
"status": status,
|
| 294 |
+
"model_id": state.get("model_id") or launch.get("model_id") or summary_file.get("model_id") or state.get("model") or "",
|
| 295 |
"target_space": target_space,
|
| 296 |
+
"target_space_url": state.get("target_space_url") or launch.get("target_space_url") or summary_file.get("target_space_url") or (f"https://huggingface.co/spaces/{target_space}" if target_space else ""),
|
| 297 |
+
"job_id": state.get("job_id") or launch.get("job_id") or summary_file.get("job_id") or "",
|
| 298 |
+
"job_url": state.get("job_url") or launch.get("job_url") or summary_file.get("job_url") or _job_url_from_launch_or_state(launch={**summary_file, **launch}, state=state),
|
| 299 |
+
"created_at": state.get("created_at") or launch.get("created_at") or summary_file.get("created_at") or "",
|
| 300 |
+
"updated_at": state.get("updated_at") or state.get("created_at") or launch.get("updated_at") or launch.get("created_at") or summary_file.get("updated_at") or summary_file.get("created_at") or "",
|
| 301 |
+
"selected_hardware": hardware.get("selected_hardware") or state.get("selected_hardware") or launch.get("preferred_space_hardware") or summary_file.get("selected_hardware") or state.get("hardware") or "",
|
| 302 |
"manual_hardware_required": bool(gate.get("manual_hardware_required") or hardware.get("manual_action_required") or launch.get("manual_hardware_required")),
|
| 303 |
"health_passed": bool((gate.get("implementation_signals") or {}).get("health_passed") or smoke.get("health_passed")),
|
| 304 |
"smoke_test_passed": bool(smoke.get("ok") or smoke.get("status") == "success"),
|
|
|
|
| 317 |
"events": paths.events,
|
| 318 |
"report": paths.report,
|
| 319 |
},
|
| 320 |
+
"summary_file": _safe_read_json(f"{paths.root}/summary.json", token=token),
|
| 321 |
"launch": _safe_read_json(f"{paths.root}/launch.json", token=token),
|
| 322 |
"state": read_json(paths.state, token=token) or {},
|
| 323 |
"events": read_events(run_id, bucket_source=bucket_source, token=token),
|
|
|
|
| 339 |
return path.rstrip('/').split('/')[-1]
|
| 340 |
|
| 341 |
|
| 342 |
+
def _discover_run_ids(root: str, *, token: str | None = None, limit: int = 300) -> list[str]:
|
| 343 |
+
"""Discover run ids in a bucket even when the run folder is partial.
|
| 344 |
+
|
| 345 |
+
HfFileSystem may expose object-store prefixes slightly differently between
|
| 346 |
+
Buckets/runtime versions. Use ls plus a few targeted globs so running Jobs
|
| 347 |
+
with only launch.json/summary.json are still shown in the Run Explorer.
|
| 348 |
+
"""
|
| 349 |
+
fs = _fs(token)
|
| 350 |
+
run_ids: set[str] = set()
|
| 351 |
+
|
| 352 |
+
def add_from_path(path: str) -> None:
|
| 353 |
+
if "/runs/" in path:
|
| 354 |
+
tail = path.split("/runs/", 1)[1]
|
| 355 |
+
else:
|
| 356 |
+
tail = path.replace(root.rstrip("/") + "/", "", 1)
|
| 357 |
+
run_id = tail.strip("/").split("/", 1)[0]
|
| 358 |
+
if run_id and run_id != "runs":
|
| 359 |
+
run_ids.add(run_id)
|
| 360 |
+
|
| 361 |
+
try:
|
| 362 |
+
for entry in fs.ls(root, detail=True):
|
| 363 |
+
name = entry.get("name") if isinstance(entry, dict) else str(entry)
|
| 364 |
+
if name:
|
| 365 |
+
add_from_path(name)
|
| 366 |
+
except Exception:
|
| 367 |
+
pass
|
| 368 |
+
|
| 369 |
+
for pattern in (f"{root}/*", f"{root}/*/summary.json", f"{root}/*/launch.json", f"{root}/*/state.json"):
|
| 370 |
+
try:
|
| 371 |
+
for path in fs.glob(pattern):
|
| 372 |
+
add_from_path(str(path))
|
| 373 |
+
if len(run_ids) >= limit:
|
| 374 |
+
break
|
| 375 |
+
except Exception:
|
| 376 |
+
continue
|
| 377 |
+
|
| 378 |
+
return sorted(run_ids, reverse=True)[:limit]
|
| 379 |
+
|
| 380 |
+
|
| 381 |
def list_recent_runs(
|
| 382 |
*,
|
| 383 |
bucket_source: str,
|
|
|
|
| 388 |
) -> list[dict[str, Any]]:
|
| 389 |
"""List recent run summaries from a user's bucket.
|
| 390 |
|
| 391 |
+
Best effort: a run can be visible as soon as launch.json/summary.json exists,
|
| 392 |
+
before the worker writes full state/events. This keeps running Jobs visible
|
| 393 |
+
and makes Job links available immediately.
|
| 394 |
"""
|
|
|
|
| 395 |
root = f"{bucket_uri_from_source(bucket_source)}/runs"
|
| 396 |
+
run_ids = _discover_run_ids(root, token=token)
|
| 397 |
+
if not run_ids:
|
|
|
|
|
|
|
|
|
|
| 398 |
return []
|
| 399 |
|
| 400 |
runs: list[dict[str, Any]] = []
|
| 401 |
+
for run_id in run_ids:
|
| 402 |
+
summary_file = read_json(f"{root}/{run_id}/summary.json", token=token) or {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
launch = read_json(f"{root}/{run_id}/launch.json", token=token) or {}
|
| 404 |
state = read_json(f"{root}/{run_id}/state.json", token=token) or {}
|
| 405 |
gate = read_json(f"{root}/{run_id}/inference_gate.json", token=token) or {}
|
| 406 |
smoke = read_json(f"{root}/{run_id}/tests/generation_smoke.json", token=token) or read_json(f"{root}/{run_id}/generation_smoke.json", token=token) or {}
|
| 407 |
hardware = read_json(f"{root}/{run_id}/hardware_strategy.json", token=token) or {}
|
| 408 |
+
partial_bundle = {
|
| 409 |
+
"summary_file": summary_file,
|
| 410 |
+
"launch": launch,
|
| 411 |
+
"state": state,
|
| 412 |
+
"inference_gate": gate,
|
| 413 |
+
"generation_smoke": smoke,
|
| 414 |
+
"hardware_strategy": hardware,
|
| 415 |
+
}
|
| 416 |
item = summarize_run_bundle(run_id, partial_bundle, bucket_source=bucket_source)
|
| 417 |
item["bucket_source"] = bucket_source
|
| 418 |
+
haystack = " ".join(str(item.get(k, "")) for k in ["run_id", "model_id", "target_space", "status", "kind", "job_id"]).lower()
|
| 419 |
if query and query.lower() not in haystack:
|
| 420 |
continue
|
| 421 |
if status and status not in {"all", ""} and item["status"] != status:
|