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 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
- last_status = ["connecting..."] # Use list for closure mutability
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 docker pull output for layer progress
318
- # Lines look like: "abc123: Downloading [====> ] 10MB/50MB"
319
- # Or: "abc123: Extracting [====> ] 10MB/50MB"
320
- # Or: "abc123: Pull complete", "Digest: sha256:...", "Status: ..."
321
- display_text = None
322
- if ": " in line:
323
- parts = line.split(": ", 1)
324
- if len(parts) == 2:
325
- layer_id = parts[0][-8:] # Last 8 chars of layer ID
326
- layer_status = parts[1] # Don't truncate - keep full progress
327
- # Show progress bars for Downloading/Extracting with MB info
328
- if "Downloading" in layer_status or "Extracting" in layer_status:
329
- # Keep progress: "[====> ] 10.5MB/50MB"
330
- display_text = f"{layer_id}: {layer_status[:50]}"
331
- elif "Pull complete" in layer_status:
332
- display_text = f"{layer_id}: done"
333
- elif "Download complete" in layer_status:
334
- display_text = f"{layer_id}: download done"
335
- elif "Already exists" in layer_status:
336
- display_text = f"{layer_id}: cached"
337
- elif "Waiting" in layer_status:
338
- display_text = f"{layer_id}: waiting"
339
- elif "Verifying" in layer_status:
340
- display_text = f"{layer_id}: verifying"
341
- else:
342
- display_text = line[:55]
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
- display_text = line[:55]
383
+ display = line[:40]
384
+ else:
385
+ return
345
386
 
346
- if display_text and display_text != last_status[0]:
347
- last_status[0] = display_text
348
- status.update(f"[yellow]↓[/yellow] {short_name} [dim]{display_text}[/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 line of output
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=subprocess.PIPE,
78
- stderr=subprocess.STDOUT,
79
- text=True,
80
- bufsize=1,
84
+ stdout=slave_fd,
85
+ stderr=slave_fd,
86
+ stdin=slave_fd,
87
+ close_fds=True,
81
88
  )
82
89
 
83
- # Stream output line by line
84
- for line in iter(process.stdout.readline, ""):
85
- line = line.strip()
86
- if line and on_progress:
87
- on_progress(image, line)
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
- process.wait()
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 (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hte-cli
3
- Version: 0.2.8
3
+ Version: 0.2.10
4
4
  Summary: Human Time-to-Completion Evaluation CLI
5
5
  Project-URL: Homepage, https://github.com/sean-peters-au/lyptus-mono
6
6
  Author: Lyptus Research
@@ -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=W9R_jHBLhLho2GyroKzCCg6EhBluCrFJdZ9zCaKFGuo,42745
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=454yoZEI1duNYrZC8UjhfZzDRP4Nxdrf2TvnZ_54G1k,4439
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.8.dist-info/METADATA,sha256=XT1WLIfvC2PRQtcFB19qTpBdMApP_UKabzal-Fcn8Cw,3767
13
- hte_cli-0.2.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- hte_cli-0.2.8.dist-info/entry_points.txt,sha256=XbyEEi1H14DFAt0Kdl22e_IRVEGzimSzYSh5HlhKlFA,41
15
- hte_cli-0.2.8.dist-info/RECORD,,
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,,