hte-cli 0.2.30__tar.gz → 0.2.31__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hte_cli-0.2.30 → hte_cli-0.2.31}/PKG-INFO +1 -1
- {hte_cli-0.2.30 → hte_cli-0.2.31}/pyproject.toml +1 -1
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/api_client.py +24 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/cli.py +73 -20
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/image_utils.py +1 -4
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/unit/test_image_utils.py +3 -1
- {hte_cli-0.2.30 → hte_cli-0.2.31}/.gitignore +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/README.md +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/__init__.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/__main__.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/config.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/errors.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/events.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/runner.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/scorers.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/src/hte_cli/version_check.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/__init__.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/__init__.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/automated_runner.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/conftest.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/e2e_test.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/test_benchmark_flows.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/test_eval_logs.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/test_infrastructure.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/test_runtime_imports.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/test_session_lifecycle.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/e2e/verify_docker_deps.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/unit/__init__.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/unit/conftest.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/unit/test_runner.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/tests/unit/test_scorers.py +0 -0
- {hte_cli-0.2.30 → hte_cli-0.2.31}/uv.lock +0 -0
|
@@ -265,3 +265,27 @@ class APIClient:
|
|
|
265
265
|
json=payload,
|
|
266
266
|
timeout=UPLOAD_TIMEOUT,
|
|
267
267
|
)
|
|
268
|
+
|
|
269
|
+
def upload_partial_log(
|
|
270
|
+
self,
|
|
271
|
+
session_id: str,
|
|
272
|
+
eval_log_bytes: bytes,
|
|
273
|
+
) -> dict:
|
|
274
|
+
"""Upload partial eval log for interrupted sessions.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
session_id: The session ID
|
|
278
|
+
eval_log_bytes: Partial eval log content
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Response dict with status and log_path
|
|
282
|
+
"""
|
|
283
|
+
payload = {
|
|
284
|
+
"eval_log_base64": base64.b64encode(eval_log_bytes).decode("ascii"),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return self.post(
|
|
288
|
+
f"/sessions/{session_id}/partial-log",
|
|
289
|
+
json=payload,
|
|
290
|
+
timeout=UPLOAD_TIMEOUT,
|
|
291
|
+
)
|
|
@@ -23,6 +23,42 @@ console = Console()
|
|
|
23
23
|
SUPPORT_EMAIL = "jacktpayne51@gmail.com"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _find_eval_log_bytes(runner) -> bytes | None:
|
|
27
|
+
"""Find and read eval log bytes from runner's work directory.
|
|
28
|
+
|
|
29
|
+
Used for interrupted sessions to upload partial logs.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
# Look for eval logs in the work directory
|
|
33
|
+
if not runner.work_dir.exists():
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
# Find any .eval files in the work directory tree
|
|
37
|
+
eval_files = list(runner.work_dir.rglob("*.eval"))
|
|
38
|
+
if not eval_files:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
# Get the most recent one
|
|
42
|
+
eval_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
43
|
+
return eval_files[0].read_bytes()
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _upload_partial_log(api: APIClient, session_id: str, eval_log_bytes: bytes, console: Console) -> None:
|
|
49
|
+
"""Upload partial eval log for interrupted session.
|
|
50
|
+
|
|
51
|
+
Best-effort: silently handles failures to not block exit.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
size_kb = len(eval_log_bytes) / 1024
|
|
55
|
+
console.print(f"[dim]Uploading partial eval log ({size_kb:.0f} KB)...[/dim]")
|
|
56
|
+
api.upload_partial_log(session_id, eval_log_bytes)
|
|
57
|
+
console.print("[dim]Partial eval log uploaded.[/dim]")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
console.print(f"[dim]Could not upload partial log: {e}[/dim]")
|
|
60
|
+
|
|
61
|
+
|
|
26
62
|
@click.group()
|
|
27
63
|
@click.version_option(__version__, prog_name="hte-cli")
|
|
28
64
|
@click.pass_context
|
|
@@ -338,15 +374,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
338
374
|
host_platform = get_host_docker_platform()
|
|
339
375
|
|
|
340
376
|
if is_linux_arm:
|
|
341
|
-
console.print(
|
|
342
|
-
f"[yellow]![/yellow] Detected [bold]Linux ARM64[/bold] environment"
|
|
343
|
-
)
|
|
377
|
+
console.print("[yellow]![/yellow] Detected [bold]Linux ARM64[/bold] environment")
|
|
344
378
|
console.print(
|
|
345
379
|
f" [dim]Will verify cached images match host architecture ({host_platform})[/dim]"
|
|
346
380
|
)
|
|
347
|
-
console.print(
|
|
348
|
-
f" [dim]Mismatched images will be automatically re-pulled[/dim]"
|
|
349
|
-
)
|
|
381
|
+
console.print(" [dim]Mismatched images will be automatically re-pulled[/dim]")
|
|
350
382
|
console.print()
|
|
351
383
|
|
|
352
384
|
console.print(f"[bold]Step 2:[/bold] Pulling {len(images)} Docker image(s)...")
|
|
@@ -382,7 +414,7 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
382
414
|
f" [dim]Cached image: {image_arch} | Host: {host_arch}[/dim]"
|
|
383
415
|
)
|
|
384
416
|
console.print(
|
|
385
|
-
|
|
417
|
+
" [dim]Removing cached image and re-pulling correct architecture...[/dim]"
|
|
386
418
|
)
|
|
387
419
|
|
|
388
420
|
needed_fix, fix_msg = fix_image_architecture(img)
|
|
@@ -396,7 +428,7 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
396
428
|
# No ARM variant available - this is an x86-only image
|
|
397
429
|
# Re-pull the amd64 version and warn about QEMU
|
|
398
430
|
console.print(
|
|
399
|
-
|
|
431
|
+
" [dim]No ARM variant available - re-pulling x86 version...[/dim]"
|
|
400
432
|
)
|
|
401
433
|
success = pull_image_with_progress(img)
|
|
402
434
|
if success:
|
|
@@ -407,7 +439,9 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
407
439
|
pulled_images.append(img)
|
|
408
440
|
continue
|
|
409
441
|
else:
|
|
410
|
-
console.print(
|
|
442
|
+
console.print(
|
|
443
|
+
f" [red]✗[/red] {short_name} [dim](failed to pull)[/dim]"
|
|
444
|
+
)
|
|
411
445
|
failed_images.append(img)
|
|
412
446
|
pull_errors[img] = "failed to pull x86 fallback"
|
|
413
447
|
continue
|
|
@@ -431,12 +465,12 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
431
465
|
|
|
432
466
|
def show_progress(image: str, line: str):
|
|
433
467
|
# Show docker output directly - includes MB progress from PTY
|
|
434
|
-
# Lines look like: "abc123: Downloading 360.9MB/4.075GB"
|
|
468
|
+
# Lines look like: "abc123: Downloading [======> ] 360.9MB/4.075GB"
|
|
435
469
|
if ": " in line:
|
|
436
470
|
parts = line.split(": ", 1)
|
|
437
471
|
if len(parts) == 2:
|
|
438
472
|
layer_id = parts[0][-8:]
|
|
439
|
-
layer_status = parts[1][:
|
|
473
|
+
layer_status = parts[1][:70] # Increased to include size info
|
|
440
474
|
display = f"{layer_id}: {layer_status}"
|
|
441
475
|
if display != last_status[0]:
|
|
442
476
|
last_status[0] = display
|
|
@@ -455,6 +489,7 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
455
489
|
# On Linux ARM64, verify pulled image architecture
|
|
456
490
|
if is_linux_arm:
|
|
457
491
|
from hte_cli.image_utils import get_image_architecture
|
|
492
|
+
|
|
458
493
|
pulled_arch = get_image_architecture(img)
|
|
459
494
|
|
|
460
495
|
if pulled_arch == "arm64":
|
|
@@ -467,7 +502,7 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
467
502
|
f" [yellow]![/yellow] {short_name} [dim](downloaded, arch: amd64)[/dim]"
|
|
468
503
|
)
|
|
469
504
|
console.print(
|
|
470
|
-
|
|
505
|
+
" [yellow]This is an x86 image - requires QEMU emulation on ARM[/yellow]"
|
|
471
506
|
)
|
|
472
507
|
x86_images_on_arm.append(img)
|
|
473
508
|
else:
|
|
@@ -499,9 +534,7 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
499
534
|
console.print(
|
|
500
535
|
f"[yellow]⚠ Warning:[/yellow] {len(x86_images_on_arm)} x86 image(s) detected on ARM host"
|
|
501
536
|
)
|
|
502
|
-
console.print(
|
|
503
|
-
" These require QEMU emulation. If container fails to start, run:"
|
|
504
|
-
)
|
|
537
|
+
console.print(" These require QEMU emulation. If container fails to start, run:")
|
|
505
538
|
console.print(
|
|
506
539
|
" [bold]docker run --privileged --rm tonistiigi/binfmt --install all[/bold]"
|
|
507
540
|
)
|
|
@@ -518,14 +551,21 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
518
551
|
|
|
519
552
|
# Architecture-specific advice
|
|
520
553
|
if is_linux_arm:
|
|
521
|
-
console.print(
|
|
522
|
-
|
|
554
|
+
console.print(
|
|
555
|
+
" 2. You're on Linux ARM64 - try: docker pull <image> --platform linux/arm64"
|
|
556
|
+
)
|
|
557
|
+
console.print(
|
|
558
|
+
" 3. For x86-only images, enable QEMU: docker run --privileged --rm tonistiigi/binfmt --install all"
|
|
559
|
+
)
|
|
523
560
|
else:
|
|
524
561
|
console.print(" 2. Try manual pull: docker pull <image>")
|
|
525
562
|
|
|
526
563
|
console.print(" 4. Check network connectivity")
|
|
527
564
|
console.print()
|
|
528
|
-
console.print(
|
|
565
|
+
console.print(
|
|
566
|
+
"Session remains active - you can retry with: hte-cli session join "
|
|
567
|
+
+ session_id
|
|
568
|
+
)
|
|
529
569
|
sys.exit(1)
|
|
530
570
|
|
|
531
571
|
# Send setup_completed - THIS STARTS THE TIMER ON SERVER
|
|
@@ -579,6 +619,10 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
579
619
|
eval_log_bytes = result.eval_log_path.read_bytes()
|
|
580
620
|
except KeyboardInterrupt:
|
|
581
621
|
events.docker_stopped(exit_code=130)
|
|
622
|
+
# Try to find and upload any partial eval log before exiting
|
|
623
|
+
eval_log_bytes = _find_eval_log_bytes(runner)
|
|
624
|
+
if eval_log_bytes:
|
|
625
|
+
_upload_partial_log(api, session_id, eval_log_bytes, console)
|
|
582
626
|
console.print()
|
|
583
627
|
console.print(
|
|
584
628
|
"[yellow]Interrupted. Session remains active - you can reconnect later.[/yellow]"
|
|
@@ -586,6 +630,10 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
586
630
|
sys.exit(0)
|
|
587
631
|
except Exception as e:
|
|
588
632
|
events.docker_stopped(exit_code=1)
|
|
633
|
+
# Try to upload partial log on failure too
|
|
634
|
+
eval_log_bytes = _find_eval_log_bytes(runner)
|
|
635
|
+
if eval_log_bytes:
|
|
636
|
+
_upload_partial_log(api, session_id, eval_log_bytes, console)
|
|
589
637
|
console.print(f"[red]Task execution failed: {e}[/red]")
|
|
590
638
|
sys.exit(1)
|
|
591
639
|
finally:
|
|
@@ -935,7 +983,9 @@ def diagnose_cmd():
|
|
|
935
983
|
console.print(" [red]✗[/red] QEMU x86 emulation NOT working")
|
|
936
984
|
console.print()
|
|
937
985
|
console.print(" [yellow]To enable QEMU emulation, run:[/yellow]")
|
|
938
|
-
console.print(
|
|
986
|
+
console.print(
|
|
987
|
+
" [bold]docker run --privileged --rm tonistiigi/binfmt --install all[/bold]"
|
|
988
|
+
)
|
|
939
989
|
except subprocess.TimeoutExpired:
|
|
940
990
|
console.print(" [yellow]![/yellow] QEMU test timed out")
|
|
941
991
|
except Exception as e:
|
|
@@ -979,7 +1029,10 @@ def _check_docker() -> tuple[bool, str | None]:
|
|
|
979
1029
|
timeout=10,
|
|
980
1030
|
)
|
|
981
1031
|
if result.returncode != 0:
|
|
982
|
-
return
|
|
1032
|
+
return (
|
|
1033
|
+
False,
|
|
1034
|
+
"Docker is not running. Start Docker (Docker Desktop, colima, or dockerd).",
|
|
1035
|
+
)
|
|
983
1036
|
except FileNotFoundError:
|
|
984
1037
|
return False, "Docker is not installed. Install from https://docs.docker.com/get-docker/"
|
|
985
1038
|
except Exception as e:
|
|
@@ -124,6 +124,7 @@ def is_running_in_linux_vm_on_arm() -> bool:
|
|
|
124
124
|
True if running Linux on ARM64
|
|
125
125
|
"""
|
|
126
126
|
import sys
|
|
127
|
+
|
|
127
128
|
return sys.platform == "linux" and get_host_architecture() in ("aarch64", "arm64")
|
|
128
129
|
|
|
129
130
|
|
|
@@ -313,10 +314,6 @@ def pull_image_with_progress(
|
|
|
313
314
|
|
|
314
315
|
# Read output from master with timeout
|
|
315
316
|
output_buffer = ""
|
|
316
|
-
# Regex to parse docker progress: "abc123: Downloading [===> ] 10.5MB/50MB"
|
|
317
|
-
progress_pattern = re.compile(
|
|
318
|
-
r"([a-f0-9]+):\s*(Downloading|Extracting|Verifying Checksum|Download complete|Pull complete|Already exists|Waiting)(?:\s+\[.*?\]\s+)?(\d+\.?\d*\s*[kMG]?B)?(?:/(\d+\.?\d*\s*[kMG]?B))?"
|
|
319
|
-
)
|
|
320
317
|
|
|
321
318
|
while True:
|
|
322
319
|
# Check if process is done
|
|
@@ -610,7 +610,9 @@ class TestFixImageArchitecture:
|
|
|
610
610
|
@patch("hte_cli.image_utils.remove_image")
|
|
611
611
|
@patch("hte_cli.image_utils.check_image_architecture_matches_host")
|
|
612
612
|
@patch("hte_cli.image_utils.platform.machine")
|
|
613
|
-
def test_returns_false_when_repull_fails(
|
|
613
|
+
def test_returns_false_when_repull_fails(
|
|
614
|
+
self, mock_machine, mock_check, mock_remove, mock_pull
|
|
615
|
+
):
|
|
614
616
|
"""Returns (False, message) when re-pull fails."""
|
|
615
617
|
mock_machine.return_value = "aarch64"
|
|
616
618
|
mock_check.return_value = (False, "amd64", "aarch64")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|