arborist/arborist/progress.py
russell@unturf.com 2c9d00ac95
modified: CLAUDE.md
modified:   Makefile
	modified:   README.md
	deleted:    aborist/search/__init__.py
	renamed:    aborist/__init__.py -> arborist/__init__.py
	renamed:    aborist/cli.py -> arborist/cli.py
	renamed:    aborist/compress.py -> arborist/compress.py
	renamed:    aborist/concepts/__init__.py -> arborist/concepts/__init__.py
	renamed:    aborist/concepts/extract.py -> arborist/concepts/extract.py
	renamed:    aborist/concepts/query.py -> arborist/concepts/query.py
	renamed:    aborist/concepts/seed.py -> arborist/concepts/seed.py
	renamed:    aborist/concepts/store.py -> arborist/concepts/store.py
	renamed:    aborist/distill/__init__.py -> arborist/distill/__init__.py
	renamed:    aborist/distill/base.py -> arborist/distill/base.py
	renamed:    aborist/distill/first_sentence.py -> arborist/distill/first_sentence.py
	renamed:    aborist/distill/runner.py -> arborist/distill/runner.py
	renamed:    aborist/distill/tfidf.py -> arborist/distill/tfidf.py
	renamed:    aborist/document.py -> arborist/document.py
	renamed:    aborist/evict.py -> arborist/evict.py
	renamed:    aborist/ingest.py -> arborist/ingest.py
	renamed:    aborist/journal.py -> arborist/journal.py
	renamed:    aborist/merkle.py -> arborist/merkle.py
	renamed:    aborist/mesh/__init__.py -> arborist/mesh/__init__.py
	renamed:    aborist/mesh/crypto.py -> arborist/mesh/crypto.py
	renamed:    aborist/mesh/members.py -> arborist/mesh/members.py
	renamed:    aborist/mesh/state.py -> arborist/mesh/state.py
	renamed:    aborist/mesh/wire.py -> arborist/mesh/wire.py
	renamed:    aborist/progress.py -> arborist/progress.py
	renamed:    aborist/qa/__init__.py -> arborist/qa/__init__.py
	renamed:    aborist/qa/client.py -> arborist/qa/client.py
	renamed:    aborist/qa/concepts.py -> arborist/qa/concepts.py
	renamed:    aborist/qa/dag.py -> arborist/qa/dag.py
	renamed:    aborist/qa/evidence.py -> arborist/qa/evidence.py
	renamed:    aborist/qa/frame.py -> arborist/qa/frame.py
	renamed:    aborist/qa/inspect.py -> arborist/qa/inspect.py
	renamed:    aborist/qa/keys.py -> arborist/qa/keys.py
	renamed:    aborist/qa/metacognition.py -> arborist/qa/metacognition.py
	renamed:    aborist/qa/model_profiles.py -> arborist/qa/model_profiles.py
	renamed:    aborist/qa/parse_claims.py -> arborist/qa/parse_claims.py
	renamed:    aborist/qa/prompts.py -> arborist/qa/prompts.py
	renamed:    aborist/qa/quantifier.py -> arborist/qa/quantifier.py
	renamed:    aborist/qa/quantifier_reminder.py -> arborist/qa/quantifier_reminder.py
	renamed:    aborist/qa/query.py -> arborist/qa/query.py
	renamed:    aborist/qa/repair.py -> arborist/qa/repair.py
	renamed:    aborist/qa/retrieval_plan.py -> arborist/qa/retrieval_plan.py
	renamed:    aborist/qa/runner.py -> arborist/qa/runner.py
	renamed:    aborist/qa/soft_preflight.py -> arborist/qa/soft_preflight.py
	renamed:    aborist/qa/verify.py -> arborist/qa/verify.py
	renamed:    aborist/qa/warrant.py -> arborist/qa/warrant.py
	new file:   arborist/search/__init__.py
	renamed:    aborist/search/base.py -> arborist/search/base.py
	renamed:    aborist/search/fts5.py -> arborist/search/fts5.py
	renamed:    aborist/snapshot.py -> arborist/snapshot.py
	renamed:    aborist/source.py -> arborist/source.py
	renamed:    aborist/sources/__init__.py -> arborist/sources/__init__.py
	renamed:    aborist/sources/crawler/__init__.py -> arborist/sources/crawler/__init__.py
	renamed:    aborist/sources/crawler/async_web_fetcher.py -> arborist/sources/crawler/async_web_fetcher.py
	renamed:    aborist/sources/crawler/bridge.py -> arborist/sources/crawler/bridge.py
	renamed:    aborist/sources/crawler/web_fetch.py -> arborist/sources/crawler/web_fetch.py
	renamed:    aborist/sources/grok.py -> arborist/sources/grok.py
	renamed:    aborist/sources/html_page.py -> arborist/sources/html_page.py
	renamed:    aborist/sources/providence.py -> arborist/sources/providence.py
	renamed:    aborist/sources/vcs.py -> arborist/sources/vcs.py
	renamed:    aborist/sources/wikipedia.py -> arborist/sources/wikipedia.py
	renamed:    aborist/sources/wikipedia_xml.py -> arborist/sources/wikipedia_xml.py
	renamed:    aborist/store.py -> arborist/store.py
	renamed:    aborist/wikitext.py -> arborist/wikitext.py
	modified:   pyproject.toml
2026-05-07 09:24:46 -04:00

86 lines
2.6 KiB
Python

"""Periodic stderr progress reporting for long-running ingests/distillation.
Stdlib only. Prints on a rate-limited schedule (default every 2 seconds) so
ingests of 100k+ docs give live feedback without spamming. Optional total
estimate produces a percent + ETA.
Multiple parallel shard processes will interleave their lines; each can be
given a `prefix` to disambiguate.
"""
from __future__ import annotations
import sys
import time
from typing import IO
class Progress:
"""Rate-limited progress reporter to stderr (or any stream)."""
def __init__(
self,
*,
interval: float = 2.0,
total_estimate: int | None = None,
prefix: str = "",
stream: IO[str] = sys.stderr,
):
self.start = time.monotonic()
self.last_print = self.start
self.last_seen = 0
self.interval = interval
self.total_estimate = total_estimate
self.prefix = prefix
self.stream = stream
def tick(
self,
seen: int,
*,
inserted: int | None = None,
force: bool = False,
) -> None:
"""Maybe print a status line. Call after each batch flush."""
now = time.monotonic()
if not force and (now - self.last_print) < self.interval:
return
elapsed = now - self.start
delta_seen = max(0, seen - self.last_seen)
delta_t = max(now - self.last_print, 1e-6)
rate_now = delta_seen / delta_t
rate_avg = seen / elapsed if elapsed > 0 else 0.0
bits = [
f"{self.prefix}[{_format_elapsed(elapsed)}]",
f"{seen:>10,} seen",
]
if inserted is not None:
bits.append(f"{inserted:>9,} new")
bits.append(f"{rate_now:>5.0f} now")
bits.append(f"({rate_avg:>4.0f} avg) docs/s")
if self.total_estimate and rate_now > 0:
pct = 100.0 * min(1.0, seen / self.total_estimate)
remaining = max(0, self.total_estimate - seen)
eta = remaining / rate_now
bits.append(f"{pct:>5.1f}%")
bits.append(f"ETA {_format_elapsed(eta)}")
print(" " + " | ".join(bits), file=self.stream, flush=True)
self.last_print = now
self.last_seen = seen
def done(self, seen: int, *, inserted: int | None = None) -> None:
"""Print a final status line regardless of interval."""
self.tick(seen, inserted=inserted, force=True)
def _format_elapsed(secs: float) -> str:
secs = int(secs)
h, rem = divmod(secs, 3600)
m, s = divmod(rem, 60)
if h:
return f"{h}h{m:02d}m"
if m:
return f"{m:>2}m{s:02d}s"
return f" {s:>2}s"