asyncprogress 0.3.4__tar.gz → 0.3.6__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.6}/PKG-INFO +9 -11
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/README.md +4 -8
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/pyproject.toml +9 -9
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/__init__.py +1 -1
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/_bar.py +60 -62
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/_eta.py +23 -27
- asyncprogress-0.3.6/src/asyncprogress/_gather.py +72 -0
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/_iterator.py +20 -20
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/_renderer.py +28 -30
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/_terminal.py +1 -1
- asyncprogress-0.3.4/src/asyncprogress/_gather.py +0 -111
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/LICENSE +0 -0
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/_as_completed.py +0 -0
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/_multi.py +0 -0
- {asyncprogress-0.3.4 → asyncprogress-0.3.6}/src/asyncprogress/py.typed +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
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.6
|
|
4
|
+
Summary: Async-aware progress bars for Python's asyncio ecosystem
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
Keywords: asyncio,progress,progress-bar,
|
|
7
|
+
Keywords: async,asyncio,progress,progress-bar,tqdm
|
|
8
8
|
Author: AgentSoft
|
|
9
9
|
Author-email: agentsoft@example.com
|
|
10
10
|
Requires-Python: >=3.9,<4.0
|
|
@@ -21,7 +21,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Provides-Extra: color
|
|
24
|
-
|
|
24
|
+
Project-URL: Homepage, https://github.com/agentsoft/asyncprogress
|
|
25
|
+
Project-URL: Repository, https://github.com/agentsoft/asyncprogress
|
|
25
26
|
Description-Content-Type: text/markdown
|
|
26
27
|
|
|
27
28
|
# asyncprogress
|
|
@@ -34,19 +35,16 @@ Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory depende
|
|
|
34
35
|
|
|
35
36
|
## Why asyncprogress?
|
|
36
37
|
|
|
37
|
-
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were
|
|
38
|
-
code. `asyncprogress` is
|
|
38
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were designed for
|
|
39
|
+
synchronous code. `asyncprogress` is built from the ground up for `asyncio`:
|
|
39
40
|
|
|
40
|
-
- **Native `async for` support** — wrap any sync or async iterable
|
|
41
|
+
- **Native `async for` support** — wrap any sync or async iterable with one line
|
|
41
42
|
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
42
43
|
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
43
44
|
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
45
|
+
- **Multiple concurrent bars** — `MultiProgressBar` for parallel pipelines
|
|
44
46
|
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
45
47
|
|
|
46
48
|
## Installation
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
pip install asyncprogress
|
|
52
|
-
```
|
|
@@ -8,19 +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
|
|
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
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
pip install asyncprogress
|
|
26
|
-
```
|
|
@@ -1,12 +1,13 @@
|
|
|
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.6"
|
|
4
|
+
description = "Async-aware progress bars for Python's asyncio ecosystem"
|
|
5
5
|
authors = ["AgentSoft <agentsoft@example.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
homepage = "https://github.com/agentsoft/asyncprogress"
|
|
9
|
+
repository = "https://github.com/agentsoft/asyncprogress"
|
|
10
|
+
keywords = ["async", "asyncio", "progress", "progress-bar", "tqdm"]
|
|
10
11
|
classifiers = [
|
|
11
12
|
"Development Status :: 4 - Beta",
|
|
12
13
|
"Intended Audience :: Developers",
|
|
@@ -19,10 +20,10 @@ classifiers = [
|
|
|
19
20
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
21
|
"Framework :: AsyncIO",
|
|
21
22
|
]
|
|
23
|
+
packages = [{include = "asyncprogress", from = "src"}]
|
|
22
24
|
|
|
23
25
|
[tool.poetry.dependencies]
|
|
24
26
|
python = "^3.9"
|
|
25
|
-
colorama = {version = ">=0.4.4", optional = true}
|
|
26
27
|
|
|
27
28
|
[tool.poetry.extras]
|
|
28
29
|
color = ["colorama"]
|
|
@@ -40,19 +41,18 @@ build-backend = "poetry.core.masonry.api"
|
|
|
40
41
|
[tool.pytest.ini_options]
|
|
41
42
|
asyncio_mode = "auto"
|
|
42
43
|
testpaths = ["tests"]
|
|
43
|
-
addopts = "--tb=short"
|
|
44
44
|
|
|
45
45
|
[tool.ruff]
|
|
46
46
|
line-length = 100
|
|
47
47
|
target-version = "py39"
|
|
48
48
|
|
|
49
49
|
[tool.ruff.lint]
|
|
50
|
-
select = ["E", "F", "
|
|
50
|
+
select = ["E", "F", "UP", "I"]
|
|
51
51
|
ignore = ["E501"]
|
|
52
52
|
|
|
53
53
|
[tool.coverage.run]
|
|
54
|
-
source = ["
|
|
55
|
-
|
|
54
|
+
source = ["asyncprogress"]
|
|
55
|
+
branch = true
|
|
56
56
|
|
|
57
57
|
[tool.coverage.report]
|
|
58
58
|
show_missing = true
|
|
@@ -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,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
EWMA-based ETA calculator for
|
|
3
|
-
|
|
4
|
-
Pure math module — no I/O, fully unit-testable.
|
|
2
|
+
EWMA-based ETA calculator for async progress tracking.
|
|
5
3
|
"""
|
|
6
4
|
|
|
7
5
|
from __future__ import annotations
|
|
@@ -11,8 +9,8 @@ class EWMACalculator:
|
|
|
11
9
|
"""
|
|
12
10
|
Exponential Weighted Moving Average calculator for ETA estimation.
|
|
13
11
|
|
|
14
|
-
Uses EWMA over completion timestamps
|
|
15
|
-
which
|
|
12
|
+
Uses EWMA over completion timestamps to estimate items/second rate,
|
|
13
|
+
which handles bursty async I/O patterns better than simple linear regression.
|
|
16
14
|
|
|
17
15
|
Args:
|
|
18
16
|
alpha: Smoothing factor. Higher = more weight on recent samples.
|
|
@@ -20,41 +18,39 @@ class EWMACalculator:
|
|
|
20
18
|
"""
|
|
21
19
|
|
|
22
20
|
def __init__(self, alpha: float = 0.3) -> None:
|
|
23
|
-
if not (0 < alpha <= 1
|
|
21
|
+
if not (0 < alpha <= 1):
|
|
24
22
|
raise ValueError(f"alpha must be in (0, 1], got {alpha}")
|
|
25
23
|
self._alpha = alpha
|
|
26
24
|
self._ewma_rate: float | None = None
|
|
27
25
|
self._last_timestamp: float | None = None
|
|
28
|
-
self._last_completed: int =
|
|
26
|
+
self._last_completed: int | None = None
|
|
29
27
|
|
|
30
28
|
def record(self, timestamp: float, completed: int) -> None:
|
|
31
29
|
"""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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
|
|
30
|
+
if self._last_timestamp is not None and self._last_completed is not None:
|
|
31
|
+
dt = timestamp - self._last_timestamp
|
|
32
|
+
dc = completed - self._last_completed
|
|
33
|
+
if dt > 0 and dc >= 0:
|
|
34
|
+
instant_rate = dc / dt
|
|
35
|
+
if self._ewma_rate is None:
|
|
36
|
+
self._ewma_rate = instant_rate
|
|
37
|
+
else:
|
|
38
|
+
self._ewma_rate = (
|
|
39
|
+
self._alpha * instant_rate
|
|
40
|
+
+ (1 - self._alpha) * self._ewma_rate
|
|
41
|
+
)
|
|
42
|
+
elif dt == 0:
|
|
43
|
+
# Same timestamp — rate stays unchanged, don't update
|
|
44
|
+
pass
|
|
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]
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Terminal rendering for
|
|
3
|
-
|
|
4
|
-
String formatting only — no I/O, fully unit-testable.
|
|
2
|
+
Terminal rendering for progress bars and spinners.
|
|
5
3
|
"""
|
|
6
4
|
|
|
7
5
|
from __future__ import annotations
|
|
@@ -33,7 +31,7 @@ _BINARY_PREFIXES = ["", "Ki", "Mi", "Gi", "Ti", "Pi"]
|
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
def _apply_color(text: str, color: str) -> str:
|
|
36
|
-
"""Apply ANSI color to text
|
|
34
|
+
"""Apply ANSI color to text."""
|
|
37
35
|
code = _ANSI_COLORS.get(color.lower(), "")
|
|
38
36
|
if not code:
|
|
39
37
|
return text
|
|
@@ -41,7 +39,7 @@ def _apply_color(text: str, color: str) -> str:
|
|
|
41
39
|
|
|
42
40
|
|
|
43
41
|
def _format_duration(seconds: float) -> str:
|
|
44
|
-
"""Format
|
|
42
|
+
"""Format seconds as H:MM:SS or M:SS."""
|
|
45
43
|
m, s = divmod(int(seconds), 60)
|
|
46
44
|
h, m = divmod(m, 60)
|
|
47
45
|
if h:
|
|
@@ -61,15 +59,15 @@ def _scale_value(value: float, divisor: int) -> tuple[float, str]:
|
|
|
61
59
|
|
|
62
60
|
class BarRenderer:
|
|
63
61
|
"""
|
|
64
|
-
Renders progress bar
|
|
62
|
+
Renders a single-line progress bar string.
|
|
65
63
|
|
|
66
|
-
|
|
64
|
+
No I/O is performed here — only string formatting.
|
|
67
65
|
"""
|
|
68
66
|
|
|
69
67
|
def _compute_percentage(self, completed: int, total: int) -> float:
|
|
70
|
-
"""Compute percentage, handling
|
|
68
|
+
"""Compute percentage, handling total=0 gracefully."""
|
|
71
69
|
if total == 0:
|
|
72
|
-
return 100.0
|
|
70
|
+
return 100.0
|
|
73
71
|
return min(100.0, (completed / total) * 100.0)
|
|
74
72
|
|
|
75
73
|
def _format_count(
|
|
@@ -127,54 +125,43 @@ class BarRenderer:
|
|
|
127
125
|
unit_scale: bool = False,
|
|
128
126
|
unit_divisor: int = 1000,
|
|
129
127
|
) -> str:
|
|
130
|
-
"""
|
|
131
|
-
Render a single-line progress bar string.
|
|
132
|
-
|
|
133
|
-
Returns a string ready for terminal output (no newline).
|
|
134
|
-
"""
|
|
128
|
+
"""Return a single-line progress bar string ready for terminal output."""
|
|
135
129
|
parts: list[str] = []
|
|
136
130
|
|
|
137
|
-
# Description
|
|
138
131
|
if description:
|
|
139
132
|
parts.append(description)
|
|
140
133
|
|
|
141
|
-
# Bar graphic
|
|
142
134
|
if total is not None:
|
|
143
135
|
pct = self._compute_percentage(completed, total)
|
|
144
|
-
filled = int(bar_width *
|
|
136
|
+
filled = int(bar_width * completed / total) if total > 0 else bar_width
|
|
145
137
|
bar = fill_char * filled + empty_char * (bar_width - filled)
|
|
146
138
|
parts.append(f" {bar} ")
|
|
147
139
|
|
|
148
|
-
# Percentage
|
|
149
140
|
if show_percentage:
|
|
150
141
|
parts.append(f"{pct:.0f}%")
|
|
151
142
|
|
|
152
|
-
# Count
|
|
153
143
|
if show_count:
|
|
154
144
|
parts.append(
|
|
155
145
|
self._format_count(completed, total, unit, unit_scale, unit_divisor)
|
|
156
146
|
)
|
|
157
147
|
else:
|
|
158
|
-
# No total — just show count
|
|
159
148
|
if show_count:
|
|
160
149
|
parts.append(
|
|
161
150
|
self._format_count(completed, None, unit, unit_scale, unit_divisor)
|
|
162
151
|
)
|
|
163
152
|
|
|
164
|
-
# Elapsed
|
|
165
153
|
if show_elapsed:
|
|
166
154
|
parts.append(f"⏱ {_format_duration(elapsed)}")
|
|
167
155
|
|
|
168
|
-
# Rate
|
|
169
|
-
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
170
|
-
if rate_str:
|
|
171
|
-
parts.append(rate_str)
|
|
172
|
-
|
|
173
|
-
# ETA
|
|
174
156
|
if show_eta and eta is not None:
|
|
175
157
|
parts.append(f"ETA: {_format_duration(eta)}")
|
|
176
158
|
|
|
177
|
-
|
|
159
|
+
if rate is not None:
|
|
160
|
+
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
161
|
+
if rate_str:
|
|
162
|
+
parts.append(rate_str)
|
|
163
|
+
|
|
164
|
+
line = " ".join(parts)
|
|
178
165
|
if color:
|
|
179
166
|
line = _apply_color(line, color)
|
|
180
167
|
return line
|
|
@@ -190,6 +177,9 @@ class BarRenderer:
|
|
|
190
177
|
color: str | None,
|
|
191
178
|
show_elapsed: bool = True,
|
|
192
179
|
show_count: bool = True,
|
|
180
|
+
unit: str = "it",
|
|
181
|
+
unit_scale: bool = False,
|
|
182
|
+
unit_divisor: int = 1000,
|
|
193
183
|
) -> str:
|
|
194
184
|
"""Render a single-line spinner for indeterminate progress."""
|
|
195
185
|
spinner = SPINNER_FRAMES[frame_index % len(SPINNER_FRAMES)]
|
|
@@ -198,11 +188,19 @@ class BarRenderer:
|
|
|
198
188
|
parts.append(description)
|
|
199
189
|
parts.append(spinner)
|
|
200
190
|
if show_count:
|
|
201
|
-
|
|
191
|
+
if unit == "it":
|
|
192
|
+
parts.append(f"{completed} items")
|
|
193
|
+
else:
|
|
194
|
+
count_str = self._format_count(
|
|
195
|
+
completed, None, unit, unit_scale, unit_divisor
|
|
196
|
+
)
|
|
197
|
+
parts.append(count_str)
|
|
202
198
|
if show_elapsed:
|
|
203
199
|
parts.append(f"⏱ {_format_duration(elapsed)}")
|
|
204
200
|
if rate is not None:
|
|
205
|
-
|
|
201
|
+
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
202
|
+
if rate_str:
|
|
203
|
+
parts.append(rate_str)
|
|
206
204
|
line = " ".join(parts)
|
|
207
205
|
if color:
|
|
208
206
|
line = _apply_color(line, color)
|
|
@@ -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
|