hte-cli 0.2.24__py3-none-any.whl → 0.2.25__py3-none-any.whl
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/cli.py +53 -7
- hte_cli/image_utils.py +36 -1
- {hte_cli-0.2.24.dist-info → hte_cli-0.2.25.dist-info}/METADATA +1 -1
- {hte_cli-0.2.24.dist-info → hte_cli-0.2.25.dist-info}/RECORD +6 -6
- {hte_cli-0.2.24.dist-info → hte_cli-0.2.25.dist-info}/WHEEL +0 -0
- {hte_cli-0.2.24.dist-info → hte_cli-0.2.25.dist-info}/entry_points.txt +0 -0
hte_cli/cli.py
CHANGED
|
@@ -172,6 +172,17 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
172
172
|
console.print("[red]Not logged in. Run: hte-cli auth login[/red]")
|
|
173
173
|
sys.exit(1)
|
|
174
174
|
|
|
175
|
+
# Check Docker is running before we start (with retry prompt)
|
|
176
|
+
while True:
|
|
177
|
+
docker_ok, docker_error = _check_docker()
|
|
178
|
+
if docker_ok:
|
|
179
|
+
break
|
|
180
|
+
console.print(f"[red]{docker_error}[/red]")
|
|
181
|
+
console.print()
|
|
182
|
+
if not click.confirm("Start Docker and retry?", default=True):
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
console.print("[dim]Checking Docker again...[/dim]")
|
|
185
|
+
|
|
175
186
|
api = APIClient(config)
|
|
176
187
|
|
|
177
188
|
# Step 1: Join session
|
|
@@ -201,8 +212,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
201
212
|
# Check if reconnecting (session already in_progress)
|
|
202
213
|
is_reconnect = session_info.get("status") == "in_progress"
|
|
203
214
|
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
# Always run setup on reconnect - previous attempt may have failed
|
|
216
|
+
# (e.g., image pull failed, Docker wasn't running, etc.)
|
|
217
|
+
if is_reconnect:
|
|
218
|
+
force_setup = True
|
|
219
|
+
console.print("[yellow]Reconnecting to existing session (re-running setup)...[/yellow]")
|
|
206
220
|
console.print()
|
|
207
221
|
|
|
208
222
|
console.print(
|
|
@@ -219,7 +233,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
219
233
|
import time
|
|
220
234
|
from hte_cli.events import EventStreamer
|
|
221
235
|
from hte_cli.runner import TaskRunner
|
|
222
|
-
from hte_cli.image_utils import
|
|
236
|
+
from hte_cli.image_utils import (
|
|
237
|
+
extract_images_from_compose,
|
|
238
|
+
extract_image_platforms_from_compose,
|
|
239
|
+
pull_image_with_progress,
|
|
240
|
+
)
|
|
223
241
|
|
|
224
242
|
# Create event streamer
|
|
225
243
|
events = EventStreamer(api, session_id)
|
|
@@ -285,9 +303,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
285
303
|
failed_images = []
|
|
286
304
|
|
|
287
305
|
if not is_reconnect or force_setup:
|
|
288
|
-
# Extract images from compose
|
|
306
|
+
# Extract images and their platforms from compose
|
|
307
|
+
image_platforms = {}
|
|
289
308
|
if compose_yaml:
|
|
290
309
|
images = extract_images_from_compose(compose_yaml)
|
|
310
|
+
image_platforms = extract_image_platforms_from_compose(compose_yaml)
|
|
291
311
|
|
|
292
312
|
# Send setup_started event (includes CLI version for debugging)
|
|
293
313
|
events.setup_started(images=images, cli_version=__version__)
|
|
@@ -298,9 +318,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
298
318
|
|
|
299
319
|
console.print(f"[bold]Step 2:[/bold] Pulling {len(images)} Docker image(s)...")
|
|
300
320
|
pull_start = time.monotonic()
|
|
321
|
+
pull_errors = {}
|
|
301
322
|
|
|
302
323
|
for img in images:
|
|
303
324
|
short_name = img.split("/")[-1][:40]
|
|
325
|
+
platform = image_platforms.get(img)
|
|
304
326
|
|
|
305
327
|
# Check if already cached
|
|
306
328
|
if check_image_exists_locally(img):
|
|
@@ -310,6 +332,7 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
310
332
|
|
|
311
333
|
# Need to pull - show progress
|
|
312
334
|
last_status = ["connecting..."]
|
|
335
|
+
last_error = [""]
|
|
313
336
|
with console.status(
|
|
314
337
|
f"[yellow]↓[/yellow] {short_name} [dim]connecting...[/dim]"
|
|
315
338
|
) as status:
|
|
@@ -328,14 +351,23 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
328
351
|
status.update(
|
|
329
352
|
f"[yellow]↓[/yellow] {short_name} [dim]{display}[/dim]"
|
|
330
353
|
)
|
|
354
|
+
# Capture error messages
|
|
355
|
+
if "error" in line.lower() or "denied" in line.lower():
|
|
356
|
+
last_error[0] = line
|
|
331
357
|
|
|
332
|
-
success = pull_image_with_progress(
|
|
358
|
+
success = pull_image_with_progress(
|
|
359
|
+
img, platform=platform, on_progress=show_progress
|
|
360
|
+
)
|
|
333
361
|
|
|
334
362
|
if success:
|
|
335
363
|
console.print(f" [green]✓[/green] {short_name} [dim](downloaded)[/dim]")
|
|
336
364
|
pulled_images.append(img)
|
|
337
365
|
else:
|
|
338
|
-
|
|
366
|
+
platform_note = f" (platform: {platform})" if platform else ""
|
|
367
|
+
console.print(f" [red]✗[/red] {short_name}{platform_note} [dim](failed)[/dim]")
|
|
368
|
+
if last_error[0]:
|
|
369
|
+
console.print(f" [dim]{last_error[0][:60]}[/dim]")
|
|
370
|
+
pull_errors[img] = last_error[0]
|
|
339
371
|
failed_images.append(img)
|
|
340
372
|
|
|
341
373
|
pull_duration = time.monotonic() - pull_start
|
|
@@ -347,6 +379,20 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
347
379
|
)
|
|
348
380
|
console.print()
|
|
349
381
|
|
|
382
|
+
# Fail fast if any required image couldn't be pulled
|
|
383
|
+
if failed_images:
|
|
384
|
+
console.print(
|
|
385
|
+
f"[red]Error: Failed to pull {len(failed_images)} required Docker image(s).[/red]"
|
|
386
|
+
)
|
|
387
|
+
console.print()
|
|
388
|
+
console.print("[yellow]Troubleshooting:[/yellow]")
|
|
389
|
+
console.print(" 1. Check Docker is running: docker info")
|
|
390
|
+
console.print(" 2. Try manual pull: docker pull python:3.12-slim --platform linux/amd64")
|
|
391
|
+
console.print(" 3. Check network connectivity")
|
|
392
|
+
console.print()
|
|
393
|
+
console.print("Session remains active - you can retry with: hte-cli session join " + session_id)
|
|
394
|
+
sys.exit(1)
|
|
395
|
+
|
|
350
396
|
# Send setup_completed - THIS STARTS THE TIMER ON SERVER
|
|
351
397
|
total_setup = time.monotonic() - setup_start_time
|
|
352
398
|
events.setup_completed(total_seconds=total_setup)
|
|
@@ -644,7 +690,7 @@ def _check_docker() -> tuple[bool, str | None]:
|
|
|
644
690
|
timeout=10,
|
|
645
691
|
)
|
|
646
692
|
if result.returncode != 0:
|
|
647
|
-
return False, "Docker is not running. Start Docker Desktop or
|
|
693
|
+
return False, "Docker is not running. Start Docker (Docker Desktop, colima, or dockerd)."
|
|
648
694
|
except FileNotFoundError:
|
|
649
695
|
return False, "Docker is not installed. Install from https://docs.docker.com/get-docker/"
|
|
650
696
|
except Exception as e:
|
hte_cli/image_utils.py
CHANGED
|
@@ -38,6 +38,33 @@ def extract_images_from_compose(compose_yaml: str) -> list[str]:
|
|
|
38
38
|
return []
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def extract_image_platforms_from_compose(compose_yaml: str) -> dict[str, str | None]:
|
|
42
|
+
"""
|
|
43
|
+
Extract Docker image names and their platforms from a compose.yaml string.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
compose_yaml: Docker Compose YAML content
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dict mapping image names to their platform (or None if no platform specified)
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
compose_data = yaml.safe_load(compose_yaml)
|
|
53
|
+
if not compose_data or "services" not in compose_data:
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
image_platforms = {}
|
|
57
|
+
for service_name, service_config in compose_data.get("services", {}).items():
|
|
58
|
+
if isinstance(service_config, dict) and "image" in service_config:
|
|
59
|
+
image = service_config["image"]
|
|
60
|
+
platform = service_config.get("platform")
|
|
61
|
+
image_platforms[image] = platform
|
|
62
|
+
return image_platforms
|
|
63
|
+
except yaml.YAMLError as e:
|
|
64
|
+
logger.warning(f"Failed to parse compose.yaml: {e}")
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
|
|
41
68
|
def check_image_exists_locally(image: str) -> bool:
|
|
42
69
|
"""
|
|
43
70
|
Check if a Docker image exists locally.
|
|
@@ -61,16 +88,20 @@ def check_image_exists_locally(image: str) -> bool:
|
|
|
61
88
|
|
|
62
89
|
def pull_image_with_progress(
|
|
63
90
|
image: str,
|
|
91
|
+
platform: str | None = None,
|
|
64
92
|
on_progress: Callable[[str, str], None] | None = None,
|
|
65
93
|
on_complete: Callable[[str, bool], None] | None = None,
|
|
94
|
+
on_error: Callable[[str, str], None] | None = None,
|
|
66
95
|
) -> bool:
|
|
67
96
|
"""
|
|
68
97
|
Pull a Docker image with progress callbacks using PTY for real progress output.
|
|
69
98
|
|
|
70
99
|
Args:
|
|
71
100
|
image: Image name to pull
|
|
101
|
+
platform: Optional platform to pull (e.g., "linux/amd64")
|
|
72
102
|
on_progress: Callback(image, status_line) called for each progress update
|
|
73
103
|
on_complete: Callback(image, success) called when pull completes
|
|
104
|
+
on_error: Callback(image, error_message) called when pull fails
|
|
74
105
|
|
|
75
106
|
Returns:
|
|
76
107
|
True if pull succeeded, False otherwise
|
|
@@ -79,8 +110,12 @@ def pull_image_with_progress(
|
|
|
79
110
|
# Use PTY to get real progress output from docker
|
|
80
111
|
master_fd, slave_fd = pty.openpty()
|
|
81
112
|
|
|
113
|
+
cmd = ["docker", "pull", image]
|
|
114
|
+
if platform:
|
|
115
|
+
cmd.extend(["--platform", platform])
|
|
116
|
+
|
|
82
117
|
process = subprocess.Popen(
|
|
83
|
-
|
|
118
|
+
cmd,
|
|
84
119
|
stdout=slave_fd,
|
|
85
120
|
stderr=slave_fd,
|
|
86
121
|
stdin=slave_fd,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
hte_cli/__init__.py,sha256=fDGXp-r8bIoLtlQnn5xJ_CpwMhonvk9bGjZQsjA2mDI,914
|
|
2
2
|
hte_cli/__main__.py,sha256=63n0gNGfskidWDU0aAIF2N8lylVCLYKVIkrN9QiORoo,107
|
|
3
3
|
hte_cli/api_client.py,sha256=m42kfFZS72Nu_VuDwxRsLNy4ziCcvgk7KNWBh9gwqy0,9257
|
|
4
|
-
hte_cli/cli.py,sha256=
|
|
4
|
+
hte_cli/cli.py,sha256=5aKf-k7qw3e1tmwpy34KDivIJ5l__2W-OEkGynCbQbU,26354
|
|
5
5
|
hte_cli/config.py,sha256=42Xv__YMSeRLs2zhGukJkIXFKtnBtYCHnONfViGyt2g,3387
|
|
6
6
|
hte_cli/errors.py,sha256=1J5PpxcUKBu6XjigMMCPOq4Zc12tnv8LhAsiaVFWLQM,2762
|
|
7
7
|
hte_cli/events.py,sha256=oDKCS-a0IZ7bz7xkwQj5eM4DoDCYvnclAGohrMTWf8s,5644
|
|
8
|
-
hte_cli/image_utils.py,sha256=
|
|
8
|
+
hte_cli/image_utils.py,sha256=nVHhUY-QZ4uPpGSx3ByOiVGOnm9T11p_cVlb39FQb_Y,7717
|
|
9
9
|
hte_cli/runner.py,sha256=SWl9FF4X3e9eBbZyL0ujhmmSL5OK8J6st-Ty0jD5AWM,14550
|
|
10
10
|
hte_cli/scorers.py,sha256=B0ZjQ3Fh-VDkc_8CDc86yW7vpdimbV3RSqs7l-VeUIg,6629
|
|
11
11
|
hte_cli/version_check.py,sha256=WVZyGy2XfAghQYdd2N9-0Qfg-7pgp9gt4761-PnmacI,1708
|
|
12
|
-
hte_cli-0.2.
|
|
13
|
-
hte_cli-0.2.
|
|
14
|
-
hte_cli-0.2.
|
|
15
|
-
hte_cli-0.2.
|
|
12
|
+
hte_cli-0.2.25.dist-info/METADATA,sha256=Sqc87sNbJMTRSJaR71y4Y6DpXjSyJ7-UDAix0p-bRpw,3820
|
|
13
|
+
hte_cli-0.2.25.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
hte_cli-0.2.25.dist-info/entry_points.txt,sha256=XbyEEi1H14DFAt0Kdl22e_IRVEGzimSzYSh5HlhKlFA,41
|
|
15
|
+
hte_cli-0.2.25.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|