asyncprogress 0.3.4__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.4 → asyncprogress-0.3.5}/PKG-INFO +8 -5
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/README.md +4 -3
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/pyproject.toml +6 -4
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/__init__.py +1 -1
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/_bar.py +60 -62
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/_eta.py +20 -24
- asyncprogress-0.3.5/src/asyncprogress/_gather.py +72 -0
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/_iterator.py +20 -20
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/_renderer.py +53 -46
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/_terminal.py +5 -5
- asyncprogress-0.3.4/src/asyncprogress/_gather.py +0 -111
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/LICENSE +0 -0
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/_as_completed.py +0 -0
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/_multi.py +0 -0
- {asyncprogress-0.3.4 → asyncprogress-0.3.5}/src/asyncprogress/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asyncprogress
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -19,9 +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/agentsoft/asyncprogress
|
|
26
|
+
Project-URL: Repository, https://github.com/agentsoft/asyncprogress
|
|
25
27
|
Description-Content-Type: text/markdown
|
|
26
28
|
|
|
27
29
|
# asyncprogress
|
|
@@ -34,13 +36,14 @@ Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory depende
|
|
|
34
36
|
|
|
35
37
|
## Why asyncprogress?
|
|
36
38
|
|
|
37
|
-
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were
|
|
38
|
-
code. `asyncprogress` is
|
|
39
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were designed for
|
|
40
|
+
synchronous code. `asyncprogress` is built from the ground up for `asyncio`:
|
|
39
41
|
|
|
40
|
-
- **Native `async for` support** — wrap any sync or async iterable
|
|
42
|
+
- **Native `async for` support** — wrap any sync or async iterable with one line
|
|
41
43
|
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
42
44
|
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
43
45
|
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
46
|
+
- **Multiple concurrent bars** — `MultiProgressBar` for parallel pipelines
|
|
44
47
|
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
45
48
|
|
|
46
49
|
## Installation
|
|
@@ -8,13 +8,14 @@ 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
|
|
12
|
-
code. `asyncprogress` is
|
|
11
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were designed for
|
|
12
|
+
synchronous code. `asyncprogress` is built from the ground up for `asyncio`:
|
|
13
13
|
|
|
14
|
-
- **Native `async for` support** — wrap any sync or async iterable
|
|
14
|
+
- **Native `async for` support** — wrap any sync or async iterable with one line
|
|
15
15
|
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
16
16
|
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
17
17
|
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
18
|
+
- **Multiple concurrent bars** — `MultiProgressBar` for parallel pipelines
|
|
18
19
|
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "asyncprogress"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.5"
|
|
4
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
|
-
|
|
8
|
+
homepage = "https://github.com/agentsoft/asyncprogress"
|
|
9
|
+
repository = "https://github.com/agentsoft/asyncprogress"
|
|
9
10
|
keywords = ["asyncio", "progress", "progress-bar", "async", "tqdm"]
|
|
10
11
|
classifiers = [
|
|
11
12
|
"Development Status :: 4 - Beta",
|
|
@@ -16,9 +17,10 @@ classifiers = [
|
|
|
16
17
|
"Programming Language :: Python :: 3.10",
|
|
17
18
|
"Programming Language :: Python :: 3.11",
|
|
18
19
|
"Programming Language :: Python :: 3.12",
|
|
19
|
-
"Topic :: Software Development :: Libraries
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
20
21
|
"Framework :: AsyncIO",
|
|
21
22
|
]
|
|
23
|
+
packages = [{include = "asyncprogress", from = "src"}]
|
|
22
24
|
|
|
23
25
|
[tool.poetry.dependencies]
|
|
24
26
|
python = "^3.9"
|
|
@@ -47,7 +49,7 @@ line-length = 100
|
|
|
47
49
|
target-version = "py39"
|
|
48
50
|
|
|
49
51
|
[tool.ruff.lint]
|
|
50
|
-
select = ["E", "F", "
|
|
52
|
+
select = ["E", "F", "I", "UP"]
|
|
51
53
|
ignore = ["E501"]
|
|
52
54
|
|
|
53
55
|
[tool.coverage.run]
|
|
@@ -12,7 +12,7 @@ from typing import Any, TextIO, TypeVar
|
|
|
12
12
|
|
|
13
13
|
from asyncprogress._eta import EWMACalculator
|
|
14
14
|
from asyncprogress._renderer import BarRenderer
|
|
15
|
-
from asyncprogress._terminal import erase_line, is_tty
|
|
15
|
+
from asyncprogress._terminal import erase_line, is_tty
|
|
16
16
|
|
|
17
17
|
T = TypeVar("T")
|
|
18
18
|
|
|
@@ -21,11 +21,8 @@ class ProgressBar:
|
|
|
21
21
|
"""
|
|
22
22
|
Async context manager for manual progress tracking.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for item in items:
|
|
27
|
-
await process(item)
|
|
28
|
-
await bar.update()
|
|
24
|
+
Provides a progress bar with EWMA-based ETA estimation, configurable
|
|
25
|
+
appearance, and non-blocking terminal output.
|
|
29
26
|
|
|
30
27
|
Args:
|
|
31
28
|
total: Total number of steps. None activates spinner mode.
|
|
@@ -34,17 +31,23 @@ class ProgressBar:
|
|
|
34
31
|
fill_char: Character for completed portion.
|
|
35
32
|
empty_char: Character for remaining portion.
|
|
36
33
|
color: ANSI color name (e.g., "green", "cyan", "red").
|
|
37
|
-
show_eta:
|
|
38
|
-
show_elapsed:
|
|
39
|
-
show_count:
|
|
40
|
-
show_percentage:
|
|
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.
|
|
41
38
|
update_interval: Seconds between terminal redraws.
|
|
42
|
-
ewma_alpha: EWMA smoothing factor (0
|
|
39
|
+
ewma_alpha: EWMA smoothing factor (0.1=smooth, 0.5=reactive).
|
|
43
40
|
file: Output stream (default: sys.stderr).
|
|
44
|
-
disable:
|
|
45
|
-
unit: Unit label (
|
|
46
|
-
unit_scale:
|
|
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).
|
|
47
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()
|
|
48
51
|
"""
|
|
49
52
|
|
|
50
53
|
def __init__(
|
|
@@ -90,22 +93,16 @@ class ProgressBar:
|
|
|
90
93
|
self._last_render_time: float = 0.0
|
|
91
94
|
self._spinner_frame: int = 0
|
|
92
95
|
self._render_task: asyncio.Task[None] | None = None
|
|
96
|
+
self._running: bool = False
|
|
93
97
|
|
|
94
98
|
self._eta_calc = EWMACalculator(alpha=ewma_alpha)
|
|
95
99
|
self._renderer = BarRenderer()
|
|
96
100
|
|
|
97
|
-
# Track whether we've written any output (for cursor control)
|
|
98
|
-
self._lines_written: int = 0
|
|
99
|
-
|
|
100
101
|
async def __aenter__(self) -> ProgressBar:
|
|
101
102
|
"""Start the progress bar."""
|
|
102
103
|
self._start_time = time.monotonic()
|
|
103
|
-
self._last_render_time =
|
|
104
|
-
self.
|
|
105
|
-
self._completed = 0
|
|
106
|
-
self._spinner_frame = 0
|
|
107
|
-
self._eta_calc.reset()
|
|
108
|
-
# Start background render loop
|
|
104
|
+
self._last_render_time = self._start_time
|
|
105
|
+
self._running = True
|
|
109
106
|
self._render_task = asyncio.create_task(self._render_loop())
|
|
110
107
|
await asyncio.sleep(0) # Yield so render task can initialize
|
|
111
108
|
return self
|
|
@@ -113,7 +110,8 @@ class ProgressBar:
|
|
|
113
110
|
async def __aexit__(self, *args: Any) -> None:
|
|
114
111
|
"""Stop the progress bar and render final state."""
|
|
115
112
|
await self.finish()
|
|
116
|
-
|
|
113
|
+
self._running = False
|
|
114
|
+
if self._render_task is not None:
|
|
117
115
|
self._render_task.cancel()
|
|
118
116
|
try:
|
|
119
117
|
await self._render_task
|
|
@@ -122,13 +120,11 @@ class ProgressBar:
|
|
|
122
120
|
self._render_task = None
|
|
123
121
|
|
|
124
122
|
async def _render_loop(self) -> None:
|
|
125
|
-
"""Background task that periodically
|
|
126
|
-
while
|
|
127
|
-
now = time.monotonic()
|
|
128
|
-
if now - self._last_render_time >= self._update_interval:
|
|
129
|
-
self._write_line(self._get_render_string())
|
|
130
|
-
self._last_render_time = now
|
|
123
|
+
"""Background task that periodically re-renders the bar."""
|
|
124
|
+
while self._running:
|
|
131
125
|
await asyncio.sleep(self._update_interval)
|
|
126
|
+
if self._running and not self._finished:
|
|
127
|
+
self._write_line(self._get_render_string())
|
|
132
128
|
|
|
133
129
|
def _get_render_string(self) -> str:
|
|
134
130
|
"""Build the current render string (spinner or bar)."""
|
|
@@ -143,6 +139,9 @@ class ProgressBar:
|
|
|
143
139
|
color=self._color,
|
|
144
140
|
show_elapsed=self._show_elapsed,
|
|
145
141
|
show_count=self._show_count,
|
|
142
|
+
unit=self._unit,
|
|
143
|
+
unit_scale=self._unit_scale,
|
|
144
|
+
unit_divisor=self._unit_divisor,
|
|
146
145
|
)
|
|
147
146
|
return self._renderer.render(
|
|
148
147
|
description=self._description,
|
|
@@ -164,36 +163,39 @@ class ProgressBar:
|
|
|
164
163
|
unit_divisor=self._unit_divisor,
|
|
165
164
|
)
|
|
166
165
|
|
|
167
|
-
def _write_line(self,
|
|
166
|
+
def _write_line(self, text: str) -> None:
|
|
168
167
|
"""Write a progress line to the output file."""
|
|
169
168
|
if self._disable:
|
|
170
169
|
return
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
self._file.write(
|
|
175
|
-
self._file.write(line + "\n")
|
|
170
|
+
if is_tty(self._file):
|
|
171
|
+
self._file.write(erase_line() + text)
|
|
172
|
+
else:
|
|
173
|
+
self._file.write("\r" + text)
|
|
176
174
|
self._file.flush()
|
|
177
|
-
self._lines_written = 1
|
|
178
175
|
|
|
179
176
|
async def _render_final(self) -> None:
|
|
180
|
-
"""Render the final
|
|
177
|
+
"""Render the final completed state."""
|
|
181
178
|
if self._disable:
|
|
182
179
|
return
|
|
183
|
-
self.
|
|
180
|
+
line = self._get_render_string()
|
|
181
|
+
if is_tty(self._file):
|
|
182
|
+
self._file.write(erase_line() + line + "\n")
|
|
183
|
+
else:
|
|
184
|
+
self._file.write("\r" + line + "\n")
|
|
185
|
+
self._file.flush()
|
|
184
186
|
|
|
185
187
|
async def _schedule_render(self) -> None:
|
|
186
|
-
"""
|
|
188
|
+
"""Trigger an immediate render if enough time has passed."""
|
|
187
189
|
now = time.monotonic()
|
|
188
190
|
if now - self._last_render_time >= self._update_interval:
|
|
189
|
-
self._write_line(self._get_render_string())
|
|
190
191
|
self._last_render_time = now
|
|
192
|
+
self._write_line(self._get_render_string())
|
|
191
193
|
|
|
192
194
|
async def update(self, n: int = 1) -> None:
|
|
193
195
|
"""
|
|
194
196
|
Increment progress by n steps.
|
|
195
197
|
|
|
196
|
-
No-op if the bar
|
|
198
|
+
No-op if the bar has already been finished.
|
|
197
199
|
Clamps to total if total is set.
|
|
198
200
|
"""
|
|
199
201
|
if self._finished:
|
|
@@ -208,7 +210,7 @@ class ProgressBar:
|
|
|
208
210
|
"""
|
|
209
211
|
Set absolute progress to value, clamped to [0, total].
|
|
210
212
|
|
|
211
|
-
No-op if the bar
|
|
213
|
+
No-op if the bar has already been finished.
|
|
212
214
|
"""
|
|
213
215
|
if self._finished:
|
|
214
216
|
return
|
|
@@ -237,23 +239,6 @@ class ProgressBar:
|
|
|
237
239
|
self._completed = self._total
|
|
238
240
|
await self._render_final()
|
|
239
241
|
|
|
240
|
-
# ── Read-only properties ──────────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
@property
|
|
243
|
-
def description(self) -> str:
|
|
244
|
-
"""The current description label for this progress bar."""
|
|
245
|
-
return self._description
|
|
246
|
-
|
|
247
|
-
@property
|
|
248
|
-
def completed(self) -> int:
|
|
249
|
-
"""Number of steps completed so far."""
|
|
250
|
-
return self._completed
|
|
251
|
-
|
|
252
|
-
@property
|
|
253
|
-
def total(self) -> int | None:
|
|
254
|
-
"""Total steps, or None if indeterminate."""
|
|
255
|
-
return self._total
|
|
256
|
-
|
|
257
242
|
@property
|
|
258
243
|
def elapsed(self) -> float:
|
|
259
244
|
"""Seconds since bar was started."""
|
|
@@ -274,7 +259,20 @@ class ProgressBar:
|
|
|
274
259
|
"""Current EWMA-smoothed items/second rate."""
|
|
275
260
|
return self._eta_calc.rate()
|
|
276
261
|
|
|
277
|
-
|
|
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
|
|
278
276
|
|
|
279
277
|
@classmethod
|
|
280
278
|
def track(
|
|
@@ -295,7 +293,7 @@ class ProgressBar:
|
|
|
295
293
|
iterable: Any sync or async iterable.
|
|
296
294
|
total: Total item count. Inferred from __len__ if available.
|
|
297
295
|
description: Label shown on the progress bar.
|
|
298
|
-
**kwargs: Forwarded to ProgressBar
|
|
296
|
+
**kwargs: Forwarded to ProgressBar constructor.
|
|
299
297
|
|
|
300
298
|
Yields:
|
|
301
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
|
|
@@ -15,46 +15,42 @@ class EWMACalculator:
|
|
|
15
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
|
|
27
28
|
self._last_timestamp: float | None = None
|
|
28
|
-
self._last_completed: int =
|
|
29
|
+
self._last_completed: int | None = None
|
|
29
30
|
|
|
30
31
|
def record(self, timestamp: float, completed: int) -> None:
|
|
31
32
|
"""Record a completion event at the given timestamp."""
|
|
32
|
-
if self._last_timestamp is None:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else:
|
|
45
|
-
self._ewma_rate = (
|
|
46
|
-
self._alpha * instant_rate + (1 - self._alpha) * self._ewma_rate
|
|
47
|
-
)
|
|
48
|
-
|
|
33
|
+
if self._last_timestamp is not None and self._last_completed is not None:
|
|
34
|
+
dt = timestamp - self._last_timestamp
|
|
35
|
+
dc = completed - self._last_completed
|
|
36
|
+
if dt > 0:
|
|
37
|
+
instant_rate = dc / dt
|
|
38
|
+
if self._ewma_rate is None:
|
|
39
|
+
self._ewma_rate = instant_rate
|
|
40
|
+
else:
|
|
41
|
+
self._ewma_rate = (
|
|
42
|
+
self._alpha * instant_rate + (1 - self._alpha) * self._ewma_rate
|
|
43
|
+
)
|
|
44
|
+
# If dt == 0, we skip updating the rate (same timestamp)
|
|
49
45
|
self._last_timestamp = timestamp
|
|
50
46
|
self._last_completed = completed
|
|
51
47
|
|
|
52
48
|
def rate(self) -> float | None:
|
|
53
|
-
"""
|
|
49
|
+
"""Return smoothed items/second, or None if insufficient data."""
|
|
54
50
|
return self._ewma_rate
|
|
55
51
|
|
|
56
52
|
def eta(self, remaining: int) -> float | None:
|
|
57
|
-
"""
|
|
53
|
+
"""Return estimated seconds to completion, or None if unknown."""
|
|
58
54
|
if remaining == 0:
|
|
59
55
|
return 0.0
|
|
60
56
|
r = self.rate()
|
|
@@ -66,4 +62,4 @@ class EWMACalculator:
|
|
|
66
62
|
"""Reset all state."""
|
|
67
63
|
self._ewma_rate = None
|
|
68
64
|
self._last_timestamp = None
|
|
69
|
-
self._last_completed =
|
|
65
|
+
self._last_completed = None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""gather() — drop-in replacement for asyncio.gather() with progress tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Coroutine
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from asyncprogress._bar import ProgressBar
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def gather(
|
|
13
|
+
*coros_or_tasks: Coroutine[Any, Any, Any] | asyncio.Task[Any],
|
|
14
|
+
description: str = "",
|
|
15
|
+
return_exceptions: bool = False,
|
|
16
|
+
**progress_kwargs: Any,
|
|
17
|
+
) -> list[Any]:
|
|
18
|
+
"""
|
|
19
|
+
Drop-in replacement for asyncio.gather() with progress tracking.
|
|
20
|
+
|
|
21
|
+
Returns results in the same order as inputs.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
*coros_or_tasks: Coroutines or Tasks to run concurrently.
|
|
25
|
+
description: Label shown on the progress bar.
|
|
26
|
+
return_exceptions: If True, exceptions are returned as values.
|
|
27
|
+
**progress_kwargs: Forwarded to ProgressBar (color, bar_width, etc.)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of results in submission order.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
results = await gather(
|
|
34
|
+
*[fetch(url) for url in urls],
|
|
35
|
+
description="Fetching",
|
|
36
|
+
color="cyan",
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
if not coros_or_tasks:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
total = len(coros_or_tasks)
|
|
43
|
+
|
|
44
|
+
async with ProgressBar(
|
|
45
|
+
total=total, description=description, **progress_kwargs
|
|
46
|
+
) as bar:
|
|
47
|
+
# Wrap each coroutine/task to update the bar on completion
|
|
48
|
+
results: list[Any] = [None] * total
|
|
49
|
+
exceptions: list[BaseException | None] = [None] * total
|
|
50
|
+
|
|
51
|
+
async def _run_one(idx: int, coro: Any) -> None:
|
|
52
|
+
try:
|
|
53
|
+
results[idx] = await coro
|
|
54
|
+
except Exception as exc: # noqa: BLE001
|
|
55
|
+
exceptions[idx] = exc
|
|
56
|
+
finally:
|
|
57
|
+
await bar.update()
|
|
58
|
+
|
|
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)
|
|
61
|
+
|
|
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
|
+
else:
|
|
71
|
+
final.append(results[i])
|
|
72
|
+
return final
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
aprogress() — async generator wrapper for
|
|
2
|
+
aprogress() — async generator wrapper for iterables.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
@@ -32,13 +32,12 @@ 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
37
|
Async generator wrapper that displays a progress bar while iterating.
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
Automatically infers total from __len__ if available. Falls back to
|
|
40
|
+
spinner mode when total is unknown.
|
|
42
41
|
|
|
43
42
|
Args:
|
|
44
43
|
iterable: Any sync or async iterable.
|
|
@@ -48,35 +47,35 @@ async def aprogress(
|
|
|
48
47
|
fill_char: Character for completed portion.
|
|
49
48
|
empty_char: Character for remaining portion.
|
|
50
49
|
color: ANSI color name (e.g., "green", "cyan").
|
|
51
|
-
show_eta:
|
|
52
|
-
show_elapsed:
|
|
53
|
-
show_count:
|
|
54
|
-
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.
|
|
55
54
|
update_interval: Seconds between terminal redraws.
|
|
56
55
|
ewma_alpha: EWMA smoothing factor.
|
|
57
56
|
file: Output stream (default: sys.stderr).
|
|
58
|
-
disable:
|
|
59
|
-
unit: Unit label (
|
|
60
|
-
unit_scale:
|
|
57
|
+
disable: If True, suppress all output.
|
|
58
|
+
unit: Unit label ("it" for items, "B" for bytes, etc.).
|
|
59
|
+
unit_scale: If True, auto-apply SI prefix (K/M/G).
|
|
61
60
|
unit_divisor: 1000 for SI, 1024 for binary.
|
|
62
61
|
|
|
63
62
|
Yields:
|
|
64
63
|
Items from the iterable, in order.
|
|
65
64
|
|
|
66
65
|
Example:
|
|
67
|
-
async for item in aprogress(
|
|
66
|
+
async for item in aprogress(range(100), description="Processing"):
|
|
68
67
|
await process(item)
|
|
69
68
|
"""
|
|
70
69
|
import sys as _sys
|
|
71
70
|
|
|
72
|
-
# Infer total from __len__ if
|
|
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
|
-
effective_total = total if total is not None else
|
|
75
|
+
effective_total = total if total is not None else None
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
"total": effective_total
|
|
77
|
+
kwargs: dict[str, Any] = {
|
|
78
|
+
"total": effective_total,
|
|
80
79
|
"description": description,
|
|
81
80
|
"bar_width": bar_width,
|
|
82
81
|
"fill_char": fill_char,
|
|
@@ -88,16 +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
|
-
|
|
98
|
-
|
|
95
|
+
if file is not None:
|
|
96
|
+
kwargs["file"] = file
|
|
97
|
+
else:
|
|
98
|
+
kwargs["file"] = _sys.stderr
|
|
99
99
|
|
|
100
|
-
async with ProgressBar(**
|
|
100
|
+
async with ProgressBar(**kwargs) as bar:
|
|
101
101
|
count = 0
|
|
102
102
|
if hasattr(iterable, "__aiter__"):
|
|
103
103
|
async for item in iterable: # type: ignore[union-attr]
|
|
@@ -8,40 +8,32 @@ 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 the colored string."""
|
|
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
|
-
"""Format
|
|
36
|
+
"""Format seconds as H:MM:SS or M:SS."""
|
|
45
37
|
m, s = divmod(int(seconds), 60)
|
|
46
38
|
h, m = divmod(m, 60)
|
|
47
39
|
if h:
|
|
@@ -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,13 +61,13 @@ 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:
|
|
70
|
-
"""Compute percentage, handling
|
|
70
|
+
"""Compute percentage, handling total=0 gracefully."""
|
|
71
71
|
if total == 0:
|
|
72
72
|
return 100.0 # Empty collection is trivially complete
|
|
73
73
|
return min(100.0, (completed / total) * 100.0)
|
|
@@ -128,53 +128,45 @@ 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
|
"""
|
|
135
135
|
parts: list[str] = []
|
|
136
136
|
|
|
137
|
-
# Description
|
|
138
137
|
if description:
|
|
139
138
|
parts.append(description)
|
|
140
139
|
|
|
141
|
-
# Bar graphic
|
|
142
140
|
if total is not None:
|
|
143
141
|
pct = self._compute_percentage(completed, total)
|
|
144
142
|
filled = int(bar_width * pct / 100)
|
|
145
143
|
bar = fill_char * filled + empty_char * (bar_width - filled)
|
|
146
144
|
parts.append(f" {bar} ")
|
|
147
145
|
|
|
148
|
-
# Percentage
|
|
149
146
|
if show_percentage:
|
|
150
147
|
parts.append(f"{pct:.0f}%")
|
|
151
148
|
|
|
152
|
-
# Count
|
|
153
149
|
if show_count:
|
|
154
150
|
parts.append(
|
|
155
151
|
self._format_count(completed, total, unit, unit_scale, unit_divisor)
|
|
156
152
|
)
|
|
157
153
|
else:
|
|
158
|
-
# No total — just show count
|
|
159
154
|
if show_count:
|
|
160
155
|
parts.append(
|
|
161
156
|
self._format_count(completed, None, unit, unit_scale, unit_divisor)
|
|
162
157
|
)
|
|
163
158
|
|
|
164
|
-
# Elapsed
|
|
165
159
|
if show_elapsed:
|
|
166
160
|
parts.append(f"⏱ {_format_duration(elapsed)}")
|
|
167
161
|
|
|
168
|
-
|
|
162
|
+
if show_eta and eta is not None:
|
|
163
|
+
parts.append(f"ETA: {_format_duration(eta)}")
|
|
164
|
+
|
|
169
165
|
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
170
166
|
if rate_str:
|
|
171
167
|
parts.append(rate_str)
|
|
172
168
|
|
|
173
|
-
|
|
174
|
-
if show_eta and eta is not None:
|
|
175
|
-
parts.append(f"ETA: {_format_duration(eta)}")
|
|
176
|
-
|
|
177
|
-
line = " ".join(parts)
|
|
169
|
+
line = " ".join(parts)
|
|
178
170
|
if color:
|
|
179
171
|
line = _apply_color(line, color)
|
|
180
172
|
return line
|
|
@@ -190,19 +182,34 @@ class BarRenderer:
|
|
|
190
182
|
color: str | None,
|
|
191
183
|
show_elapsed: bool = True,
|
|
192
184
|
show_count: bool = True,
|
|
185
|
+
unit: str = "it",
|
|
186
|
+
unit_scale: bool = False,
|
|
187
|
+
unit_divisor: int = 1000,
|
|
193
188
|
) -> str:
|
|
194
|
-
"""
|
|
189
|
+
"""
|
|
190
|
+
Render a single-line spinner for indeterminate progress.
|
|
191
|
+
|
|
192
|
+
Activates automatically when total is None.
|
|
193
|
+
"""
|
|
195
194
|
spinner = SPINNER_FRAMES[frame_index % len(SPINNER_FRAMES)]
|
|
196
195
|
parts: list[str] = []
|
|
197
196
|
if description:
|
|
198
197
|
parts.append(description)
|
|
199
198
|
parts.append(spinner)
|
|
200
199
|
if show_count:
|
|
201
|
-
|
|
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)
|
|
202
207
|
if show_elapsed:
|
|
203
208
|
parts.append(f"⏱ {_format_duration(elapsed)}")
|
|
204
209
|
if rate is not None:
|
|
205
|
-
|
|
210
|
+
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
211
|
+
if rate_str:
|
|
212
|
+
parts.append(rate_str)
|
|
206
213
|
line = " ".join(parts)
|
|
207
214
|
if color:
|
|
208
215
|
line = _apply_color(line, color)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Terminal utilities: TTY detection, ANSI
|
|
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"
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
gather() — Drop-in replacement for asyncio.gather() with progress tracking.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
from collections.abc import Coroutine
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
from asyncprogress._bar import ProgressBar
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
async def gather(
|
|
15
|
-
*coros_or_tasks: Coroutine[Any, Any, Any] | asyncio.Task[Any],
|
|
16
|
-
description: str = "",
|
|
17
|
-
return_exceptions: bool = False,
|
|
18
|
-
**progress_kwargs: Any,
|
|
19
|
-
) -> list[Any]:
|
|
20
|
-
"""
|
|
21
|
-
Drop-in replacement for asyncio.gather() with progress tracking.
|
|
22
|
-
|
|
23
|
-
Returns results in the same order as inputs.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
*coros_or_tasks: Coroutines or tasks to run concurrently.
|
|
27
|
-
description: Label shown on the progress bar.
|
|
28
|
-
return_exceptions: If True, exceptions are returned as values
|
|
29
|
-
rather than raised (mirrors asyncio.gather behavior).
|
|
30
|
-
**progress_kwargs: Forwarded to ProgressBar (color, bar_width, etc.)
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
List of results in submission order.
|
|
34
|
-
|
|
35
|
-
Example:
|
|
36
|
-
results = await gather(
|
|
37
|
-
*[fetch(url) for url in urls],
|
|
38
|
-
description="Fetching",
|
|
39
|
-
color="cyan",
|
|
40
|
-
)
|
|
41
|
-
"""
|
|
42
|
-
if not coros_or_tasks:
|
|
43
|
-
return []
|
|
44
|
-
|
|
45
|
-
total = len(coros_or_tasks)
|
|
46
|
-
|
|
47
|
-
async with ProgressBar(
|
|
48
|
-
total=total, description=description, **progress_kwargs
|
|
49
|
-
) as bar:
|
|
50
|
-
# Convert all coroutines to tasks
|
|
51
|
-
tasks: list[asyncio.Task[Any]] = []
|
|
52
|
-
for item in coros_or_tasks:
|
|
53
|
-
if isinstance(item, asyncio.Task):
|
|
54
|
-
tasks.append(item)
|
|
55
|
-
else:
|
|
56
|
-
tasks.append(asyncio.create_task(item))
|
|
57
|
-
|
|
58
|
-
# Track results in order
|
|
59
|
-
results: list[Any] = [None] * total
|
|
60
|
-
pending = set(tasks)
|
|
61
|
-
task_index = {task: i for i, task in enumerate(tasks)}
|
|
62
|
-
|
|
63
|
-
# Add completion callbacks
|
|
64
|
-
completed_count = 0
|
|
65
|
-
|
|
66
|
-
async def wait_for_all() -> None:
|
|
67
|
-
nonlocal completed_count
|
|
68
|
-
for future in asyncio.as_completed(list(pending)):
|
|
69
|
-
try:
|
|
70
|
-
result = await future
|
|
71
|
-
# Find original index — we need to track by task identity
|
|
72
|
-
completed_count += 1
|
|
73
|
-
await bar.update()
|
|
74
|
-
_ = result # will be collected below
|
|
75
|
-
except Exception:
|
|
76
|
-
completed_count += 1
|
|
77
|
-
await bar.update()
|
|
78
|
-
if not return_exceptions:
|
|
79
|
-
raise
|
|
80
|
-
|
|
81
|
-
# Actually gather with proper ordering
|
|
82
|
-
# Use asyncio.gather for ordering, but track progress via callbacks
|
|
83
|
-
# Reset bar since we already incremented via wait_for_all approach
|
|
84
|
-
# Better approach: use done callbacks
|
|
85
|
-
|
|
86
|
-
# Re-implement with proper ordering using gather + callbacks
|
|
87
|
-
async with ProgressBar(
|
|
88
|
-
total=total, description=description, **progress_kwargs
|
|
89
|
-
) as bar:
|
|
90
|
-
tasks2: list[asyncio.Task[Any]] = []
|
|
91
|
-
for item in coros_or_tasks:
|
|
92
|
-
if isinstance(item, asyncio.Task):
|
|
93
|
-
tasks2.append(item)
|
|
94
|
-
else:
|
|
95
|
-
tasks2.append(asyncio.create_task(item))
|
|
96
|
-
|
|
97
|
-
# Wrap each task to update bar on completion
|
|
98
|
-
async def tracked(task: asyncio.Task[Any]) -> Any:
|
|
99
|
-
try:
|
|
100
|
-
result = await task
|
|
101
|
-
await bar.update()
|
|
102
|
-
return result
|
|
103
|
-
except Exception as exc:
|
|
104
|
-
await bar.update()
|
|
105
|
-
raise exc
|
|
106
|
-
|
|
107
|
-
tracked_tasks = [tracked(t) for t in tasks2]
|
|
108
|
-
raw_results = await asyncio.gather(*tracked_tasks, return_exceptions=return_exceptions)
|
|
109
|
-
results = list(raw_results)
|
|
110
|
-
|
|
111
|
-
return results
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|