Add download progress tracking and byte formatting utilities

- Introduced `fmt_bytes` function for human-readable byte size formatting.
- Added `DownloadProgress` class to track and display download progress with speed and completion percentage.
- Updated `download` functions in `civitai_fetch.py` and `llamacpp_fetch.py` to utilize progress tracking.
- Enhanced `ollama_pull.py` to support streaming progress updates during model pulls.
- Updated tests to validate new formatting and progress tracking functionalities, ensuring accurate output and user feedback.
This commit is contained in:
Leonid Pershin
2026-08-21 07:16:04 +03:00
parent adba4976ee
commit 4186d0bcf1
4 changed files with 259 additions and 25 deletions
+74 -7
View File
@@ -1,15 +1,43 @@
#!/usr/bin/env python3
"""Pull Ollama models from a JSON list. Stdlib only. Runs on the VM."""
"""Pull Ollama models from a JSON list. Stdlib only. Runs on the VM.
Uses POST /api/pull with stream JSON for completed/total + speed lines.
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
JOBS = Path("/tmp/gpu-rent-ollama-models.json")
MARKER = Path("/mnt/swarm_data/.gpu-rent-ollama-pulling")
OLLAMA = "http://127.0.0.1:11434"
def fmt_bytes(n: float) -> str:
n = float(n)
for unit, div in (("GB", 1024**3), ("MB", 1024**2), ("KB", 1024), ("B", 1)):
if n >= div or unit == "B":
if unit == "B":
return f"{int(n)}B"
return f"{n / div:.1f}{unit}"
return f"{n:.0f}B"
def progress_line(label: str, done: int, total: int | None, speed: float, *, width: int = 22) -> str:
if total and total > 0:
pct = min(100.0, 100.0 * done / total)
filled = min(width, max(0, int(width * done / total)))
bar = "#" * filled + "-" * (width - filled)
return (
f"{label} [{bar}] {pct:5.1f}% "
f"{fmt_bytes(done)}/{fmt_bytes(total)} {fmt_bytes(speed)}/s"
)
return f"{label} {fmt_bytes(done)} {fmt_bytes(speed)}/s"
def listed() -> set[str]:
@@ -32,7 +60,6 @@ def already_have(have: set[str], wanted: str) -> bool:
"""Exact tag match only — qwen2.5:3b must not satisfy qwen2.5:7b."""
if wanted in have:
return True
# ollama list sometimes omits :latest
if ":" not in wanted and f"{wanted}:latest" in have:
return True
if wanted.endswith(":latest") and wanted.rsplit(":", 1)[0] in have:
@@ -40,6 +67,45 @@ def already_have(have: set[str], wanted: str) -> bool:
return False
def pull_stream(name: str, label: str) -> None:
body = json.dumps({"name": name, "stream": True}).encode("utf-8")
req = urllib.request.Request(
f"{OLLAMA}/api/pull",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
t0 = time.monotonic()
last_print = 0.0
with urllib.request.urlopen(req, timeout=7200) as resp:
while True:
raw = resp.readline()
if not raw:
break
try:
ev = json.loads(raw.decode("utf-8"))
except json.JSONDecodeError:
continue
if ev.get("error"):
raise RuntimeError(str(ev["error"]))
status = str(ev.get("status") or "")
total = int(ev.get("total") or 0) or None
done = int(ev.get("completed") or 0)
now = time.monotonic()
if total and done:
if now - last_print >= 1.0 or done >= total:
elapsed = max(now - t0, 0.001)
speed = done / elapsed
print(progress_line(label, done, total, speed), flush=True)
last_print = now
elif status and status not in {"success"} and now - last_print >= 2.0:
print(f"{label} {status}", flush=True)
last_print = now
if status == "success":
break
print(f"{label} ok", flush=True)
def main() -> int:
if not JOBS.is_file():
print("no jobs file", file=sys.stderr)
@@ -57,14 +123,15 @@ def main() -> int:
name = str(name).strip()
if not name:
continue
prefix = f"[{i}/{len(models)}]"
if already_have(have, name):
print(f"[{i}/{len(models)}] уже есть {name}")
print(f"{prefix} уже есть {name}")
continue
print(f"[{i}/{len(models)}] ollama pull {name}")
print(f"{prefix} ollama pull {name}", flush=True)
try:
subprocess.check_call(["ollama", "pull", name])
pull_stream(name, label=f"{prefix} {name}")
have.add(name)
except subprocess.CalledProcessError as exc:
except (urllib.error.URLError, urllib.error.HTTPError, OSError, TimeoutError, RuntimeError) as exc:
failed += 1
print(f"FAIL pull {name}: {exc}", file=sys.stderr)
finally:
@@ -76,4 +143,4 @@ def main() -> int:
if __name__ == "__main__":
sys.exit(main())
raise SystemExit(main())