hte-cli 0.2.24__py3-none-any.whl → 0.2.26__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 +54 -7
- hte_cli/image_utils.py +36 -1
- {hte_cli-0.2.24.dist-info → hte_cli-0.2.26.dist-info}/METADATA +1 -1
- {hte_cli-0.2.24.dist-info → hte_cli-0.2.26.dist-info}/RECORD +6 -6
- {hte_cli-0.2.24.dist-info → hte_cli-0.2.26.dist-info}/WHEEL +0 -0
- {hte_cli-0.2.24.dist-info → hte_cli-0.2.26.dist-info}/entry_points.txt +0 -0
hte_cli/cli.py
CHANGED
|
@@ -172,6 +172,18 @@ 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
|
+
console.print("[dim]✓ Docker running[/dim]")
|
|
180
|
+
break
|
|
181
|
+
console.print(f"[red]{docker_error}[/red]")
|
|
182
|
+
console.print()
|
|
183
|
+
if not click.confirm("Start Docker and retry?", default=True):
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
console.print("[dim]Checking Docker again...[/dim]")
|
|
186
|
+
|
|
175
187
|
api = APIClient(config)
|
|
176
188
|
|
|
177
189
|
# Step 1: Join session
|
|
@@ -201,8 +213,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
201
213
|
# Check if reconnecting (session already in_progress)
|
|
202
214
|
is_reconnect = session_info.get("status") == "in_progress"
|
|
203
215
|
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
# Always run setup on reconnect - previous attempt may have failed
|
|
217
|
+
# (e.g., image pull failed, Docker wasn't running, etc.)
|
|
218
|
+
if is_reconnect:
|
|
219
|
+
force_setup = True
|
|
220
|
+
console.print("[yellow]Reconnecting to existing session (re-running setup)...[/yellow]")
|
|
206
221
|
console.print()
|
|
207
222
|
|
|
208
223
|
console.print(
|
|
@@ -219,7 +234,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
219
234
|
import time
|
|
220
235
|
from hte_cli.events import EventStreamer
|
|
221
236
|
from hte_cli.runner import TaskRunner
|
|
222
|
-
from hte_cli.image_utils import
|
|
237
|
+
from hte_cli.image_utils import (
|
|
238
|
+
extract_images_from_compose,
|
|
239
|
+
extract_image_platforms_from_compose,
|
|
240
|
+
pull_image_with_progress,
|
|
241
|
+
)
|
|
223
242
|
|
|
224
243
|
# Create event streamer
|
|
225
244
|
events = EventStreamer(api, session_id)
|
|
@@ -285,9 +304,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
285
304
|
failed_images = []
|
|
286
305
|
|
|
287
306
|
if not is_reconnect or force_setup:
|
|
288
|
-
# Extract images from compose
|
|
307
|
+
# Extract images and their platforms from compose
|
|
308
|
+
image_platforms = {}
|
|
289
309
|
if compose_yaml:
|
|
290
310
|
images = extract_images_from_compose(compose_yaml)
|
|
311
|
+
image_platforms = extract_image_platforms_from_compose(compose_yaml)
|
|
291
312
|
|
|
292
313
|
# Send setup_started event (includes CLI version for debugging)
|
|
293
314
|
events.setup_started(images=images, cli_version=__version__)
|
|
@@ -298,9 +319,11 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
298
319
|
|
|
299
320
|
console.print(f"[bold]Step 2:[/bold] Pulling {len(images)} Docker image(s)...")
|
|
300
321
|
pull_start = time.monotonic()
|
|
322
|
+
pull_errors = {}
|
|
301
323
|
|
|
302
324
|
for img in images:
|
|
303
325
|
short_name = img.split("/")[-1][:40]
|
|
326
|
+
platform = image_platforms.get(img)
|
|
304
327
|
|
|
305
328
|
# Check if already cached
|
|
306
329
|
if check_image_exists_locally(img):
|
|
@@ -310,6 +333,7 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
310
333
|
|
|
311
334
|
# Need to pull - show progress
|
|
312
335
|
last_status = ["connecting..."]
|
|
336
|
+
last_error = [""]
|
|
313
337
|
with console.status(
|
|
314
338
|
f"[yellow]↓[/yellow] {short_name} [dim]connecting...[/dim]"
|
|
315
339
|
) as status:
|
|
@@ -328,14 +352,23 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
328
352
|
status.update(
|
|
329
353
|
f"[yellow]↓[/yellow] {short_name} [dim]{display}[/dim]"
|
|
330
354
|
)
|
|
355
|
+
# Capture error messages
|
|
356
|
+
if "error" in line.lower() or "denied" in line.lower():
|
|
357
|
+
last_error[0] = line
|
|
331
358
|
|
|
332
|
-
success = pull_image_with_progress(
|
|
359
|
+
success = pull_image_with_progress(
|
|
360
|
+
img, platform=platform, on_progress=show_progress
|
|
361
|
+
)
|
|
333
362
|
|
|
334
363
|
if success:
|
|
335
364
|
console.print(f" [green]✓[/green] {short_name} [dim](downloaded)[/dim]")
|
|
336
365
|
pulled_images.append(img)
|
|
337
366
|
else:
|
|
338
|
-
|
|
367
|
+
platform_note = f" (platform: {platform})" if platform else ""
|
|
368
|
+
console.print(f" [red]✗[/red] {short_name}{platform_note} [dim](failed)[/dim]")
|
|
369
|
+
if last_error[0]:
|
|
370
|
+
console.print(f" [dim]{last_error[0][:60]}[/dim]")
|
|
371
|
+
pull_errors[img] = last_error[0]
|
|
339
372
|
failed_images.append(img)
|
|
340
373
|
|
|
341
374
|
pull_duration = time.monotonic() - pull_start
|
|
@@ -347,6 +380,20 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
347
380
|
)
|
|
348
381
|
console.print()
|
|
349
382
|
|
|
383
|
+
# Fail fast if any required image couldn't be pulled
|
|
384
|
+
if failed_images:
|
|
385
|
+
console.print(
|
|
386
|
+
f"[red]Error: Failed to pull {len(failed_images)} required Docker image(s).[/red]"
|
|
387
|
+
)
|
|
388
|
+
console.print()
|
|
389
|
+
console.print("[yellow]Troubleshooting:[/yellow]")
|
|
390
|
+
console.print(" 1. Check Docker is running: docker info")
|
|
391
|
+
console.print(" 2. Try manual pull: docker pull python:3.12-slim --platform linux/amd64")
|
|
392
|
+
console.print(" 3. Check network connectivity")
|
|
393
|
+
console.print()
|
|
394
|
+
console.print("Session remains active - you can retry with: hte-cli session join " + session_id)
|
|
395
|
+
sys.exit(1)
|
|
396
|
+
|
|
350
397
|
# Send setup_completed - THIS STARTS THE TIMER ON SERVER
|
|
351
398
|
total_setup = time.monotonic() - setup_start_time
|
|
352
399
|
events.setup_completed(total_seconds=total_setup)
|
|
@@ -644,7 +691,7 @@ def _check_docker() -> tuple[bool, str | None]:
|
|
|
644
691
|
timeout=10,
|
|
645
692
|
)
|
|
646
693
|
if result.returncode != 0:
|
|
647
|
-
return False, "Docker is not running. Start Docker Desktop or
|
|
694
|
+
return False, "Docker is not running. Start Docker (Docker Desktop, colima, or dockerd)."
|
|
648
695
|
except FileNotFoundError:
|
|
649
696
|
return False, "Docker is not installed. Install from https://docs.docker.com/get-docker/"
|
|
650
697
|
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=ywYfUmxuPt693c24vhK5bwL9KCe3IkR_lcca6cQHa9E,26413
|
|
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.26.dist-info/METADATA,sha256=8KvyBSr89IAjdcx9UMs2Obl2HMoMJFjeS2r5aH7XgLM,3820
|
|
13
|
+
hte_cli-0.2.26.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
hte_cli-0.2.26.dist-info/entry_points.txt,sha256=XbyEEi1H14DFAt0Kdl22e_IRVEGzimSzYSh5HlhKlFA,41
|
|
15
|
+
hte_cli-0.2.26.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|