fffiloni commited on
Commit
945f24f
·
verified ·
1 Parent(s): 63f374b

Upload 9 files

Browse files
Files changed (3) hide show
  1. src/jobs.py +12 -1
  2. src/progress.py +109 -76
  3. src/view_models.py +20 -52
src/jobs.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import re
4
  from typing import Any
5
 
6
- from huggingface_hub import Volume, fetch_job_logs, inspect_job, run_job
7
 
8
  from .config import bucket_uri_from_source, user_bucket_source, settings
9
  from .bucket import assert_user_bucket_ready
@@ -241,3 +241,14 @@ def fetch_recent_logs_safe(job_id: str, token: str | None = None, max_lines: int
241
  return "\n".join(str(line).rstrip("\n") for line in logs[-max_lines:])
242
  except Exception as exc: # noqa: BLE001
243
  return f"Could not fetch job logs: {exc}"
 
 
 
 
 
 
 
 
 
 
 
 
3
  import re
4
  from typing import Any
5
 
6
+ from huggingface_hub import Volume, cancel_job, fetch_job_logs, inspect_job, run_job
7
 
8
  from .config import bucket_uri_from_source, user_bucket_source, settings
9
  from .bucket import assert_user_bucket_ready
 
241
  return "\n".join(str(line).rstrip("\n") for line in logs[-max_lines:])
242
  except Exception as exc: # noqa: BLE001
243
  return f"Could not fetch job logs: {exc}"
244
+
245
+
246
+ def cancel_job_safe(job_id: str, *, namespace: str | None = None, token: str | None = None) -> dict[str, Any]:
247
+ """Cancel a running HF Job. Returns a JSON-safe status payload."""
248
+ if not job_id:
249
+ return {"ok": False, "error": "Missing job_id"}
250
+ try:
251
+ cancel_job(job_id=job_id, namespace=namespace, token=token)
252
+ return {"ok": True, "job_id": job_id, "status": "cancelled"}
253
+ except Exception as exc: # noqa: BLE001
254
+ return {"ok": False, "job_id": job_id, "error": str(exc)}
src/progress.py CHANGED
@@ -1,12 +1,12 @@
1
  from __future__ import annotations
2
 
3
- from dataclasses import dataclass
4
  from datetime import datetime, timezone
5
  from typing import Any
6
 
 
 
7
  STEP_ORDER = [
8
- "bucket_ready",
9
- "job_launched",
10
  "dependencies",
11
  "auth",
12
  "model_analysis",
@@ -15,80 +15,80 @@ STEP_ORDER = [
15
  "pi_install",
16
  "pi_config",
17
  "pi_run",
 
 
 
 
 
18
  "create_space",
 
19
  "upload_files",
20
- "hardware",
 
21
  "api_validation",
 
 
22
  "inference_gate",
23
  "report_write",
24
  "done",
 
25
  ]
26
 
27
  STEP_LABELS = {
28
- "bucket_ready": "Bucket ready",
29
- "job_launched": "Job launched",
30
  "dependencies": "Dependencies",
31
- "auth": "Authenticated",
32
  "model_analysis": "Model analysis",
33
  "workspace": "Workspace",
34
  "node": "Node/npm",
35
- "pi_install": "Pi installed",
36
- "pi_config": "Pi configured",
37
- "pi_run": "Pi running",
 
 
 
 
 
38
  "create_space": "Create Space",
 
39
  "upload_files": "Upload files",
40
- "hardware": "Hardware request",
 
41
  "api_validation": "API validation",
42
- "inference_gate": "Gate",
 
 
43
  "report_write": "Report",
44
  "done": "Done",
45
- }
46
-
47
- STEP_PROGRESS = {
48
- "bucket_ready": 5,
49
- "job_launched": 8,
50
- "dependencies": 12,
51
- "auth": 16,
52
- "model_analysis": 22,
53
- "workspace": 28,
54
- "node": 34,
55
- "pi_install": 40,
56
- "pi_config": 44,
57
- "pi_run": 62,
58
- "create_space": 72,
59
- "upload_files": 80,
60
- "hardware": 86,
61
- "api_validation": 92,
62
- "inference_gate": 96,
63
- "report_write": 98,
64
- "done": 100,
65
  }
66
 
67
  STEP_ALIASES = {
68
- "bootstrap": "job_launched",
69
- "dependencies": "dependencies",
70
- "auth": "auth",
71
- "model_analysis": "model_analysis",
72
- "workspace": "workspace",
73
- "node": "node",
74
- "pi_install": "pi_install",
75
- "pi_config": "pi_config",
76
- "pi_run": "pi_run",
77
- "create_space": "create_space",
78
- "upload_files": "upload_files",
79
- "hardware_preferred": "hardware",
80
- "hardware_fallback": "hardware",
81
- "hardware": "hardware",
82
- "api_validation": "api_validation",
83
- "inference_gate": "inference_gate",
84
- "report_write": "report_write",
85
- "done": "done",
86
- "failure": "done",
87
  }
88
 
89
- DONE_STATUSES = {"success", "done", "completed", "passed", "full_inference_success", "full_inference_candidate_health_passed", "manual_hardware_required", "technical_blocker"}
90
- RUNNING_STATUSES = {"started", "running", "waiting"}
91
- FAILED_STATUSES = {"failed", "error"}
 
 
 
 
 
 
 
 
 
 
 
92
 
93
 
94
  def _parse_ts(ts: str | None) -> datetime | None:
@@ -103,19 +103,38 @@ def _parse_ts(ts: str | None) -> datetime | None:
103
  def _canonical_step(step: str | None) -> str | None:
104
  if not step:
105
  return None
106
- return STEP_ALIASES.get(step, step if step in STEP_PROGRESS else None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
 
109
  def progress_from_events(events: list[dict[str, Any]] | None, *, state: dict[str, Any] | None = None) -> dict[str, Any]:
110
- """Build a stable UI progress model from worker events.jsonl + optional state.json."""
 
 
 
 
111
  events = events or []
112
  state = state or {}
113
  step_status = {step: "pending" for step in STEP_ORDER}
114
- last_event = None
115
- current_step = "job_launched"
116
  terminal_status = None
117
  first_ts = None
118
  last_ts = None
 
119
 
120
  for event in events:
121
  if not isinstance(event, dict):
@@ -130,43 +149,57 @@ def progress_from_events(events: list[dict[str, Any]] | None, *, state: dict[str
130
  last_ts = ts
131
  if not step:
132
  continue
 
133
  current_step = step
 
 
 
134
  if status in FAILED_STATUSES:
135
  step_status[step] = "failed"
136
  terminal_status = "failed"
 
 
 
137
  elif status in RUNNING_STATUSES:
138
- if step_status.get(step) != "done":
139
  step_status[step] = "running"
140
  elif status in DONE_STATUSES or status:
141
  step_status[step] = "done"
142
 
143
- if step == "done" and status:
144
  terminal_status = status
145
 
146
- # Mark all previous steps as done up to the current running/done step.
147
- current_index = STEP_ORDER.index(current_step) if current_step in STEP_ORDER else 0
148
- for step in STEP_ORDER[:current_index]:
149
- if step_status[step] == "pending":
150
- step_status[step] = "done"
 
 
151
 
152
  if terminal_status and terminal_status != "failed":
153
- current_step = "done"
154
- for step in STEP_ORDER:
155
- if step_status[step] != "failed":
156
- step_status[step] = "done"
 
 
 
 
 
 
 
157
 
158
- status_from_state = state.get("status") or state.get("gate_status")
159
  overall_status = terminal_status or status_from_state or ("running" if events else "not_started")
160
 
161
- if overall_status in {"failed", "error"}:
162
- progress = max(STEP_PROGRESS.get(current_step, 8), 8)
 
 
163
  elif current_step == "done" or overall_status in DONE_STATUSES:
164
  progress = 100
165
  else:
166
- progress = STEP_PROGRESS.get(current_step, 8)
167
- if step_status.get(current_step) == "running":
168
- previous = STEP_ORDER[max(0, current_index - 1)] if current_index > 0 else current_step
169
- progress = max(STEP_PROGRESS.get(previous, 0) + 2, progress - 8)
170
 
171
  now = datetime.now(timezone.utc)
172
  elapsed = int(((last_ts or now) - first_ts).total_seconds()) if first_ts else 0
 
1
  from __future__ import annotations
2
 
 
3
  from datetime import datetime, timezone
4
  from typing import Any
5
 
6
+ # Exact worker-facing steps emitted by src/worker_payload.py.
7
+ # Keep this list close to the Job logs/events rather than a marketing pipeline.
8
  STEP_ORDER = [
9
+ "bootstrap",
 
10
  "dependencies",
11
  "auth",
12
  "model_analysis",
 
15
  "pi_install",
16
  "pi_config",
17
  "pi_run",
18
+ "pi_verification",
19
+ "metadata_sanitize",
20
+ "requirements_sanitize",
21
+ "hardware_strategy",
22
+ "create_space_hardware",
23
  "create_space",
24
+ "repair",
25
  "upload_files",
26
+ "space_runtime",
27
+ "space_logs",
28
  "api_validation",
29
+ "live_wait",
30
+ "generation_smoke",
31
  "inference_gate",
32
  "report_write",
33
  "done",
34
+ "failure",
35
  ]
36
 
37
  STEP_LABELS = {
38
+ "bootstrap": "Bootstrap",
 
39
  "dependencies": "Dependencies",
40
+ "auth": "Auth",
41
  "model_analysis": "Model analysis",
42
  "workspace": "Workspace",
43
  "node": "Node/npm",
44
+ "pi_install": "Pi install",
45
+ "pi_config": "Pi config",
46
+ "pi_run": "Pi run",
47
+ "pi_verification": "Pi verification",
48
+ "metadata_sanitize": "Metadata sanitize",
49
+ "requirements_sanitize": "Requirements sanitize",
50
+ "hardware_strategy": "Hardware strategy",
51
+ "create_space_hardware": "Create with hardware",
52
  "create_space": "Create Space",
53
+ "repair": "Repair pass",
54
  "upload_files": "Upload files",
55
+ "space_runtime": "Space runtime",
56
+ "space_logs": "Space logs",
57
  "api_validation": "API validation",
58
+ "live_wait": "Live wait",
59
+ "generation_smoke": "Generation smoke",
60
+ "inference_gate": "Inference gate",
61
  "report_write": "Report",
62
  "done": "Done",
63
+ "failure": "Failure",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
 
66
  STEP_ALIASES = {
67
+ "bucket_ready": "bootstrap",
68
+ "job_launched": "bootstrap",
69
+ "hardware_preferred": "hardware_strategy",
70
+ "hardware_fallback": "hardware_strategy",
71
+ "hardware": "hardware_strategy",
72
+ "build": "space_runtime",
73
+ "diagnose": "repair",
74
+ "diagnosis": "repair",
75
+ "patch": "repair",
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
+ DONE_STATUSES = {
79
+ "success",
80
+ "done",
81
+ "completed",
82
+ "passed",
83
+ "full_inference_success",
84
+ "full_inference_candidate_health_passed",
85
+ "manual_hardware_required",
86
+ "technical_blocker",
87
+ "health_only",
88
+ }
89
+ RUNNING_STATUSES = {"started", "running", "waiting", "pending", "scheduled"}
90
+ FAILED_STATUSES = {"failed", "error", "failure"}
91
+ CANCELLED_STATUSES = {"cancelled", "canceled"}
92
 
93
 
94
  def _parse_ts(ts: str | None) -> datetime | None:
 
103
  def _canonical_step(step: str | None) -> str | None:
104
  if not step:
105
  return None
106
+ value = str(step)
107
+ return STEP_ALIASES.get(value, value if value in STEP_ORDER else None)
108
+
109
+
110
+ def _is_terminal_status(status: str | None) -> bool:
111
+ s = str(status or "").lower()
112
+ return s in DONE_STATUSES or s in FAILED_STATUSES or s in CANCELLED_STATUSES
113
+
114
+
115
+ def _progress_for_index(index: int) -> int:
116
+ if index <= 0:
117
+ return 3
118
+ if len(STEP_ORDER) <= 1:
119
+ return 0
120
+ return int(round((index / (len(STEP_ORDER) - 1)) * 100))
121
 
122
 
123
  def progress_from_events(events: list[dict[str, Any]] | None, *, state: dict[str, Any] | None = None) -> dict[str, Any]:
124
+ """Build a live progress model from the actual worker events.
125
+
126
+ Unlike earlier product pipelines, this keeps the worker's real step names.
127
+ Missing steps stay pending; they are not automatically marked done.
128
+ """
129
  events = events or []
130
  state = state or {}
131
  step_status = {step: "pending" for step in STEP_ORDER}
132
+ last_event: dict[str, Any] | None = None
133
+ current_step = "bootstrap"
134
  terminal_status = None
135
  first_ts = None
136
  last_ts = None
137
+ observed_steps: list[str] = []
138
 
139
  for event in events:
140
  if not isinstance(event, dict):
 
149
  last_ts = ts
150
  if not step:
151
  continue
152
+
153
  current_step = step
154
+ if step not in observed_steps:
155
+ observed_steps.append(step)
156
+
157
  if status in FAILED_STATUSES:
158
  step_status[step] = "failed"
159
  terminal_status = "failed"
160
+ elif status in CANCELLED_STATUSES:
161
+ step_status[step] = "blocked"
162
+ terminal_status = "cancelled"
163
  elif status in RUNNING_STATUSES:
164
+ if step_status.get(step) not in {"done", "failed"}:
165
  step_status[step] = "running"
166
  elif status in DONE_STATUSES or status:
167
  step_status[step] = "done"
168
 
169
+ if step in {"done", "failure"} and status:
170
  terminal_status = status
171
 
172
+ status_from_state = str(state.get("status") or state.get("gate_status") or "").lower()
173
+ if status_from_state in CANCELLED_STATUSES:
174
+ terminal_status = "cancelled"
175
+ elif status_from_state in FAILED_STATUSES:
176
+ terminal_status = "failed"
177
+ elif status_from_state in DONE_STATUSES:
178
+ terminal_status = status_from_state
179
 
180
  if terminal_status and terminal_status != "failed":
181
+ if terminal_status in CANCELLED_STATUSES:
182
+ current_step = current_step or "failure"
183
+ elif current_step != "failure":
184
+ current_step = "done"
185
+
186
+ if observed_steps:
187
+ current_index = max(STEP_ORDER.index(s) for s in observed_steps if s in STEP_ORDER)
188
+ else:
189
+ current_index = 0
190
+ if current_step in STEP_ORDER:
191
+ current_index = max(current_index, STEP_ORDER.index(current_step))
192
 
 
193
  overall_status = terminal_status or status_from_state or ("running" if events else "not_started")
194
 
195
+ if overall_status in FAILED_STATUSES:
196
+ progress = max(_progress_for_index(current_index), 3)
197
+ elif overall_status in CANCELLED_STATUSES:
198
+ progress = max(_progress_for_index(current_index), 3)
199
  elif current_step == "done" or overall_status in DONE_STATUSES:
200
  progress = 100
201
  else:
202
+ progress = max(3, min(99, _progress_for_index(current_index)))
 
 
 
203
 
204
  now = datetime.now(timezone.utc)
205
  elapsed = int(((last_ts or now) - first_ts).total_seconds()) if first_ts else 0
src/view_models.py CHANGED
@@ -3,17 +3,9 @@ from __future__ import annotations
3
  from datetime import datetime, timedelta, timezone
4
  from typing import Any
5
 
6
- PRODUCT_STEPS = [
7
- {"id": "bucket_ready", "label": "Bucket ready"},
8
- {"id": "job_launched", "label": "Job launched"},
9
- {"id": "model_analysis", "label": "Model analysis"},
10
- {"id": "generate_app", "label": "Generate app"},
11
- {"id": "create_space", "label": "Create Space"},
12
- {"id": "upload_files", "label": "Upload files"},
13
- {"id": "hardware", "label": "Hardware"},
14
- {"id": "live_validation", "label": "Live validation"},
15
- {"id": "report", "label": "Report"},
16
- ]
17
 
18
  TERMINAL_GLOBAL_STATUSES = {"succeeded", "failed", "cancelled", "blocked", "waiting_manual_action"}
19
  SUCCESS_RAW_STATUSES = {
@@ -32,40 +24,11 @@ MANUAL_RAW_STATUSES = {
32
  "manual_action_required",
33
  }
34
  FAILED_RAW_STATUSES = {"failed", "failure", "error", "repair_failed"}
 
35
  BLOCKED_RAW_STATUSES = {"technical_blocker", "blocked", "health_only"}
36
  RUNNING_RAW_STATUSES = {"started", "running", "waiting", "queued", "pending", "scheduled"}
37
 
38
- STEP_TO_PHASE = {
39
- "bucket_ready": "bucket_ready",
40
- "job_launched": "job_launched",
41
- "bootstrap": "job_launched",
42
- "dependencies": "job_launched",
43
- "auth": "job_launched",
44
- "model_analysis": "model_analysis",
45
- "workspace": "generate_app",
46
- "node": "generate_app",
47
- "pi_install": "generate_app",
48
- "pi_config": "generate_app",
49
- "pi_run": "generate_app",
50
- "generate_app": "generate_app",
51
- "create_space": "create_space",
52
- "upload_files": "upload_files",
53
- "hardware_preferred": "hardware",
54
- "hardware_fallback": "hardware",
55
- "hardware": "hardware",
56
- "build": "hardware",
57
- "space_build": "hardware",
58
- "diagnose": "hardware",
59
- "diagnosis": "hardware",
60
- "repair": "hardware",
61
- "patch": "hardware",
62
- "api_validation": "live_validation",
63
- "generation_smoke": "live_validation",
64
- "inference_gate": "live_validation",
65
- "report_write": "report",
66
- "done": "report",
67
- "failure": "report",
68
- }
69
 
70
 
71
  def parse_ts(value: Any) -> datetime | None:
@@ -199,7 +162,10 @@ def normalize_run_status(
199
  manual = requires_manual_action(bundle)
200
  blocker = has_technical_blocker(bundle)
201
 
202
- if manual:
 
 
 
203
  global_status = "waiting_manual_action"
204
  verdict = "manual_action_required"
205
  elif statuses.intersection(SUCCESS_RAW_STATUSES):
@@ -246,10 +212,12 @@ def normalize_run_status(
246
 
247
 
248
  def derive_product_phase(bundle: dict[str, Any], status_model: dict[str, Any]) -> str:
249
- if status_model["global_status"] in {"succeeded", "failed", "blocked", "stale"}:
250
- return "report"
 
 
251
  if status_model["requires_manual_action"]:
252
- return "hardware"
253
 
254
  events = [e for e in (bundle.get("events") or []) if isinstance(e, dict)]
255
  for event in reversed(events):
@@ -257,19 +225,19 @@ def derive_product_phase(bundle: dict[str, Any], status_model: dict[str, Any]) -
257
  message = _lower(event.get("message"))
258
  status = _lower(event.get("status"))
259
  if "patch" in step or "repair" in step or "patched" in message or "restart" in message:
260
- return "hardware"
261
  if "missing" in message or "error" in message or status in FAILED_RAW_STATUSES:
262
- return "hardware"
263
  phase = STEP_TO_PHASE.get(step)
264
  if phase:
265
  return phase
266
 
267
  summary = bundle.get("summary") or {}
268
  if summary.get("target_space"):
269
- return "hardware"
270
  if bundle.get("launch"):
271
- return "job_launched"
272
- return "job_launched"
273
 
274
 
275
  def build_pipeline(phase: str, status_model: dict[str, Any]) -> list[dict[str, Any]]:
@@ -379,7 +347,7 @@ def build_diagnostics(bundle: dict[str, Any], status_model: dict[str, Any], phas
379
 
380
  health_passed = bool((gate.get("implementation_signals") or {}).get("health_passed") or smoke.get("health_passed"))
381
  smoke_ok = bool(smoke.get("ok") or smoke.get("status") == "success")
382
- build_ok = phase in {"live_validation", "report"} or status_model["global_status"] == "succeeded"
383
 
384
  return {
385
  "build_status": "passed" if build_ok else ("blocked" if status_model["global_status"] in {"blocked", "waiting_manual_action", "failed"} else "building"),
 
3
  from datetime import datetime, timedelta, timezone
4
  from typing import Any
5
 
6
+ from .progress import STEP_ALIASES, STEP_LABELS, STEP_ORDER
7
+
8
+ PRODUCT_STEPS = [{"id": step, "label": STEP_LABELS[step]} for step in STEP_ORDER]
 
 
 
 
 
 
 
 
9
 
10
  TERMINAL_GLOBAL_STATUSES = {"succeeded", "failed", "cancelled", "blocked", "waiting_manual_action"}
11
  SUCCESS_RAW_STATUSES = {
 
24
  "manual_action_required",
25
  }
26
  FAILED_RAW_STATUSES = {"failed", "failure", "error", "repair_failed"}
27
+ CANCELLED_RAW_STATUSES = {"cancelled", "canceled"}
28
  BLOCKED_RAW_STATUSES = {"technical_blocker", "blocked", "health_only"}
29
  RUNNING_RAW_STATUSES = {"started", "running", "waiting", "queued", "pending", "scheduled"}
30
 
31
+ STEP_TO_PHASE = {step: step for step in STEP_ORDER} | {alias: canonical for alias, canonical in STEP_ALIASES.items()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
 
34
  def parse_ts(value: Any) -> datetime | None:
 
162
  manual = requires_manual_action(bundle)
163
  blocker = has_technical_blocker(bundle)
164
 
165
+ if statuses.intersection(CANCELLED_RAW_STATUSES):
166
+ global_status = "cancelled"
167
+ verdict = "cancelled"
168
+ elif manual:
169
  global_status = "waiting_manual_action"
170
  verdict = "manual_action_required"
171
  elif statuses.intersection(SUCCESS_RAW_STATUSES):
 
212
 
213
 
214
  def derive_product_phase(bundle: dict[str, Any], status_model: dict[str, Any]) -> str:
215
+ if status_model["global_status"] == "succeeded":
216
+ return "done"
217
+ if status_model["global_status"] in {"failed", "blocked", "stale", "cancelled"}:
218
+ return "failure"
219
  if status_model["requires_manual_action"]:
220
+ return "inference_gate"
221
 
222
  events = [e for e in (bundle.get("events") or []) if isinstance(e, dict)]
223
  for event in reversed(events):
 
225
  message = _lower(event.get("message"))
226
  status = _lower(event.get("status"))
227
  if "patch" in step or "repair" in step or "patched" in message or "restart" in message:
228
+ return "hardware_strategy"
229
  if "missing" in message or "error" in message or status in FAILED_RAW_STATUSES:
230
+ return "hardware_strategy"
231
  phase = STEP_TO_PHASE.get(step)
232
  if phase:
233
  return phase
234
 
235
  summary = bundle.get("summary") or {}
236
  if summary.get("target_space"):
237
+ return "hardware_strategy"
238
  if bundle.get("launch"):
239
+ return "bootstrap"
240
+ return "bootstrap"
241
 
242
 
243
  def build_pipeline(phase: str, status_model: dict[str, Any]) -> list[dict[str, Any]]:
 
347
 
348
  health_passed = bool((gate.get("implementation_signals") or {}).get("health_passed") or smoke.get("health_passed"))
349
  smoke_ok = bool(smoke.get("ok") or smoke.get("status") == "success")
350
+ build_ok = phase in {"api_validation", "live_wait", "generation_smoke", "inference_gate", "report_write", "done"} or status_model["global_status"] == "succeeded"
351
 
352
  return {
353
  "build_status": "passed" if build_ok else ("blocked" if status_model["global_status"] in {"blocked", "waiting_manual_action", "failed"} else "building"),