hte-cli 0.2.8__py3-none-any.whl → 0.2.10__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 +73 -32
- hte_cli/image_utils.py +62 -13
- {hte_cli-0.2.8.dist-info → hte_cli-0.2.10.dist-info}/METADATA +1 -1
- {hte_cli-0.2.8.dist-info → hte_cli-0.2.10.dist-info}/RECORD +6 -6
- {hte_cli-0.2.8.dist-info → hte_cli-0.2.10.dist-info}/WHEEL +0 -0
- {hte_cli-0.2.8.dist-info → hte_cli-0.2.10.dist-info}/entry_points.txt +0 -0
hte_cli/cli.py
CHANGED
|
@@ -310,42 +310,83 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
310
310
|
cached_images.append(img)
|
|
311
311
|
continue
|
|
312
312
|
|
|
313
|
-
# Need to pull - show progress
|
|
314
|
-
|
|
313
|
+
# Need to pull - show aggregated progress across all layers
|
|
314
|
+
import re
|
|
315
|
+
layer_progress: dict[str, dict] = {} # layer_id -> {current, total, phase}
|
|
316
|
+
last_display = [""]
|
|
317
|
+
|
|
318
|
+
def parse_size(s: str) -> float:
|
|
319
|
+
"""Parse size string like '10.5MB' or '1.2GB' to bytes."""
|
|
320
|
+
if not s:
|
|
321
|
+
return 0
|
|
322
|
+
s = s.strip()
|
|
323
|
+
multipliers = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3}
|
|
324
|
+
for suffix, mult in multipliers.items():
|
|
325
|
+
if s.upper().endswith(suffix):
|
|
326
|
+
try:
|
|
327
|
+
return float(s[:-len(suffix)].strip()) * mult
|
|
328
|
+
except ValueError:
|
|
329
|
+
return 0
|
|
330
|
+
try:
|
|
331
|
+
return float(s)
|
|
332
|
+
except ValueError:
|
|
333
|
+
return 0
|
|
334
|
+
|
|
335
|
+
def format_size(b: float) -> str:
|
|
336
|
+
"""Format bytes to human readable."""
|
|
337
|
+
if b >= 1024**3:
|
|
338
|
+
return f"{b/1024**3:.1f}GB"
|
|
339
|
+
elif b >= 1024**2:
|
|
340
|
+
return f"{b/1024**2:.0f}MB"
|
|
341
|
+
elif b >= 1024:
|
|
342
|
+
return f"{b/1024:.0f}KB"
|
|
343
|
+
return f"{b:.0f}B"
|
|
344
|
+
|
|
315
345
|
with console.status(f"[yellow]↓[/yellow] {short_name} [dim]connecting...[/dim]") as status:
|
|
316
346
|
def show_progress(image: str, line: str):
|
|
317
|
-
# Parse
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
# Parse: "abc123: Downloading 10.5MB/50MB" or "abc123: Extracting 10MB/50MB"
|
|
348
|
+
size_match = re.search(
|
|
349
|
+
r"([a-f0-9]+):\s*(Downloading|Extracting)\s+(\d+\.?\d*\s*[kMGT]?B)/(\d+\.?\d*\s*[kMGT]?B)",
|
|
350
|
+
line, re.IGNORECASE
|
|
351
|
+
)
|
|
352
|
+
if size_match:
|
|
353
|
+
layer_id = size_match.group(1)
|
|
354
|
+
phase = size_match.group(2)
|
|
355
|
+
current = parse_size(size_match.group(3))
|
|
356
|
+
total = parse_size(size_match.group(4))
|
|
357
|
+
layer_progress[layer_id] = {"current": current, "total": total, "phase": phase}
|
|
358
|
+
|
|
359
|
+
# Check for completed layers
|
|
360
|
+
complete_match = re.search(r"([a-f0-9]+):\s*(Pull complete|Download complete|Already exists)", line)
|
|
361
|
+
if complete_match:
|
|
362
|
+
layer_id = complete_match.group(1)
|
|
363
|
+
if layer_id in layer_progress:
|
|
364
|
+
layer_progress[layer_id]["current"] = layer_progress[layer_id]["total"]
|
|
365
|
+
layer_progress[layer_id]["phase"] = "done"
|
|
366
|
+
|
|
367
|
+
# Aggregate progress
|
|
368
|
+
total_current = sum(lp.get("current", 0) for lp in layer_progress.values())
|
|
369
|
+
total_size = sum(lp.get("total", 0) for lp in layer_progress.values())
|
|
370
|
+
active_layers = sum(1 for lp in layer_progress.values() if lp.get("phase") in ("Downloading", "Extracting"))
|
|
371
|
+
done_layers = sum(1 for lp in layer_progress.values() if lp.get("phase") == "done")
|
|
372
|
+
|
|
373
|
+
if total_size > 0:
|
|
374
|
+
pct = (total_current / total_size) * 100
|
|
375
|
+
display = f"{format_size(total_current)}/{format_size(total_size)} ({pct:.0f}%)"
|
|
376
|
+
if active_layers > 0:
|
|
377
|
+
display += f" - {active_layers} active"
|
|
378
|
+
if done_layers > 0:
|
|
379
|
+
display += f", {done_layers} done"
|
|
380
|
+
elif "Pulling" in line or "Waiting" in line:
|
|
381
|
+
display = "preparing layers..."
|
|
343
382
|
elif line.strip():
|
|
344
|
-
|
|
383
|
+
display = line[:40]
|
|
384
|
+
else:
|
|
385
|
+
return
|
|
345
386
|
|
|
346
|
-
if
|
|
347
|
-
|
|
348
|
-
status.update(f"[yellow]↓[/yellow] {short_name} [dim]{
|
|
387
|
+
if display != last_display[0]:
|
|
388
|
+
last_display[0] = display
|
|
389
|
+
status.update(f"[yellow]↓[/yellow] {short_name} [dim]{display}[/dim]")
|
|
349
390
|
|
|
350
391
|
success = pull_image_with_progress(img, on_progress=show_progress)
|
|
351
392
|
|
hte_cli/image_utils.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"""Docker image utilities for pre-pulling compose images."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pty
|
|
6
|
+
import re
|
|
7
|
+
import select
|
|
4
8
|
import subprocess
|
|
5
9
|
from collections.abc import Callable
|
|
6
10
|
|
|
@@ -61,32 +65,77 @@ def pull_image_with_progress(
|
|
|
61
65
|
on_complete: Callable[[str, bool], None] | None = None,
|
|
62
66
|
) -> bool:
|
|
63
67
|
"""
|
|
64
|
-
Pull a Docker image with progress callbacks.
|
|
68
|
+
Pull a Docker image with progress callbacks using PTY for real progress output.
|
|
65
69
|
|
|
66
70
|
Args:
|
|
67
71
|
image: Image name to pull
|
|
68
|
-
on_progress: Callback(image, status_line) called for each
|
|
72
|
+
on_progress: Callback(image, status_line) called for each progress update
|
|
69
73
|
on_complete: Callback(image, success) called when pull completes
|
|
70
74
|
|
|
71
75
|
Returns:
|
|
72
76
|
True if pull succeeded, False otherwise
|
|
73
77
|
"""
|
|
74
78
|
try:
|
|
79
|
+
# Use PTY to get real progress output from docker
|
|
80
|
+
master_fd, slave_fd = pty.openpty()
|
|
81
|
+
|
|
75
82
|
process = subprocess.Popen(
|
|
76
83
|
["docker", "pull", image],
|
|
77
|
-
stdout=
|
|
78
|
-
stderr=
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
stdout=slave_fd,
|
|
85
|
+
stderr=slave_fd,
|
|
86
|
+
stdin=slave_fd,
|
|
87
|
+
close_fds=True,
|
|
81
88
|
)
|
|
82
89
|
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
os.close(slave_fd) # Close slave in parent
|
|
91
|
+
|
|
92
|
+
# Read output from master with timeout
|
|
93
|
+
output_buffer = ""
|
|
94
|
+
# Regex to parse docker progress: "abc123: Downloading [===> ] 10.5MB/50MB"
|
|
95
|
+
progress_pattern = re.compile(
|
|
96
|
+
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))?"
|
|
97
|
+
)
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
while True:
|
|
100
|
+
# Check if process is done
|
|
101
|
+
ret = process.poll()
|
|
102
|
+
if ret is not None:
|
|
103
|
+
# Read any remaining output
|
|
104
|
+
try:
|
|
105
|
+
while True:
|
|
106
|
+
ready, _, _ = select.select([master_fd], [], [], 0.1)
|
|
107
|
+
if not ready:
|
|
108
|
+
break
|
|
109
|
+
chunk = os.read(master_fd, 4096)
|
|
110
|
+
if not chunk:
|
|
111
|
+
break
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
# Read available output
|
|
117
|
+
try:
|
|
118
|
+
ready, _, _ = select.select([master_fd], [], [], 0.1)
|
|
119
|
+
if ready:
|
|
120
|
+
chunk = os.read(master_fd, 4096)
|
|
121
|
+
if chunk:
|
|
122
|
+
output_buffer += chunk.decode("utf-8", errors="replace")
|
|
123
|
+
|
|
124
|
+
# Parse and report progress
|
|
125
|
+
# Docker uses carriage returns to update lines in place
|
|
126
|
+
lines = output_buffer.replace("\r", "\n").split("\n")
|
|
127
|
+
output_buffer = lines[-1] # Keep incomplete line
|
|
128
|
+
|
|
129
|
+
for line in lines[:-1]:
|
|
130
|
+
line = line.strip()
|
|
131
|
+
# Strip ANSI escape codes
|
|
132
|
+
line = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", line)
|
|
133
|
+
if line and on_progress:
|
|
134
|
+
on_progress(image, line)
|
|
135
|
+
except OSError:
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
os.close(master_fd)
|
|
90
139
|
success = process.returncode == 0
|
|
91
140
|
|
|
92
141
|
if on_complete:
|
|
@@ -94,7 +143,7 @@ def pull_image_with_progress(
|
|
|
94
143
|
|
|
95
144
|
return success
|
|
96
145
|
|
|
97
|
-
except (
|
|
146
|
+
except (FileNotFoundError, OSError) as e:
|
|
98
147
|
logger.error(f"Failed to pull {image}: {e}")
|
|
99
148
|
if on_complete:
|
|
100
149
|
on_complete(image, False)
|
|
@@ -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=Aw7KmH4irG6FlCEfNOWcVmVRncARVQAifrhZcqXu2hs,44529
|
|
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=Zn-mroqaLHNzdT4DFf8st1Qclglshihdc09dBfCN070,5522
|
|
8
|
-
hte_cli/image_utils.py,sha256=
|
|
8
|
+
hte_cli/image_utils.py,sha256=TLwJdswUQrSD2bQcAXW03R8j8WG2pbHzd12TWcE7zy4,6418
|
|
9
9
|
hte_cli/runner.py,sha256=DhC8FMjHwfLR193iP4thLDRZrNssYA9KH1WYKU2JKeg,13535
|
|
10
10
|
hte_cli/scorers.py,sha256=sFoPJePRt-K191-Ga4cVmrldruJclYXTOLkU_C9nCDI,6025
|
|
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.10.dist-info/METADATA,sha256=FBr7HFYE_HjrjCOoEIn-cM2b3t-L07HXvkwAqfK0kPw,3768
|
|
13
|
+
hte_cli-0.2.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
hte_cli-0.2.10.dist-info/entry_points.txt,sha256=XbyEEi1H14DFAt0Kdl22e_IRVEGzimSzYSh5HlhKlFA,41
|
|
15
|
+
hte_cli-0.2.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|