asyncprogress 0.3.1__tar.gz → 0.3.5__tar.gz
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.
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/PKG-INFO +10 -8
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/README.md +5 -3
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/pyproject.toml +14 -18
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/__init__.py +7 -7
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/_bar.py +159 -114
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/_eta.py +10 -9
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/_gather.py +22 -23
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/_iterator.py +23 -21
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/_renderer.py +58 -45
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/_terminal.py +5 -5
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/LICENSE +0 -0
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/_as_completed.py +0 -0
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/_multi.py +0 -0
- {asyncprogress-0.3.1 → asyncprogress-0.3.5}/src/asyncprogress/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asyncprogress
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: Async-aware progress bars for Python's asyncio ecosystem
|
|
3
|
+
Version: 0.3.5
|
|
4
|
+
Summary: Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Keywords: asyncio,progress,progress-bar,async,tqdm
|
|
@@ -19,11 +19,11 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
23
|
Provides-Extra: color
|
|
24
24
|
Requires-Dist: colorama (>=0.4.4) ; extra == "color"
|
|
25
|
-
Project-URL: Homepage, https://github.com/
|
|
26
|
-
Project-URL: Repository, https://github.com/
|
|
25
|
+
Project-URL: Homepage, https://github.com/agentsoft/asyncprogress
|
|
26
|
+
Project-URL: Repository, https://github.com/agentsoft/asyncprogress
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
# asyncprogress
|
|
@@ -36,13 +36,15 @@ Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory depende
|
|
|
36
36
|
|
|
37
37
|
## Why asyncprogress?
|
|
38
38
|
|
|
39
|
-
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were
|
|
39
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were designed for
|
|
40
|
+
synchronous code. `asyncprogress` is built from the ground up for `asyncio`:
|
|
40
41
|
|
|
41
|
-
- **Native `async for` support** — wrap any sync or async iterable
|
|
42
|
+
- **Native `async for` support** — wrap any sync or async iterable with one line
|
|
42
43
|
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
43
44
|
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
44
45
|
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
45
|
-
- **
|
|
46
|
+
- **Multiple concurrent bars** — `MultiProgressBar` for parallel pipelines
|
|
47
|
+
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
46
48
|
|
|
47
49
|
## Installation
|
|
48
50
|
|
|
@@ -8,13 +8,15 @@ Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory depende
|
|
|
8
8
|
|
|
9
9
|
## Why asyncprogress?
|
|
10
10
|
|
|
11
|
-
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were
|
|
11
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were designed for
|
|
12
|
+
synchronous code. `asyncprogress` is built from the ground up for `asyncio`:
|
|
12
13
|
|
|
13
|
-
- **Native `async for` support** — wrap any sync or async iterable
|
|
14
|
+
- **Native `async for` support** — wrap any sync or async iterable with one line
|
|
14
15
|
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
15
16
|
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
16
17
|
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
17
|
-
- **
|
|
18
|
+
- **Multiple concurrent bars** — `MultiProgressBar` for parallel pipelines
|
|
19
|
+
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
18
20
|
|
|
19
21
|
## Installation
|
|
20
22
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "asyncprogress"
|
|
3
|
-
version = "0.3.
|
|
4
|
-
description = "Async-aware progress bars for Python's asyncio ecosystem"
|
|
3
|
+
version = "0.3.5"
|
|
4
|
+
description = "Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies."
|
|
5
5
|
authors = ["AgentSoft <agentsoft@example.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
8
|
-
homepage = "https://github.com/
|
|
9
|
-
repository = "https://github.com/
|
|
8
|
+
homepage = "https://github.com/agentsoft/asyncprogress"
|
|
9
|
+
repository = "https://github.com/agentsoft/asyncprogress"
|
|
10
10
|
keywords = ["asyncio", "progress", "progress-bar", "async", "tqdm"]
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Development Status :: 4 - Beta",
|
|
@@ -17,7 +17,7 @@ classifiers = [
|
|
|
17
17
|
"Programming Language :: Python :: 3.10",
|
|
18
18
|
"Programming Language :: Python :: 3.11",
|
|
19
19
|
"Programming Language :: Python :: 3.12",
|
|
20
|
-
"Topic :: Software Development :: Libraries
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
21
|
"Framework :: AsyncIO",
|
|
22
22
|
]
|
|
23
23
|
packages = [{include = "asyncprogress", from = "src"}]
|
|
@@ -42,23 +42,19 @@ build-backend = "poetry.core.masonry.api"
|
|
|
42
42
|
[tool.pytest.ini_options]
|
|
43
43
|
asyncio_mode = "auto"
|
|
44
44
|
testpaths = ["tests"]
|
|
45
|
-
addopts = "--
|
|
46
|
-
|
|
47
|
-
[tool.coverage.run]
|
|
48
|
-
source = ["src/asyncprogress"]
|
|
49
|
-
|
|
50
|
-
[tool.coverage.report]
|
|
51
|
-
exclude_lines = [
|
|
52
|
-
"pragma: no cover",
|
|
53
|
-
"def __repr__",
|
|
54
|
-
"if TYPE_CHECKING:",
|
|
55
|
-
"raise NotImplementedError",
|
|
56
|
-
]
|
|
45
|
+
addopts = "--tb=short"
|
|
57
46
|
|
|
58
47
|
[tool.ruff]
|
|
59
48
|
line-length = 100
|
|
60
49
|
target-version = "py39"
|
|
61
50
|
|
|
62
51
|
[tool.ruff.lint]
|
|
63
|
-
select = ["E", "F", "
|
|
52
|
+
select = ["E", "F", "I", "UP"]
|
|
64
53
|
ignore = ["E501"]
|
|
54
|
+
|
|
55
|
+
[tool.coverage.run]
|
|
56
|
+
source = ["src/asyncprogress"]
|
|
57
|
+
omit = ["tests/*"]
|
|
58
|
+
|
|
59
|
+
[tool.coverage.report]
|
|
60
|
+
show_missing = true
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
asyncprogress — Async-aware progress bars for Python's asyncio ecosystem.
|
|
3
3
|
|
|
4
|
-
Zero mandatory dependencies. Pure stdlib
|
|
4
|
+
Zero mandatory dependencies. Pure Python stdlib.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -13,13 +13,13 @@ from asyncprogress._gather import gather
|
|
|
13
13
|
from asyncprogress._iterator import aprogress
|
|
14
14
|
from asyncprogress._multi import MultiProgressBar
|
|
15
15
|
|
|
16
|
-
__version__ = "0.3.1"
|
|
17
|
-
|
|
18
16
|
__all__ = [
|
|
19
17
|
"aprogress",
|
|
20
|
-
"aprogress_as_completed",
|
|
21
|
-
"EWMACalculator",
|
|
22
|
-
"gather",
|
|
23
|
-
"MultiProgressBar",
|
|
24
18
|
"ProgressBar",
|
|
19
|
+
"MultiProgressBar",
|
|
20
|
+
"gather",
|
|
21
|
+
"EWMACalculator",
|
|
22
|
+
"aprogress_as_completed",
|
|
25
23
|
]
|
|
24
|
+
|
|
25
|
+
__version__ = "0.3.5"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
ProgressBar — core async context manager for progress tracking.
|
|
3
|
+
"""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
4
6
|
|
|
@@ -17,29 +19,35 @@ T = TypeVar("T")
|
|
|
17
19
|
|
|
18
20
|
class ProgressBar:
|
|
19
21
|
"""
|
|
20
|
-
Async context manager
|
|
22
|
+
Async context manager for manual progress tracking.
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for item in data:
|
|
25
|
-
await process(item)
|
|
26
|
-
await bar.update()
|
|
24
|
+
Provides a progress bar with EWMA-based ETA estimation, configurable
|
|
25
|
+
appearance, and non-blocking terminal output.
|
|
27
26
|
|
|
28
27
|
Args:
|
|
29
|
-
total: Total number of steps. None
|
|
28
|
+
total: Total number of steps. None activates spinner mode.
|
|
30
29
|
description: Label shown before the bar.
|
|
31
30
|
bar_width: Width of the bar graphic in characters.
|
|
32
|
-
fill_char: Character
|
|
33
|
-
empty_char: Character
|
|
34
|
-
color: ANSI color name (e.g., "green", "cyan").
|
|
35
|
-
show_eta: Whether to
|
|
36
|
-
show_elapsed: Whether to
|
|
37
|
-
show_count: Whether to
|
|
38
|
-
show_percentage: Whether to
|
|
39
|
-
update_interval:
|
|
40
|
-
ewma_alpha: EWMA smoothing factor (0
|
|
41
|
-
file: Output
|
|
31
|
+
fill_char: Character for completed portion.
|
|
32
|
+
empty_char: Character for remaining portion.
|
|
33
|
+
color: ANSI color name (e.g., "green", "cyan", "red").
|
|
34
|
+
show_eta: Whether to show estimated time remaining.
|
|
35
|
+
show_elapsed: Whether to show elapsed time.
|
|
36
|
+
show_count: Whether to show [completed/total] count.
|
|
37
|
+
show_percentage: Whether to show percentage.
|
|
38
|
+
update_interval: Seconds between terminal redraws.
|
|
39
|
+
ewma_alpha: EWMA smoothing factor (0.1=smooth, 0.5=reactive).
|
|
40
|
+
file: Output stream (default: sys.stderr).
|
|
42
41
|
disable: If True, suppress all output.
|
|
42
|
+
unit: Unit label ("it" for items, "B" for bytes, etc.).
|
|
43
|
+
unit_scale: If True, auto-apply SI prefix (K/M/G).
|
|
44
|
+
unit_divisor: 1000 for SI, 1024 for binary.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
async with ProgressBar(total=100, description="Processing") as bar:
|
|
48
|
+
for item in items:
|
|
49
|
+
await process(item)
|
|
50
|
+
await bar.update()
|
|
43
51
|
"""
|
|
44
52
|
|
|
45
53
|
def __init__(
|
|
@@ -58,6 +66,9 @@ class ProgressBar:
|
|
|
58
66
|
ewma_alpha: float = 0.3,
|
|
59
67
|
file: TextIO = sys.stderr,
|
|
60
68
|
disable: bool = False,
|
|
69
|
+
unit: str = "it",
|
|
70
|
+
unit_scale: bool = False,
|
|
71
|
+
unit_divisor: int = 1000,
|
|
61
72
|
) -> None:
|
|
62
73
|
self._total = total
|
|
63
74
|
self._description = description
|
|
@@ -72,101 +83,48 @@ class ProgressBar:
|
|
|
72
83
|
self._update_interval = update_interval
|
|
73
84
|
self._file = file
|
|
74
85
|
self._disable = disable
|
|
86
|
+
self._unit = unit
|
|
87
|
+
self._unit_scale = unit_scale
|
|
88
|
+
self._unit_divisor = unit_divisor
|
|
75
89
|
|
|
76
90
|
self._completed: int = 0
|
|
77
91
|
self._finished: bool = False
|
|
78
92
|
self._start_time: float = 0.0
|
|
79
93
|
self._last_render_time: float = 0.0
|
|
80
94
|
self._spinner_frame: int = 0
|
|
95
|
+
self._render_task: asyncio.Task[None] | None = None
|
|
96
|
+
self._running: bool = False
|
|
81
97
|
|
|
82
98
|
self._eta_calc = EWMACalculator(alpha=ewma_alpha)
|
|
83
99
|
self._renderer = BarRenderer()
|
|
84
|
-
self._render_task: asyncio.Task[None] | None = None
|
|
85
100
|
|
|
86
101
|
async def __aenter__(self) -> ProgressBar:
|
|
87
102
|
"""Start the progress bar."""
|
|
88
103
|
self._start_time = time.monotonic()
|
|
89
|
-
self._last_render_time =
|
|
90
|
-
self.
|
|
91
|
-
self.
|
|
92
|
-
|
|
93
|
-
self._eta_calc.reset()
|
|
94
|
-
await self._schedule_render()
|
|
104
|
+
self._last_render_time = self._start_time
|
|
105
|
+
self._running = True
|
|
106
|
+
self._render_task = asyncio.create_task(self._render_loop())
|
|
107
|
+
await asyncio.sleep(0) # Yield so render task can initialize
|
|
95
108
|
return self
|
|
96
109
|
|
|
97
110
|
async def __aexit__(self, *args: Any) -> None:
|
|
98
|
-
"""
|
|
111
|
+
"""Stop the progress bar and render final state."""
|
|
99
112
|
await self.finish()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@property
|
|
117
|
-
def elapsed(self) -> float:
|
|
118
|
-
"""Seconds since bar was started."""
|
|
119
|
-
if self._start_time == 0.0:
|
|
120
|
-
return 0.0
|
|
121
|
-
return time.monotonic() - self._start_time
|
|
122
|
-
|
|
123
|
-
@property
|
|
124
|
-
def eta(self) -> float | None:
|
|
125
|
-
"""Estimated seconds remaining, or None if unknown."""
|
|
126
|
-
if self._total is None:
|
|
127
|
-
return None
|
|
128
|
-
remaining = self._total - self._completed
|
|
129
|
-
return self._eta_calc.eta(remaining)
|
|
130
|
-
|
|
131
|
-
@property
|
|
132
|
-
def rate(self) -> float | None:
|
|
133
|
-
"""Current EWMA-smoothed items/second rate."""
|
|
134
|
-
return self._eta_calc.rate()
|
|
135
|
-
|
|
136
|
-
async def update(self, n: int = 1) -> None:
|
|
137
|
-
"""Increment progress by n steps."""
|
|
138
|
-
if self._finished:
|
|
139
|
-
return
|
|
140
|
-
self._completed += n
|
|
141
|
-
if self._total is not None:
|
|
142
|
-
self._completed = min(self._completed, self._total)
|
|
143
|
-
self._eta_calc.record(time.monotonic(), self._completed)
|
|
144
|
-
await self._schedule_render()
|
|
145
|
-
|
|
146
|
-
async def set(self, value: int) -> None:
|
|
147
|
-
"""Set absolute progress to value, clamped to [0, total]."""
|
|
148
|
-
if self._finished:
|
|
149
|
-
return
|
|
150
|
-
if self._total is not None:
|
|
151
|
-
self._completed = max(0, min(value, self._total))
|
|
152
|
-
else:
|
|
153
|
-
self._completed = max(0, value)
|
|
154
|
-
self._eta_calc.record(time.monotonic(), self._completed)
|
|
155
|
-
await self._schedule_render()
|
|
156
|
-
|
|
157
|
-
async def set_description(self, description: str) -> None:
|
|
158
|
-
"""Update the description text dynamically."""
|
|
159
|
-
self._description = description
|
|
160
|
-
await self._schedule_render()
|
|
161
|
-
|
|
162
|
-
async def finish(self) -> None:
|
|
163
|
-
"""Mark as complete regardless of current count. Idempotent."""
|
|
164
|
-
if self._finished:
|
|
165
|
-
return
|
|
166
|
-
self._finished = True
|
|
167
|
-
if self._total is not None:
|
|
168
|
-
self._completed = self._total
|
|
169
|
-
await self._render_final()
|
|
113
|
+
self._running = False
|
|
114
|
+
if self._render_task is not None:
|
|
115
|
+
self._render_task.cancel()
|
|
116
|
+
try:
|
|
117
|
+
await self._render_task
|
|
118
|
+
except asyncio.CancelledError:
|
|
119
|
+
pass
|
|
120
|
+
self._render_task = None
|
|
121
|
+
|
|
122
|
+
async def _render_loop(self) -> None:
|
|
123
|
+
"""Background task that periodically re-renders the bar."""
|
|
124
|
+
while self._running:
|
|
125
|
+
await asyncio.sleep(self._update_interval)
|
|
126
|
+
if self._running and not self._finished:
|
|
127
|
+
self._write_line(self._get_render_string())
|
|
170
128
|
|
|
171
129
|
def _get_render_string(self) -> str:
|
|
172
130
|
"""Build the current render string (spinner or bar)."""
|
|
@@ -181,6 +139,9 @@ class ProgressBar:
|
|
|
181
139
|
color=self._color,
|
|
182
140
|
show_elapsed=self._show_elapsed,
|
|
183
141
|
show_count=self._show_count,
|
|
142
|
+
unit=self._unit,
|
|
143
|
+
unit_scale=self._unit_scale,
|
|
144
|
+
unit_divisor=self._unit_divisor,
|
|
184
145
|
)
|
|
185
146
|
return self._renderer.render(
|
|
186
147
|
description=self._description,
|
|
@@ -197,38 +158,122 @@ class ProgressBar:
|
|
|
197
158
|
show_elapsed=self._show_elapsed,
|
|
198
159
|
show_count=self._show_count,
|
|
199
160
|
show_percentage=self._show_percentage,
|
|
161
|
+
unit=self._unit,
|
|
162
|
+
unit_scale=self._unit_scale,
|
|
163
|
+
unit_divisor=self._unit_divisor,
|
|
200
164
|
)
|
|
201
165
|
|
|
202
|
-
|
|
203
|
-
"""Write a
|
|
204
|
-
if self._disable:
|
|
205
|
-
return
|
|
206
|
-
now = time.monotonic()
|
|
207
|
-
if now - self._last_render_time >= self._update_interval:
|
|
208
|
-
self._last_render_time = now
|
|
209
|
-
self._write_line(self._get_render_string())
|
|
210
|
-
|
|
211
|
-
def _write_line(self, line: str) -> None:
|
|
212
|
-
"""Write a single progress line to the output file."""
|
|
166
|
+
def _write_line(self, text: str) -> None:
|
|
167
|
+
"""Write a progress line to the output file."""
|
|
213
168
|
if self._disable:
|
|
214
169
|
return
|
|
215
170
|
if is_tty(self._file):
|
|
216
|
-
self._file.write(erase_line() +
|
|
171
|
+
self._file.write(erase_line() + text)
|
|
217
172
|
else:
|
|
218
|
-
self._file.write(
|
|
173
|
+
self._file.write("\r" + text)
|
|
219
174
|
self._file.flush()
|
|
220
175
|
|
|
221
176
|
async def _render_final(self) -> None:
|
|
222
|
-
"""
|
|
177
|
+
"""Render the final completed state."""
|
|
223
178
|
if self._disable:
|
|
224
179
|
return
|
|
225
180
|
line = self._get_render_string()
|
|
226
181
|
if is_tty(self._file):
|
|
227
182
|
self._file.write(erase_line() + line + "\n")
|
|
228
183
|
else:
|
|
229
|
-
self._file.write(line + "\n")
|
|
184
|
+
self._file.write("\r" + line + "\n")
|
|
230
185
|
self._file.flush()
|
|
231
186
|
|
|
187
|
+
async def _schedule_render(self) -> None:
|
|
188
|
+
"""Trigger an immediate render if enough time has passed."""
|
|
189
|
+
now = time.monotonic()
|
|
190
|
+
if now - self._last_render_time >= self._update_interval:
|
|
191
|
+
self._last_render_time = now
|
|
192
|
+
self._write_line(self._get_render_string())
|
|
193
|
+
|
|
194
|
+
async def update(self, n: int = 1) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Increment progress by n steps.
|
|
197
|
+
|
|
198
|
+
No-op if the bar has already been finished.
|
|
199
|
+
Clamps to total if total is set.
|
|
200
|
+
"""
|
|
201
|
+
if self._finished:
|
|
202
|
+
return
|
|
203
|
+
self._completed += n
|
|
204
|
+
if self._total is not None:
|
|
205
|
+
self._completed = min(self._completed, self._total)
|
|
206
|
+
self._eta_calc.record(time.monotonic(), self._completed)
|
|
207
|
+
await self._schedule_render()
|
|
208
|
+
|
|
209
|
+
async def set(self, value: int) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Set absolute progress to value, clamped to [0, total].
|
|
212
|
+
|
|
213
|
+
No-op if the bar has already been finished.
|
|
214
|
+
"""
|
|
215
|
+
if self._finished:
|
|
216
|
+
return
|
|
217
|
+
if self._total is not None:
|
|
218
|
+
self._completed = max(0, min(value, self._total))
|
|
219
|
+
else:
|
|
220
|
+
self._completed = max(0, value)
|
|
221
|
+
self._eta_calc.record(time.monotonic(), self._completed)
|
|
222
|
+
await self._schedule_render()
|
|
223
|
+
|
|
224
|
+
async def set_description(self, description: str) -> None:
|
|
225
|
+
"""Update the description text dynamically."""
|
|
226
|
+
self._description = description
|
|
227
|
+
await self._schedule_render()
|
|
228
|
+
|
|
229
|
+
async def finish(self) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Mark as complete regardless of current count.
|
|
232
|
+
|
|
233
|
+
Idempotent — safe to call multiple times.
|
|
234
|
+
"""
|
|
235
|
+
if self._finished:
|
|
236
|
+
return
|
|
237
|
+
self._finished = True
|
|
238
|
+
if self._total is not None:
|
|
239
|
+
self._completed = self._total
|
|
240
|
+
await self._render_final()
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def elapsed(self) -> float:
|
|
244
|
+
"""Seconds since bar was started."""
|
|
245
|
+
if self._start_time == 0.0:
|
|
246
|
+
return 0.0
|
|
247
|
+
return time.monotonic() - self._start_time
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def eta(self) -> float | None:
|
|
251
|
+
"""Estimated seconds remaining, or None if unknown."""
|
|
252
|
+
if self._total is None:
|
|
253
|
+
return None
|
|
254
|
+
remaining = self._total - self._completed
|
|
255
|
+
return self._eta_calc.eta(remaining)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def rate(self) -> float | None:
|
|
259
|
+
"""Current EWMA-smoothed items/second rate."""
|
|
260
|
+
return self._eta_calc.rate()
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def description(self) -> str:
|
|
264
|
+
"""The current description label for this progress bar."""
|
|
265
|
+
return self._description
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def completed(self) -> int:
|
|
269
|
+
"""Number of steps completed so far."""
|
|
270
|
+
return self._completed
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def total(self) -> int | None:
|
|
274
|
+
"""Total steps, or None if indeterminate."""
|
|
275
|
+
return self._total
|
|
276
|
+
|
|
232
277
|
@classmethod
|
|
233
278
|
def track(
|
|
234
279
|
cls,
|
|
@@ -241,14 +286,14 @@ class ProgressBar:
|
|
|
241
286
|
"""
|
|
242
287
|
Convenience wrapper: iterate over iterable with a progress bar.
|
|
243
288
|
|
|
244
|
-
Equivalent to aprogress(iterable, total=total, description=description).
|
|
245
|
-
Provided as a classmethod for discoverability.
|
|
289
|
+
Equivalent to aprogress(iterable, total=total, description=description, **kwargs).
|
|
290
|
+
Provided as a classmethod for discoverability when users import only ProgressBar.
|
|
246
291
|
|
|
247
292
|
Args:
|
|
248
293
|
iterable: Any sync or async iterable.
|
|
249
294
|
total: Total item count. Inferred from __len__ if available.
|
|
250
295
|
description: Label shown on the progress bar.
|
|
251
|
-
**kwargs: Forwarded to ProgressBar.
|
|
296
|
+
**kwargs: Forwarded to ProgressBar constructor.
|
|
252
297
|
|
|
253
298
|
Yields:
|
|
254
299
|
Items from the iterable, in order.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
EWMA-based ETA calculator for asyncprogress.
|
|
3
3
|
|
|
4
|
-
Pure math
|
|
4
|
+
Pure math — no I/O, fully unit-testable.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -11,16 +11,17 @@ class EWMACalculator:
|
|
|
11
11
|
"""
|
|
12
12
|
Exponential Weighted Moving Average calculator for ETA estimation.
|
|
13
13
|
|
|
14
|
-
Uses EWMA over completion
|
|
15
|
-
which handles bursty async I/O
|
|
14
|
+
Uses EWMA over completion timestamps rather than simple linear regression,
|
|
15
|
+
which better handles the bursty completion patterns common in async I/O.
|
|
16
16
|
|
|
17
17
|
Args:
|
|
18
|
-
alpha: Smoothing factor
|
|
18
|
+
alpha: Smoothing factor (0 < alpha <= 1).
|
|
19
|
+
Higher values give more weight to recent samples.
|
|
19
20
|
Recommended range: 0.1 (smooth) to 0.5 (reactive).
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
def __init__(self, alpha: float = 0.3) -> None:
|
|
23
|
-
if not (0 < alpha <= 1
|
|
24
|
+
if not (0 < alpha <= 1):
|
|
24
25
|
raise ValueError(f"alpha must be in (0, 1], got {alpha}")
|
|
25
26
|
self._alpha = alpha
|
|
26
27
|
self._ewma_rate: float | None = None
|
|
@@ -40,16 +41,16 @@ class EWMACalculator:
|
|
|
40
41
|
self._ewma_rate = (
|
|
41
42
|
self._alpha * instant_rate + (1 - self._alpha) * self._ewma_rate
|
|
42
43
|
)
|
|
43
|
-
# If dt == 0,
|
|
44
|
+
# If dt == 0, we skip updating the rate (same timestamp)
|
|
44
45
|
self._last_timestamp = timestamp
|
|
45
46
|
self._last_completed = completed
|
|
46
47
|
|
|
47
48
|
def rate(self) -> float | None:
|
|
48
|
-
"""
|
|
49
|
+
"""Return smoothed items/second, or None if insufficient data."""
|
|
49
50
|
return self._ewma_rate
|
|
50
51
|
|
|
51
52
|
def eta(self, remaining: int) -> float | None:
|
|
52
|
-
"""
|
|
53
|
+
"""Return estimated seconds to completion, or None if unknown."""
|
|
53
54
|
if remaining == 0:
|
|
54
55
|
return 0.0
|
|
55
56
|
r = self.rate()
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
gather() — Drop-in replacement for asyncio.gather() with progress tracking.
|
|
3
|
-
"""
|
|
1
|
+
"""gather() — drop-in replacement for asyncio.gather() with progress tracking."""
|
|
4
2
|
|
|
5
3
|
from __future__ import annotations
|
|
6
4
|
|
|
@@ -20,22 +18,22 @@ async def gather(
|
|
|
20
18
|
"""
|
|
21
19
|
Drop-in replacement for asyncio.gather() with progress tracking.
|
|
22
20
|
|
|
23
|
-
Returns results in the same order as inputs
|
|
21
|
+
Returns results in the same order as inputs.
|
|
24
22
|
|
|
25
23
|
Args:
|
|
26
24
|
*coros_or_tasks: Coroutines or Tasks to run concurrently.
|
|
27
25
|
description: Label shown on the progress bar.
|
|
28
|
-
return_exceptions: If True, exceptions are returned as
|
|
29
|
-
rather than raised (mirrors asyncio.gather behavior).
|
|
26
|
+
return_exceptions: If True, exceptions are returned as values.
|
|
30
27
|
**progress_kwargs: Forwarded to ProgressBar (color, bar_width, etc.)
|
|
31
28
|
|
|
32
29
|
Returns:
|
|
33
|
-
List of results in
|
|
30
|
+
List of results in submission order.
|
|
34
31
|
|
|
35
32
|
Example:
|
|
36
33
|
results = await gather(
|
|
37
34
|
*[fetch(url) for url in urls],
|
|
38
|
-
description="
|
|
35
|
+
description="Fetching",
|
|
36
|
+
color="cyan",
|
|
39
37
|
)
|
|
40
38
|
"""
|
|
41
39
|
if not coros_or_tasks:
|
|
@@ -48,26 +46,27 @@ async def gather(
|
|
|
48
46
|
) as bar:
|
|
49
47
|
# Wrap each coroutine/task to update the bar on completion
|
|
50
48
|
results: list[Any] = [None] * total
|
|
49
|
+
exceptions: list[BaseException | None] = [None] * total
|
|
51
50
|
|
|
52
|
-
async def
|
|
51
|
+
async def _run_one(idx: int, coro: Any) -> None:
|
|
53
52
|
try:
|
|
54
53
|
results[idx] = await coro
|
|
55
|
-
except Exception as exc:
|
|
56
|
-
|
|
57
|
-
results[idx] = exc
|
|
58
|
-
else:
|
|
59
|
-
raise
|
|
54
|
+
except Exception as exc: # noqa: BLE001
|
|
55
|
+
exceptions[idx] = exc
|
|
60
56
|
finally:
|
|
61
57
|
await bar.update()
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for i, coro in enumerate(coros_or_tasks)
|
|
66
|
-
]
|
|
59
|
+
tasks = [asyncio.create_task(_run_one(i, c)) for i, c in enumerate(coros_or_tasks)]
|
|
60
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
# Re-raise or return exceptions based on return_exceptions flag
|
|
63
|
+
final: list[Any] = []
|
|
64
|
+
for i in range(total):
|
|
65
|
+
if exceptions[i] is not None:
|
|
66
|
+
if return_exceptions:
|
|
67
|
+
final.append(exceptions[i])
|
|
68
|
+
else:
|
|
69
|
+
raise exceptions[i] # type: ignore[misc]
|
|
70
70
|
else:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return results
|
|
71
|
+
final.append(results[i])
|
|
72
|
+
return final
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
aprogress() —
|
|
2
|
+
aprogress() — async generator wrapper for iterables.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
@@ -32,50 +32,49 @@ async def aprogress(
|
|
|
32
32
|
unit: str = "it",
|
|
33
33
|
unit_scale: bool = False,
|
|
34
34
|
unit_divisor: int = 1000,
|
|
35
|
-
**kwargs: Any,
|
|
36
35
|
) -> AsyncGenerator[T, None]:
|
|
37
36
|
"""
|
|
38
|
-
Async generator wrapper that
|
|
37
|
+
Async generator wrapper that displays a progress bar while iterating.
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
and no `__len__` is present.
|
|
39
|
+
Automatically infers total from __len__ if available. Falls back to
|
|
40
|
+
spinner mode when total is unknown.
|
|
43
41
|
|
|
44
42
|
Args:
|
|
45
43
|
iterable: Any sync or async iterable.
|
|
46
|
-
total: Total item count. Inferred from
|
|
44
|
+
total: Total item count. Inferred from __len__ if available.
|
|
47
45
|
description: Label shown on the progress bar.
|
|
48
46
|
bar_width: Width of the bar graphic in characters.
|
|
49
47
|
fill_char: Character for completed portion.
|
|
50
|
-
empty_char: Character for
|
|
48
|
+
empty_char: Character for remaining portion.
|
|
51
49
|
color: ANSI color name (e.g., "green", "cyan").
|
|
52
|
-
show_eta:
|
|
53
|
-
show_elapsed:
|
|
54
|
-
show_count:
|
|
55
|
-
show_percentage:
|
|
50
|
+
show_eta: Whether to show estimated time remaining.
|
|
51
|
+
show_elapsed: Whether to show elapsed time.
|
|
52
|
+
show_count: Whether to show [completed/total] count.
|
|
53
|
+
show_percentage: Whether to show percentage.
|
|
56
54
|
update_interval: Seconds between terminal redraws.
|
|
57
55
|
ewma_alpha: EWMA smoothing factor.
|
|
58
|
-
file: Output
|
|
56
|
+
file: Output stream (default: sys.stderr).
|
|
59
57
|
disable: If True, suppress all output.
|
|
60
|
-
unit: Unit label for
|
|
61
|
-
unit_scale:
|
|
62
|
-
unit_divisor:
|
|
58
|
+
unit: Unit label ("it" for items, "B" for bytes, etc.).
|
|
59
|
+
unit_scale: If True, auto-apply SI prefix (K/M/G).
|
|
60
|
+
unit_divisor: 1000 for SI, 1024 for binary.
|
|
63
61
|
|
|
64
62
|
Yields:
|
|
65
63
|
Items from the iterable, in order.
|
|
66
64
|
|
|
67
65
|
Example:
|
|
68
|
-
async for item in aprogress(
|
|
66
|
+
async for item in aprogress(range(100), description="Processing"):
|
|
69
67
|
await process(item)
|
|
70
68
|
"""
|
|
71
69
|
import sys as _sys
|
|
72
70
|
|
|
71
|
+
# Infer total from __len__ if available
|
|
73
72
|
if total is None and hasattr(iterable, "__len__"):
|
|
74
73
|
total = len(iterable) # type: ignore[arg-type]
|
|
75
74
|
|
|
76
75
|
effective_total = total if total is not None else None
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
kwargs: dict[str, Any] = {
|
|
79
78
|
"total": effective_total,
|
|
80
79
|
"description": description,
|
|
81
80
|
"bar_width": bar_width,
|
|
@@ -88,15 +87,17 @@ async def aprogress(
|
|
|
88
87
|
"show_percentage": show_percentage,
|
|
89
88
|
"update_interval": update_interval,
|
|
90
89
|
"ewma_alpha": ewma_alpha,
|
|
91
|
-
"file": file if file is not None else _sys.stderr,
|
|
92
90
|
"disable": disable,
|
|
93
91
|
"unit": unit,
|
|
94
92
|
"unit_scale": unit_scale,
|
|
95
93
|
"unit_divisor": unit_divisor,
|
|
96
94
|
}
|
|
97
|
-
|
|
95
|
+
if file is not None:
|
|
96
|
+
kwargs["file"] = file
|
|
97
|
+
else:
|
|
98
|
+
kwargs["file"] = _sys.stderr
|
|
98
99
|
|
|
99
|
-
async with ProgressBar(**
|
|
100
|
+
async with ProgressBar(**kwargs) as bar:
|
|
100
101
|
count = 0
|
|
101
102
|
if hasattr(iterable, "__aiter__"):
|
|
102
103
|
async for item in iterable: # type: ignore[union-attr]
|
|
@@ -109,5 +110,6 @@ async def aprogress(
|
|
|
109
110
|
yield item
|
|
110
111
|
await bar.update()
|
|
111
112
|
|
|
113
|
+
# If iterable was empty, force completion
|
|
112
114
|
if count == 0:
|
|
113
115
|
await bar.finish()
|
|
@@ -1,45 +1,37 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Terminal rendering for asyncprogress.
|
|
3
3
|
|
|
4
|
-
String formatting only
|
|
4
|
+
String formatting only — no I/O, fully unit-testable.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
SPINNER_FRAMES: list[str] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
"black": "\
|
|
13
|
-
"red": "\
|
|
14
|
-
"green": "\
|
|
15
|
-
"yellow": "\
|
|
16
|
-
"blue": "\
|
|
17
|
-
"magenta": "\
|
|
18
|
-
"cyan": "\
|
|
19
|
-
"white": "\
|
|
20
|
-
"bright_black": "\
|
|
21
|
-
"bright_red": "\
|
|
22
|
-
"bright_green": "\
|
|
23
|
-
"bright_yellow": "\
|
|
24
|
-
"bright_blue": "\
|
|
25
|
-
"bright_magenta": "\
|
|
26
|
-
"bright_cyan": "\
|
|
27
|
-
"bright_white": "\
|
|
11
|
+
ANSI_COLORS: dict[str, str] = {
|
|
12
|
+
"black": "\033[30m",
|
|
13
|
+
"red": "\033[31m",
|
|
14
|
+
"green": "\033[32m",
|
|
15
|
+
"yellow": "\033[33m",
|
|
16
|
+
"blue": "\033[34m",
|
|
17
|
+
"magenta": "\033[35m",
|
|
18
|
+
"cyan": "\033[36m",
|
|
19
|
+
"white": "\033[37m",
|
|
20
|
+
"bright_black": "\033[90m",
|
|
21
|
+
"bright_red": "\033[91m",
|
|
22
|
+
"bright_green": "\033[92m",
|
|
23
|
+
"bright_yellow": "\033[93m",
|
|
24
|
+
"bright_blue": "\033[94m",
|
|
25
|
+
"bright_magenta": "\033[95m",
|
|
26
|
+
"bright_cyan": "\033[96m",
|
|
27
|
+
"bright_white": "\033[97m",
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
ANSI_RESET = "\033[0m"
|
|
30
30
|
|
|
31
31
|
_SI_PREFIXES = ["", "K", "M", "G", "T", "P"]
|
|
32
32
|
_BINARY_PREFIXES = ["", "Ki", "Mi", "Gi", "Ti", "Pi"]
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def _apply_color(text: str, color: str) -> str:
|
|
36
|
-
"""Apply ANSI color to text, returning reset at end."""
|
|
37
|
-
code = _ANSI_COLORS.get(color.lower(), "")
|
|
38
|
-
if not code:
|
|
39
|
-
return text
|
|
40
|
-
return f"{code}{text}{_ANSI_RESET}"
|
|
41
|
-
|
|
42
|
-
|
|
43
35
|
def _format_duration(seconds: float) -> str:
|
|
44
36
|
"""Format seconds as H:MM:SS or M:SS."""
|
|
45
37
|
m, s = divmod(int(seconds), 60)
|
|
@@ -49,6 +41,14 @@ def _format_duration(seconds: float) -> str:
|
|
|
49
41
|
return f"{m}:{s:02d}"
|
|
50
42
|
|
|
51
43
|
|
|
44
|
+
def _apply_color(text: str, color: str) -> str:
|
|
45
|
+
"""Apply ANSI color to text."""
|
|
46
|
+
code = ANSI_COLORS.get(color.lower(), "")
|
|
47
|
+
if not code:
|
|
48
|
+
return text
|
|
49
|
+
return f"{code}{text}{ANSI_RESET}"
|
|
50
|
+
|
|
51
|
+
|
|
52
52
|
def _scale_value(value: float, divisor: int) -> tuple[float, str]:
|
|
53
53
|
"""Return (scaled_value, prefix_string) for display."""
|
|
54
54
|
prefixes = _BINARY_PREFIXES if divisor == 1024 else _SI_PREFIXES
|
|
@@ -61,9 +61,9 @@ def _scale_value(value: float, divisor: int) -> tuple[float, str]:
|
|
|
61
61
|
|
|
62
62
|
class BarRenderer:
|
|
63
63
|
"""
|
|
64
|
-
Renders progress bar
|
|
64
|
+
Renders a single-line progress bar string.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
No I/O — returns strings only. Fully unit-testable.
|
|
67
67
|
"""
|
|
68
68
|
|
|
69
69
|
def _compute_percentage(self, completed: int, total: int) -> float:
|
|
@@ -80,7 +80,7 @@ class BarRenderer:
|
|
|
80
80
|
unit_scale: bool,
|
|
81
81
|
unit_divisor: int,
|
|
82
82
|
) -> str:
|
|
83
|
-
"""Format the count
|
|
83
|
+
"""Format the count display string."""
|
|
84
84
|
if not unit_scale:
|
|
85
85
|
if total is not None:
|
|
86
86
|
return f"[{completed}/{total} {unit}]"
|
|
@@ -98,7 +98,7 @@ class BarRenderer:
|
|
|
98
98
|
unit_scale: bool,
|
|
99
99
|
unit_divisor: int,
|
|
100
100
|
) -> str:
|
|
101
|
-
"""Format the rate
|
|
101
|
+
"""Format the rate display string."""
|
|
102
102
|
if rate is None:
|
|
103
103
|
return ""
|
|
104
104
|
if not unit_scale:
|
|
@@ -128,7 +128,7 @@ class BarRenderer:
|
|
|
128
128
|
unit_divisor: int = 1000,
|
|
129
129
|
) -> str:
|
|
130
130
|
"""
|
|
131
|
-
Render a single-line progress bar string.
|
|
131
|
+
Render a single-line deterministic progress bar string.
|
|
132
132
|
|
|
133
133
|
Returns a string ready for terminal output (no newline).
|
|
134
134
|
"""
|
|
@@ -139,9 +139,9 @@ class BarRenderer:
|
|
|
139
139
|
|
|
140
140
|
if total is not None:
|
|
141
141
|
pct = self._compute_percentage(completed, total)
|
|
142
|
-
filled = int(bar_width * pct / 100)
|
|
143
|
-
|
|
144
|
-
parts.append(f" {
|
|
142
|
+
filled = int(bar_width * pct / 100)
|
|
143
|
+
bar = fill_char * filled + empty_char * (bar_width - filled)
|
|
144
|
+
parts.append(f" {bar} ")
|
|
145
145
|
|
|
146
146
|
if show_percentage:
|
|
147
147
|
parts.append(f"{pct:.0f}%")
|
|
@@ -151,7 +151,6 @@ class BarRenderer:
|
|
|
151
151
|
self._format_count(completed, total, unit, unit_scale, unit_divisor)
|
|
152
152
|
)
|
|
153
153
|
else:
|
|
154
|
-
# Indeterminate — no bar graphic
|
|
155
154
|
if show_count:
|
|
156
155
|
parts.append(
|
|
157
156
|
self._format_count(completed, None, unit, unit_scale, unit_divisor)
|
|
@@ -163,12 +162,11 @@ class BarRenderer:
|
|
|
163
162
|
if show_eta and eta is not None:
|
|
164
163
|
parts.append(f"ETA: {_format_duration(eta)}")
|
|
165
164
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
parts.append(rate_str)
|
|
165
|
+
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
166
|
+
if rate_str:
|
|
167
|
+
parts.append(rate_str)
|
|
170
168
|
|
|
171
|
-
line = " ".join(
|
|
169
|
+
line = " ".join(parts)
|
|
172
170
|
if color:
|
|
173
171
|
line = _apply_color(line, color)
|
|
174
172
|
return line
|
|
@@ -184,19 +182,34 @@ class BarRenderer:
|
|
|
184
182
|
color: str | None,
|
|
185
183
|
show_elapsed: bool = True,
|
|
186
184
|
show_count: bool = True,
|
|
185
|
+
unit: str = "it",
|
|
186
|
+
unit_scale: bool = False,
|
|
187
|
+
unit_divisor: int = 1000,
|
|
187
188
|
) -> str:
|
|
188
|
-
"""
|
|
189
|
+
"""
|
|
190
|
+
Render a single-line spinner for indeterminate progress.
|
|
191
|
+
|
|
192
|
+
Activates automatically when total is None.
|
|
193
|
+
"""
|
|
189
194
|
spinner = SPINNER_FRAMES[frame_index % len(SPINNER_FRAMES)]
|
|
190
195
|
parts: list[str] = []
|
|
191
196
|
if description:
|
|
192
197
|
parts.append(description)
|
|
193
198
|
parts.append(spinner)
|
|
194
199
|
if show_count:
|
|
195
|
-
|
|
200
|
+
if unit == "it":
|
|
201
|
+
parts.append(f"{completed} items")
|
|
202
|
+
else:
|
|
203
|
+
count_str = self._format_count(
|
|
204
|
+
completed, None, unit, unit_scale, unit_divisor
|
|
205
|
+
)
|
|
206
|
+
parts.append(count_str)
|
|
196
207
|
if show_elapsed:
|
|
197
208
|
parts.append(f"⏱ {_format_duration(elapsed)}")
|
|
198
209
|
if rate is not None:
|
|
199
|
-
|
|
210
|
+
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
211
|
+
if rate_str:
|
|
212
|
+
parts.append(rate_str)
|
|
200
213
|
line = " ".join(parts)
|
|
201
214
|
if color:
|
|
202
215
|
line = _apply_color(line, color)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Terminal utilities
|
|
2
|
+
Terminal utilities: TTY detection, ANSI cursor control.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
@@ -15,19 +15,19 @@ def is_tty(file: TextIO = sys.stderr) -> bool:
|
|
|
15
15
|
|
|
16
16
|
def move_cursor_up(n: int = 1) -> str:
|
|
17
17
|
"""Return ANSI escape sequence to move cursor up n lines."""
|
|
18
|
-
return f"\
|
|
18
|
+
return f"\033[{n}A"
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def erase_line() -> str:
|
|
22
22
|
"""Return ANSI escape sequence to erase the current line."""
|
|
23
|
-
return "\
|
|
23
|
+
return "\033[2K\r"
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def hide_cursor() -> str:
|
|
27
27
|
"""Return ANSI escape sequence to hide the cursor."""
|
|
28
|
-
return "\
|
|
28
|
+
return "\033[?25l"
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def show_cursor() -> str:
|
|
32
32
|
"""Return ANSI escape sequence to show the cursor."""
|
|
33
|
-
return "\
|
|
33
|
+
return "\033[?25h"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|