Skip to content

Commit 5a3489b

Browse files
committed
feat: v0.4.0 — defork progress/bar.py to codechu-meter + codechu-fmt
ProgressBar's three inline helpers (_RateEstimator, _fmt_duration, _fmt_rate) duplicated what codechu-meter and codechu-fmt already provide. With codechu-fmt v0.4.0 exposing integer_seconds, precision='auto', and bare_items, the inline versions can go. - progress/bar.py: import RateEstimator from codechu_meter and format_duration / format_rate from codechu_fmt. Call sites: eta_str = format_duration(eta_s, integer_seconds=True) elapsed = format_duration(elapsed, integer_seconds=True) rate_str = format_rate(rate, unit=..., precision="auto", bare_items=False) ~19 lines net smaller. - pyproject.toml: new required dependencies codechu-fmt>=0.4,<0.5 codechu-meter>=0.3,<0.4 Both pure-stdlib, no transitive deps. - description trimmed: dropped "stdlib-only" since cli now pulls two tiny Codechu siblings. - Output regression noted in CHANGELOG: very long ETAs now show as "1h Xm" instead of "61m 40s" (was a quirk of the forked helper; hour expansion is the family-wide convention). 182/182 tests pass.
1 parent 1c50d8f commit 5a3489b

4 files changed

Lines changed: 37 additions & 56 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@
44

55
## [Unreleased]
66

7+
## [0.4.0] - 2026-05-20
8+
9+
### Changed
10+
- **Defork**: `ProgressBar` now uses `codechu_meter.RateEstimator` and
11+
`codechu_fmt.format_duration` / `format_rate` directly. The inline
12+
`_RateEstimator` / `_fmt_duration` / `_fmt_rate` helpers in
13+
`progress/bar.py` are removed.
14+
- **Visible output for very long ETAs**: duration formatting now
15+
promotes ≥3600 s to ``"1h Xm"`` (was ``"61m 40s"``). This is the
16+
documented improvement of the consolidation; matches the rest of
17+
the Codechu fmt family.
18+
19+
### Dependencies
20+
- New required dependencies: ``codechu-fmt>=0.4,<0.5`` and
21+
``codechu-meter>=0.3,<0.4``. Both are pure-stdlib, no transitive
22+
deps.
23+
724
## [0.3.0] - 2026-05-20
825

926
### Added

‎pyproject.toml‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "codechu-cli"
7-
version = "0.3.0"
8-
description = "Stdlib-only CLI primitives — colors, progress, prompts, spinners, emoji helpers — with consistent UX across Codechu tools."
7+
version = "0.4.0"
8+
description = "CLI primitives — colors, progress, prompts, spinners, emoji helpers — with consistent UX across Codechu tools."
99
readme = "README.md"
1010
requires-python = ">=3.10"
1111
license = { file = "LICENSE" }
@@ -25,7 +25,10 @@ classifiers = [
2525
"Topic :: Software Development :: Libraries",
2626
"Topic :: Terminals",
2727
]
28-
dependencies = []
28+
dependencies = [
29+
"codechu-fmt>=0.4,<0.5",
30+
"codechu-meter>=0.3,<0.4",
31+
]
2932

3033
[project.optional-dependencies]
3134
dev = [

‎src/codechu_cli/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from .prompt import confirm, multiselect, prompt, select
3131
from .table import Table
3232

33-
__version__ = "0.3.0"
33+
__version__ = "0.4.0"
3434

3535
__all__ = [
3636
"BAR_STYLES",

‎src/codechu_cli/progress/bar.py‎

Lines changed: 13 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,17 @@
44

55
import sys
66
import time
7-
from collections import deque
87
from typing import IO
98

9+
from codechu_fmt import format_duration, format_rate
10+
from codechu_meter import RateEstimator
11+
1012
from .._term import is_tty
1113
from .line import ProgressLine
1214
from .styles_bar import BAR_STYLES, DEFAULT_BAR_STYLE, SUBPIXEL
1315
from .styles_spinner import SPINNER_STYLES
1416

1517

16-
def _fmt_duration(secs: float) -> str:
17-
"""Compact duration: ``Xs`` under 60 s, ``Xm Ys`` otherwise."""
18-
if secs < 0:
19-
secs = 0.0
20-
s = int(round(secs))
21-
if s < 60:
22-
return f"{s}s"
23-
return f"{s // 60}m {s % 60}s"
24-
25-
26-
def _fmt_rate(rate: float, unit: str) -> str:
27-
"""Compact rate string like ``42 items/s``."""
28-
if rate >= 100:
29-
return f"{int(round(rate))} {unit}/s"
30-
if rate >= 10:
31-
return f"{rate:.1f} {unit}/s"
32-
return f"{rate:.2f} {unit}/s"
33-
34-
35-
class _RateEstimator:
36-
"""Tiny windowed rate estimator (events per second)."""
37-
38-
def __init__(self, window_seconds: float = 1.0) -> None:
39-
self._window = window_seconds
40-
self._events: deque[tuple[float, float]] = deque() # (ts, n)
41-
42-
def observe(self, n: float = 1) -> None:
43-
now = time.monotonic()
44-
self._events.append((now, float(n)))
45-
cutoff = now - self._window
46-
while self._events and self._events[0][0] < cutoff:
47-
self._events.popleft()
48-
49-
def rate(self) -> float:
50-
if not self._events:
51-
return 0.0
52-
now = time.monotonic()
53-
cutoff = now - self._window
54-
while self._events and self._events[0][0] < cutoff:
55-
self._events.popleft()
56-
if not self._events:
57-
return 0.0
58-
total = sum(n for _, n in self._events)
59-
return total / self._window
60-
61-
6218
class ProgressBar:
6319
"""Bracketed progress bar with percent + count.
6420
@@ -114,7 +70,7 @@ def __init__(
11470
self._indeterm_idx = 0
11571
self._t_start = time.monotonic()
11672
self._last_advance = time.monotonic()
117-
self._rate = _RateEstimator(window_seconds=1.0)
73+
self._rate = RateEstimator(window_seconds=1.0)
11874

11975
# Lazy: line is constructed on first render so stream/enabled
12076
# changes via the builder are respected.
@@ -336,7 +292,7 @@ def _render(self, *, label: str) -> None:
336292
pct_str = str(int(round(ratio * 100)))
337293
if self.total > 0 and self.current > 0:
338294
eta_s = elapsed * (self.total - self.current) / self.current
339-
eta_str = _fmt_duration(max(0.0, eta_s))
295+
eta_str = format_duration(max(0.0, eta_s), integer_seconds=True)
340296
else:
341297
eta_str = "?"
342298
remaining_str = str(max(0, self.total - self.current))
@@ -347,7 +303,7 @@ def _render(self, *, label: str) -> None:
347303
pct_str = str(int(round(ratio * 100)))
348304
if self.total > 0 and self.current > 0:
349305
eta_s = elapsed * (self.total - self.current) / self.current
350-
eta_str = _fmt_duration(max(0.0, eta_s))
306+
eta_str = format_duration(max(0.0, eta_s), integer_seconds=True)
351307
else:
352308
eta_str = "?"
353309
remaining_str = str(max(0, self.total - self.current))
@@ -359,15 +315,20 @@ def _render(self, *, label: str) -> None:
359315
if self._indeterminate or elapsed < 0.5 or self._rate.rate() <= 0:
360316
rate_str = "?"
361317
else:
362-
rate_str = _fmt_rate(self._rate.rate(), self._units or "items")
318+
rate_str = format_rate(
319+
self._rate.rate(),
320+
unit=self._units or "items",
321+
precision="auto",
322+
bare_items=False,
323+
)
363324

364325
msg = self._template.format(
365326
bar=bar_str,
366327
pct=pct_str,
367328
current=self.current,
368329
total=self.total,
369330
label=label,
370-
elapsed=_fmt_duration(elapsed),
331+
elapsed=format_duration(elapsed, integer_seconds=True),
371332
eta=eta_str,
372333
spinner=spinner_str,
373334
remaining=remaining_str,

0 commit comments

Comments
 (0)