asyncprogress 0.3.0__tar.gz → 0.3.4__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.0 → asyncprogress-0.3.4}/PKG-INFO +15 -11
- asyncprogress-0.3.4/README.md +26 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/pyproject.toml +7 -4
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/src/asyncprogress/__init__.py +2 -3
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/src/asyncprogress/_bar.py +158 -111
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/src/asyncprogress/_eta.py +24 -19
- asyncprogress-0.3.4/src/asyncprogress/_gather.py +111 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/src/asyncprogress/_iterator.py +21 -19
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/src/asyncprogress/_renderer.py +50 -48
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/src/asyncprogress/_terminal.py +6 -11
- asyncprogress-0.3.0/README.md +0 -22
- asyncprogress-0.3.0/src/asyncprogress/_gather.py +0 -73
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/LICENSE +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/src/asyncprogress/_as_completed.py +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/src/asyncprogress/_multi.py +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.4}/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.4
|
|
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
|
|
@@ -26,18 +26,22 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# asyncprogress
|
|
28
28
|
|
|
29
|
-
Async-aware progress bars for Python's asyncio ecosystem. Zero dependencies
|
|
29
|
+
Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
[](https://pypi.org/project/asyncprogress/)
|
|
32
|
+
[](https://pypi.org/project/asyncprogress/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
## Why asyncprogress?
|
|
36
|
+
|
|
37
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were built for synchronous
|
|
38
|
+
code. `asyncprogress` is designed from the ground up for `asyncio`:
|
|
32
39
|
|
|
33
40
|
- **Native `async for` support** — wrap any sync or async iterable
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- **
|
|
38
|
-
- **EWMA-based ETA** — accurate estimates for bursty async workloads
|
|
39
|
-
- **Multiple concurrent bars** — `MultiProgressBar` for parallel streams
|
|
40
|
-
- **Zero mandatory dependencies** — stdlib only
|
|
41
|
+
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
42
|
+
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
43
|
+
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
44
|
+
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
41
45
|
|
|
42
46
|
## Installation
|
|
43
47
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# asyncprogress
|
|
2
|
+
|
|
3
|
+
Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/asyncprogress/)
|
|
6
|
+
[](https://pypi.org/project/asyncprogress/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Why asyncprogress?
|
|
10
|
+
|
|
11
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were built for synchronous
|
|
12
|
+
code. `asyncprogress` is designed from the ground up for `asyncio`:
|
|
13
|
+
|
|
14
|
+
- **Native `async for` support** — wrap any sync or async iterable
|
|
15
|
+
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
16
|
+
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
17
|
+
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
18
|
+
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install asyncprogress
|
|
26
|
+
```
|
|
@@ -1,7 +1,7 @@
|
|
|
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.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"
|
|
@@ -40,7 +40,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
40
40
|
[tool.pytest.ini_options]
|
|
41
41
|
asyncio_mode = "auto"
|
|
42
42
|
testpaths = ["tests"]
|
|
43
|
-
addopts = "--
|
|
43
|
+
addopts = "--tb=short"
|
|
44
44
|
|
|
45
45
|
[tool.ruff]
|
|
46
46
|
line-length = 100
|
|
@@ -52,4 +52,7 @@ ignore = ["E501"]
|
|
|
52
52
|
|
|
53
53
|
[tool.coverage.run]
|
|
54
54
|
source = ["src/asyncprogress"]
|
|
55
|
-
omit = ["
|
|
55
|
+
omit = ["tests/*"]
|
|
56
|
+
|
|
57
|
+
[tool.coverage.report]
|
|
58
|
+
show_missing = true
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
asyncprogress — Async-aware progress bars for Python's asyncio ecosystem.
|
|
3
3
|
|
|
4
|
-
Zero dependencies
|
|
5
|
-
and concurrent task tracking.
|
|
4
|
+
Zero mandatory dependencies. Pure Python stdlib.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
from __future__ import annotations
|
|
@@ -23,4 +22,4 @@ __all__ = [
|
|
|
23
22
|
"aprogress_as_completed",
|
|
24
23
|
]
|
|
25
24
|
|
|
26
|
-
__version__ = "0.3.
|
|
25
|
+
__version__ = "0.3.4"
|
|
@@ -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
|
|
|
@@ -10,36 +12,39 @@ from typing import Any, TextIO, TypeVar
|
|
|
10
12
|
|
|
11
13
|
from asyncprogress._eta import EWMACalculator
|
|
12
14
|
from asyncprogress._renderer import BarRenderer
|
|
13
|
-
from asyncprogress._terminal import erase_line, is_tty
|
|
15
|
+
from asyncprogress._terminal import erase_line, is_tty, move_cursor_up
|
|
14
16
|
|
|
15
17
|
T = TypeVar("T")
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class ProgressBar:
|
|
19
21
|
"""
|
|
20
|
-
Async context manager
|
|
22
|
+
Async context manager for manual progress tracking.
|
|
21
23
|
|
|
22
24
|
Usage:
|
|
23
25
|
async with ProgressBar(total=100, description="Processing") as bar:
|
|
24
|
-
for item in
|
|
26
|
+
for item in items:
|
|
25
27
|
await process(item)
|
|
26
28
|
await bar.update()
|
|
27
29
|
|
|
28
30
|
Args:
|
|
29
|
-
total: Total number of steps. None
|
|
31
|
+
total: Total number of steps. None activates spinner mode.
|
|
30
32
|
description: Label shown before the bar.
|
|
31
33
|
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:
|
|
36
|
-
show_elapsed:
|
|
37
|
-
show_count:
|
|
38
|
-
show_percentage:
|
|
39
|
-
update_interval:
|
|
34
|
+
fill_char: Character for completed portion.
|
|
35
|
+
empty_char: Character for remaining portion.
|
|
36
|
+
color: ANSI color name (e.g., "green", "cyan", "red").
|
|
37
|
+
show_eta: Show estimated time remaining.
|
|
38
|
+
show_elapsed: Show elapsed time.
|
|
39
|
+
show_count: Show [completed/total] count.
|
|
40
|
+
show_percentage: Show percentage.
|
|
41
|
+
update_interval: Seconds between terminal redraws.
|
|
40
42
|
ewma_alpha: EWMA smoothing factor (0 < alpha <= 1).
|
|
41
|
-
file: Output
|
|
42
|
-
disable:
|
|
43
|
+
file: Output stream (default: sys.stderr).
|
|
44
|
+
disable: Suppress all output if True.
|
|
45
|
+
unit: Unit label (e.g., "it", "B").
|
|
46
|
+
unit_scale: Auto-scale with SI prefix (K/M/G).
|
|
47
|
+
unit_divisor: 1000 for SI, 1024 for binary.
|
|
43
48
|
"""
|
|
44
49
|
|
|
45
50
|
def __init__(
|
|
@@ -58,6 +63,9 @@ class ProgressBar:
|
|
|
58
63
|
ewma_alpha: float = 0.3,
|
|
59
64
|
file: TextIO = sys.stderr,
|
|
60
65
|
disable: bool = False,
|
|
66
|
+
unit: str = "it",
|
|
67
|
+
unit_scale: bool = False,
|
|
68
|
+
unit_divisor: int = 1000,
|
|
61
69
|
) -> None:
|
|
62
70
|
self._total = total
|
|
63
71
|
self._description = description
|
|
@@ -72,101 +80,55 @@ class ProgressBar:
|
|
|
72
80
|
self._update_interval = update_interval
|
|
73
81
|
self._file = file
|
|
74
82
|
self._disable = disable
|
|
83
|
+
self._unit = unit
|
|
84
|
+
self._unit_scale = unit_scale
|
|
85
|
+
self._unit_divisor = unit_divisor
|
|
75
86
|
|
|
76
87
|
self._completed: int = 0
|
|
77
88
|
self._finished: bool = False
|
|
78
89
|
self._start_time: float = 0.0
|
|
79
90
|
self._last_render_time: float = 0.0
|
|
80
91
|
self._spinner_frame: int = 0
|
|
92
|
+
self._render_task: asyncio.Task[None] | None = None
|
|
81
93
|
|
|
82
94
|
self._eta_calc = EWMACalculator(alpha=ewma_alpha)
|
|
83
95
|
self._renderer = BarRenderer()
|
|
84
|
-
|
|
96
|
+
|
|
97
|
+
# Track whether we've written any output (for cursor control)
|
|
98
|
+
self._lines_written: int = 0
|
|
85
99
|
|
|
86
100
|
async def __aenter__(self) -> ProgressBar:
|
|
87
101
|
"""Start the progress bar."""
|
|
88
102
|
self._start_time = time.monotonic()
|
|
89
103
|
self._last_render_time = 0.0
|
|
90
|
-
self._completed = 0
|
|
91
104
|
self._finished = False
|
|
105
|
+
self._completed = 0
|
|
92
106
|
self._spinner_frame = 0
|
|
93
107
|
self._eta_calc.reset()
|
|
94
|
-
|
|
108
|
+
# Start background render loop
|
|
109
|
+
self._render_task = asyncio.create_task(self._render_loop())
|
|
110
|
+
await asyncio.sleep(0) # Yield so render task can initialize
|
|
95
111
|
return self
|
|
96
112
|
|
|
97
113
|
async def __aexit__(self, *args: Any) -> None:
|
|
98
|
-
"""
|
|
114
|
+
"""Stop the progress bar and render final state."""
|
|
99
115
|
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()
|
|
116
|
+
if self._render_task is not None and not self._render_task.done():
|
|
117
|
+
self._render_task.cancel()
|
|
118
|
+
try:
|
|
119
|
+
await self._render_task
|
|
120
|
+
except asyncio.CancelledError:
|
|
121
|
+
pass
|
|
122
|
+
self._render_task = None
|
|
123
|
+
|
|
124
|
+
async def _render_loop(self) -> None:
|
|
125
|
+
"""Background task that periodically redraws the bar."""
|
|
126
|
+
while not self._finished:
|
|
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
|
|
131
|
+
await asyncio.sleep(self._update_interval)
|
|
170
132
|
|
|
171
133
|
def _get_render_string(self) -> str:
|
|
172
134
|
"""Build the current render string (spinner or bar)."""
|
|
@@ -197,37 +159,122 @@ class ProgressBar:
|
|
|
197
159
|
show_elapsed=self._show_elapsed,
|
|
198
160
|
show_count=self._show_count,
|
|
199
161
|
show_percentage=self._show_percentage,
|
|
162
|
+
unit=self._unit,
|
|
163
|
+
unit_scale=self._unit_scale,
|
|
164
|
+
unit_divisor=self._unit_divisor,
|
|
200
165
|
)
|
|
201
166
|
|
|
202
|
-
|
|
203
|
-
"""Write a
|
|
167
|
+
def _write_line(self, line: str) -> None:
|
|
168
|
+
"""Write a progress line to the output file."""
|
|
169
|
+
if self._disable:
|
|
170
|
+
return
|
|
171
|
+
tty = is_tty(self._file)
|
|
172
|
+
if tty and self._lines_written > 0:
|
|
173
|
+
# Erase previous line on TTY
|
|
174
|
+
self._file.write(move_cursor_up(1) + erase_line())
|
|
175
|
+
self._file.write(line + "\n")
|
|
176
|
+
self._file.flush()
|
|
177
|
+
self._lines_written = 1
|
|
178
|
+
|
|
179
|
+
async def _render_final(self) -> None:
|
|
180
|
+
"""Render the final (completed) state of the bar."""
|
|
204
181
|
if self._disable:
|
|
205
182
|
return
|
|
183
|
+
self._write_line(self._get_render_string())
|
|
184
|
+
|
|
185
|
+
async def _schedule_render(self) -> None:
|
|
186
|
+
"""Schedule a render if enough time has passed."""
|
|
206
187
|
now = time.monotonic()
|
|
207
188
|
if now - self._last_render_time >= self._update_interval:
|
|
208
|
-
self._last_render_time = now
|
|
209
189
|
self._write_line(self._get_render_string())
|
|
190
|
+
self._last_render_time = now
|
|
210
191
|
|
|
211
|
-
def
|
|
212
|
-
"""
|
|
213
|
-
|
|
192
|
+
async def update(self, n: int = 1) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Increment progress by n steps.
|
|
195
|
+
|
|
196
|
+
No-op if the bar is already finished.
|
|
197
|
+
Clamps to total if total is set.
|
|
198
|
+
"""
|
|
199
|
+
if self._finished:
|
|
214
200
|
return
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self.
|
|
201
|
+
self._completed += n
|
|
202
|
+
if self._total is not None:
|
|
203
|
+
self._completed = min(self._completed, self._total)
|
|
204
|
+
self._eta_calc.record(time.monotonic(), self._completed)
|
|
205
|
+
await self._schedule_render()
|
|
220
206
|
|
|
221
|
-
async def
|
|
222
|
-
"""
|
|
223
|
-
|
|
207
|
+
async def set(self, value: int) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Set absolute progress to value, clamped to [0, total].
|
|
210
|
+
|
|
211
|
+
No-op if the bar is already finished.
|
|
212
|
+
"""
|
|
213
|
+
if self._finished:
|
|
224
214
|
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
self._file.write(erase_line() + line + "\n")
|
|
215
|
+
if self._total is not None:
|
|
216
|
+
self._completed = max(0, min(value, self._total))
|
|
228
217
|
else:
|
|
229
|
-
self.
|
|
230
|
-
self.
|
|
218
|
+
self._completed = max(0, value)
|
|
219
|
+
self._eta_calc.record(time.monotonic(), self._completed)
|
|
220
|
+
await self._schedule_render()
|
|
221
|
+
|
|
222
|
+
async def set_description(self, description: str) -> None:
|
|
223
|
+
"""Update the description text dynamically."""
|
|
224
|
+
self._description = description
|
|
225
|
+
await self._schedule_render()
|
|
226
|
+
|
|
227
|
+
async def finish(self) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Mark as complete regardless of current count.
|
|
230
|
+
|
|
231
|
+
Idempotent — safe to call multiple times.
|
|
232
|
+
"""
|
|
233
|
+
if self._finished:
|
|
234
|
+
return
|
|
235
|
+
self._finished = True
|
|
236
|
+
if self._total is not None:
|
|
237
|
+
self._completed = self._total
|
|
238
|
+
await self._render_final()
|
|
239
|
+
|
|
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
|
+
@property
|
|
258
|
+
def elapsed(self) -> float:
|
|
259
|
+
"""Seconds since bar was started."""
|
|
260
|
+
if self._start_time == 0.0:
|
|
261
|
+
return 0.0
|
|
262
|
+
return time.monotonic() - self._start_time
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def eta(self) -> float | None:
|
|
266
|
+
"""Estimated seconds remaining, or None if unknown."""
|
|
267
|
+
if self._total is None:
|
|
268
|
+
return None
|
|
269
|
+
remaining = self._total - self._completed
|
|
270
|
+
return self._eta_calc.eta(remaining)
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def rate(self) -> float | None:
|
|
274
|
+
"""Current EWMA-smoothed items/second rate."""
|
|
275
|
+
return self._eta_calc.rate()
|
|
276
|
+
|
|
277
|
+
# ── Convenience classmethod ───────────────────────────────────────────────
|
|
231
278
|
|
|
232
279
|
@classmethod
|
|
233
280
|
def track(
|
|
@@ -241,14 +288,14 @@ class ProgressBar:
|
|
|
241
288
|
"""
|
|
242
289
|
Convenience wrapper: iterate over iterable with a progress bar.
|
|
243
290
|
|
|
244
|
-
Equivalent to aprogress(iterable, total=total, description=description).
|
|
245
|
-
Provided as a classmethod for discoverability.
|
|
291
|
+
Equivalent to aprogress(iterable, total=total, description=description, **kwargs).
|
|
292
|
+
Provided as a classmethod for discoverability when users import only ProgressBar.
|
|
246
293
|
|
|
247
294
|
Args:
|
|
248
295
|
iterable: Any sync or async iterable.
|
|
249
296
|
total: Total item count. Inferred from __len__ if available.
|
|
250
297
|
description: Label shown on the progress bar.
|
|
251
|
-
**kwargs: Forwarded to ProgressBar.
|
|
298
|
+
**kwargs: Forwarded to ProgressBar (color, bar_width, show_eta, etc.)
|
|
252
299
|
|
|
253
300
|
Yields:
|
|
254
301
|
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 module
|
|
4
|
+
Pure math module — no I/O, fully unit-testable.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -12,7 +12,7 @@ class EWMACalculator:
|
|
|
12
12
|
Exponential Weighted Moving Average calculator for ETA estimation.
|
|
13
13
|
|
|
14
14
|
Uses EWMA over completion timestamps rather than simple linear regression,
|
|
15
|
-
which better
|
|
15
|
+
which better handles the bursty completion patterns common in async I/O.
|
|
16
16
|
|
|
17
17
|
Args:
|
|
18
18
|
alpha: Smoothing factor. Higher = more weight on recent samples.
|
|
@@ -25,31 +25,36 @@ class EWMACalculator:
|
|
|
25
25
|
self._alpha = alpha
|
|
26
26
|
self._ewma_rate: float | None = None
|
|
27
27
|
self._last_timestamp: float | None = None
|
|
28
|
-
self._last_completed: int
|
|
28
|
+
self._last_completed: int = 0
|
|
29
29
|
|
|
30
30
|
def record(self, timestamp: float, completed: int) -> None:
|
|
31
31
|
"""Record a completion event at the given timestamp."""
|
|
32
|
-
if self._last_timestamp is
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
if self._last_timestamp is None:
|
|
33
|
+
self._last_timestamp = timestamp
|
|
34
|
+
self._last_completed = completed
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
dt = timestamp - self._last_timestamp
|
|
38
|
+
dc = completed - self._last_completed
|
|
39
|
+
|
|
40
|
+
if dt > 0:
|
|
41
|
+
instant_rate = dc / dt
|
|
42
|
+
if self._ewma_rate is None:
|
|
43
|
+
self._ewma_rate = instant_rate
|
|
44
|
+
else:
|
|
45
|
+
self._ewma_rate = (
|
|
46
|
+
self._alpha * instant_rate + (1 - self._alpha) * self._ewma_rate
|
|
47
|
+
)
|
|
48
|
+
|
|
44
49
|
self._last_timestamp = timestamp
|
|
45
50
|
self._last_completed = completed
|
|
46
51
|
|
|
47
52
|
def rate(self) -> float | None:
|
|
48
|
-
"""
|
|
53
|
+
"""Returns smoothed items/second, or None if insufficient data."""
|
|
49
54
|
return self._ewma_rate
|
|
50
55
|
|
|
51
56
|
def eta(self, remaining: int) -> float | None:
|
|
52
|
-
"""
|
|
57
|
+
"""Returns estimated seconds to completion, or None if unknown."""
|
|
53
58
|
if remaining == 0:
|
|
54
59
|
return 0.0
|
|
55
60
|
r = self.rate()
|
|
@@ -61,4 +66,4 @@ class EWMACalculator:
|
|
|
61
66
|
"""Reset all state."""
|
|
62
67
|
self._ewma_rate = None
|
|
63
68
|
self._last_timestamp = None
|
|
64
|
-
self._last_completed =
|
|
69
|
+
self._last_completed = 0
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
aprogress() —
|
|
2
|
+
aprogress() — async generator wrapper for progress tracking.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
@@ -35,48 +35,48 @@ async def aprogress(
|
|
|
35
35
|
**kwargs: Any,
|
|
36
36
|
) -> AsyncGenerator[T, None]:
|
|
37
37
|
"""
|
|
38
|
-
Async generator wrapper that
|
|
38
|
+
Async generator wrapper that displays a progress bar while iterating.
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
and no `__len__` is present.
|
|
40
|
+
Accepts both sync and async iterables. Automatically infers total from
|
|
41
|
+
__len__ when available. Falls back to spinner mode when total is unknown.
|
|
43
42
|
|
|
44
43
|
Args:
|
|
45
44
|
iterable: Any sync or async iterable.
|
|
46
|
-
total: Total item count. Inferred from
|
|
45
|
+
total: Total item count. Inferred from __len__ if available.
|
|
47
46
|
description: Label shown on the progress bar.
|
|
48
47
|
bar_width: Width of the bar graphic in characters.
|
|
49
48
|
fill_char: Character for completed portion.
|
|
50
|
-
empty_char: Character for
|
|
49
|
+
empty_char: Character for remaining portion.
|
|
51
50
|
color: ANSI color name (e.g., "green", "cyan").
|
|
52
|
-
show_eta:
|
|
53
|
-
show_elapsed:
|
|
54
|
-
show_count:
|
|
55
|
-
show_percentage:
|
|
51
|
+
show_eta: Show estimated time remaining.
|
|
52
|
+
show_elapsed: Show elapsed time.
|
|
53
|
+
show_count: Show count display.
|
|
54
|
+
show_percentage: Show percentage.
|
|
56
55
|
update_interval: Seconds between terminal redraws.
|
|
57
56
|
ewma_alpha: EWMA smoothing factor.
|
|
58
|
-
file: Output
|
|
59
|
-
disable:
|
|
60
|
-
unit: Unit label
|
|
61
|
-
unit_scale: Auto-
|
|
62
|
-
unit_divisor:
|
|
57
|
+
file: Output stream (default: sys.stderr).
|
|
58
|
+
disable: Suppress all output if True.
|
|
59
|
+
unit: Unit label (e.g., "it", "B").
|
|
60
|
+
unit_scale: Auto-scale with SI prefix.
|
|
61
|
+
unit_divisor: 1000 for SI, 1024 for binary.
|
|
63
62
|
|
|
64
63
|
Yields:
|
|
65
64
|
Items from the iterable, in order.
|
|
66
65
|
|
|
67
66
|
Example:
|
|
68
|
-
async for item in aprogress(
|
|
67
|
+
async for item in aprogress(my_list, description="Processing"):
|
|
69
68
|
await process(item)
|
|
70
69
|
"""
|
|
71
70
|
import sys as _sys
|
|
72
71
|
|
|
72
|
+
# Infer total from __len__ if not provided
|
|
73
73
|
if total is None and hasattr(iterable, "__len__"):
|
|
74
74
|
total = len(iterable) # type: ignore[arg-type]
|
|
75
75
|
|
|
76
|
-
effective_total = total if total is not None else
|
|
76
|
+
effective_total = total if total is not None else 0
|
|
77
77
|
|
|
78
78
|
bar_kwargs: dict[str, Any] = {
|
|
79
|
-
"total": effective_total,
|
|
79
|
+
"total": effective_total if total is not None else None,
|
|
80
80
|
"description": description,
|
|
81
81
|
"bar_width": bar_width,
|
|
82
82
|
"fill_char": fill_char,
|
|
@@ -94,6 +94,7 @@ async def aprogress(
|
|
|
94
94
|
"unit_scale": unit_scale,
|
|
95
95
|
"unit_divisor": unit_divisor,
|
|
96
96
|
}
|
|
97
|
+
# Merge any extra kwargs
|
|
97
98
|
bar_kwargs.update(kwargs)
|
|
98
99
|
|
|
99
100
|
async with ProgressBar(**bar_kwargs) as bar:
|
|
@@ -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,48 +1,47 @@
|
|
|
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
|
-
ANSI_COLORS: dict[str, str] = {
|
|
10
|
-
"black": "\033[30m",
|
|
11
|
-
"red": "\033[31m",
|
|
12
|
-
"green": "\033[32m",
|
|
13
|
-
"yellow": "\033[33m",
|
|
14
|
-
"blue": "\033[34m",
|
|
15
|
-
"magenta": "\033[35m",
|
|
16
|
-
"cyan": "\033[36m",
|
|
17
|
-
"white": "\033[37m",
|
|
18
|
-
"bright_black": "\033[90m",
|
|
19
|
-
"bright_red": "\033[91m",
|
|
20
|
-
"bright_green": "\033[92m",
|
|
21
|
-
"bright_yellow": "\033[93m",
|
|
22
|
-
"bright_blue": "\033[94m",
|
|
23
|
-
"bright_magenta": "\033[95m",
|
|
24
|
-
"bright_cyan": "\033[96m",
|
|
25
|
-
"bright_white": "\033[97m",
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
ANSI_RESET = "\033[0m"
|
|
29
|
-
|
|
30
9
|
SPINNER_FRAMES: list[str] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
31
10
|
|
|
11
|
+
_ANSI_COLORS: dict[str, str] = {
|
|
12
|
+
"black": "\x1b[30m",
|
|
13
|
+
"red": "\x1b[31m",
|
|
14
|
+
"green": "\x1b[32m",
|
|
15
|
+
"yellow": "\x1b[33m",
|
|
16
|
+
"blue": "\x1b[34m",
|
|
17
|
+
"magenta": "\x1b[35m",
|
|
18
|
+
"cyan": "\x1b[36m",
|
|
19
|
+
"white": "\x1b[37m",
|
|
20
|
+
"bright_black": "\x1b[90m",
|
|
21
|
+
"bright_red": "\x1b[91m",
|
|
22
|
+
"bright_green": "\x1b[92m",
|
|
23
|
+
"bright_yellow": "\x1b[93m",
|
|
24
|
+
"bright_blue": "\x1b[94m",
|
|
25
|
+
"bright_magenta": "\x1b[95m",
|
|
26
|
+
"bright_cyan": "\x1b[96m",
|
|
27
|
+
"bright_white": "\x1b[97m",
|
|
28
|
+
}
|
|
29
|
+
_ANSI_RESET = "\x1b[0m"
|
|
30
|
+
|
|
32
31
|
_SI_PREFIXES = ["", "K", "M", "G", "T", "P"]
|
|
33
32
|
_BINARY_PREFIXES = ["", "Ki", "Mi", "Gi", "Ti", "Pi"]
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
def _apply_color(text: str, color: str) -> str:
|
|
37
|
-
"""Apply ANSI color to text."""
|
|
38
|
-
code =
|
|
36
|
+
"""Apply ANSI color to text, returning the colored string."""
|
|
37
|
+
code = _ANSI_COLORS.get(color.lower(), "")
|
|
39
38
|
if not code:
|
|
40
39
|
return text
|
|
41
|
-
return f"{code}{text}{
|
|
40
|
+
return f"{code}{text}{_ANSI_RESET}"
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
def _format_duration(seconds: float) -> str:
|
|
45
|
-
"""Format seconds as H:MM:SS or M:SS."""
|
|
44
|
+
"""Format a duration in seconds as H:MM:SS or M:SS."""
|
|
46
45
|
m, s = divmod(int(seconds), 60)
|
|
47
46
|
h, m = divmod(m, 60)
|
|
48
47
|
if h:
|
|
@@ -64,22 +63,22 @@ class BarRenderer:
|
|
|
64
63
|
"""
|
|
65
64
|
Renders progress bar strings for terminal output.
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
String formatting only — no I/O side effects.
|
|
68
67
|
"""
|
|
69
68
|
|
|
70
69
|
def _compute_percentage(self, completed: int, total: int) -> float:
|
|
71
|
-
"""Compute percentage, handling zero total."""
|
|
70
|
+
"""Compute percentage, handling zero total gracefully."""
|
|
72
71
|
if total == 0:
|
|
73
|
-
return 100.0
|
|
72
|
+
return 100.0 # Empty collection is trivially complete
|
|
74
73
|
return min(100.0, (completed / total) * 100.0)
|
|
75
74
|
|
|
76
75
|
def _format_count(
|
|
77
76
|
self,
|
|
78
77
|
completed: int,
|
|
79
78
|
total: int | None,
|
|
80
|
-
unit: str
|
|
81
|
-
unit_scale: bool
|
|
82
|
-
unit_divisor: int
|
|
79
|
+
unit: str,
|
|
80
|
+
unit_scale: bool,
|
|
81
|
+
unit_divisor: int,
|
|
83
82
|
) -> str:
|
|
84
83
|
"""Format the count display string."""
|
|
85
84
|
if not unit_scale:
|
|
@@ -95,17 +94,17 @@ class BarRenderer:
|
|
|
95
94
|
def _format_rate(
|
|
96
95
|
self,
|
|
97
96
|
rate: float | None,
|
|
98
|
-
unit: str
|
|
99
|
-
unit_scale: bool
|
|
100
|
-
unit_divisor: int
|
|
97
|
+
unit: str,
|
|
98
|
+
unit_scale: bool,
|
|
99
|
+
unit_divisor: int,
|
|
101
100
|
) -> str:
|
|
102
101
|
"""Format the rate display string."""
|
|
103
102
|
if rate is None:
|
|
104
103
|
return ""
|
|
105
104
|
if not unit_scale:
|
|
106
105
|
return f"{rate:.1f} {unit}/s"
|
|
107
|
-
|
|
108
|
-
return f"{
|
|
106
|
+
r_val, r_prefix = _scale_value(rate, unit_divisor)
|
|
107
|
+
return f"{r_val:.1f} {r_prefix}{unit}/s"
|
|
109
108
|
|
|
110
109
|
def render(
|
|
111
110
|
self,
|
|
@@ -152,27 +151,30 @@ class BarRenderer:
|
|
|
152
151
|
|
|
153
152
|
# Count
|
|
154
153
|
if show_count:
|
|
155
|
-
parts.append(
|
|
154
|
+
parts.append(
|
|
155
|
+
self._format_count(completed, total, unit, unit_scale, unit_divisor)
|
|
156
|
+
)
|
|
156
157
|
else:
|
|
157
|
-
#
|
|
158
|
+
# No total — just show count
|
|
158
159
|
if show_count:
|
|
159
|
-
parts.append(
|
|
160
|
+
parts.append(
|
|
161
|
+
self._format_count(completed, None, unit, unit_scale, unit_divisor)
|
|
162
|
+
)
|
|
160
163
|
|
|
161
164
|
# Elapsed
|
|
162
165
|
if show_elapsed:
|
|
163
166
|
parts.append(f"⏱ {_format_duration(elapsed)}")
|
|
164
167
|
|
|
168
|
+
# Rate
|
|
169
|
+
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
170
|
+
if rate_str:
|
|
171
|
+
parts.append(rate_str)
|
|
172
|
+
|
|
165
173
|
# ETA
|
|
166
174
|
if show_eta and eta is not None:
|
|
167
175
|
parts.append(f"ETA: {_format_duration(eta)}")
|
|
168
176
|
|
|
169
|
-
|
|
170
|
-
if rate is not None:
|
|
171
|
-
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
172
|
-
if rate_str:
|
|
173
|
-
parts.append(rate_str)
|
|
174
|
-
|
|
175
|
-
line = " ".join(parts)
|
|
177
|
+
line = " ".join(parts)
|
|
176
178
|
if color:
|
|
177
179
|
line = _apply_color(line, color)
|
|
178
180
|
return line
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Terminal utilities: TTY detection, ANSI cursor control
|
|
2
|
+
Terminal utilities: TTY detection, ANSI escape sequences, cursor control.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
@@ -10,29 +10,24 @@ from typing import TextIO
|
|
|
10
10
|
|
|
11
11
|
def is_tty(file: TextIO = sys.stderr) -> bool:
|
|
12
12
|
"""Return True if the given file is a TTY."""
|
|
13
|
-
|
|
14
|
-
return file.isatty()
|
|
15
|
-
except AttributeError:
|
|
16
|
-
return False
|
|
13
|
+
return hasattr(file, "isatty") and file.isatty()
|
|
17
14
|
|
|
18
15
|
|
|
19
16
|
def move_cursor_up(n: int = 1) -> str:
|
|
20
17
|
"""Return ANSI escape sequence to move cursor up n lines."""
|
|
21
|
-
|
|
22
|
-
return ""
|
|
23
|
-
return f"\033[{n}A"
|
|
18
|
+
return f"\x1b[{n}A"
|
|
24
19
|
|
|
25
20
|
|
|
26
21
|
def erase_line() -> str:
|
|
27
22
|
"""Return ANSI escape sequence to erase the current line."""
|
|
28
|
-
return "\
|
|
23
|
+
return "\x1b[2K\r"
|
|
29
24
|
|
|
30
25
|
|
|
31
26
|
def hide_cursor() -> str:
|
|
32
27
|
"""Return ANSI escape sequence to hide the cursor."""
|
|
33
|
-
return "\
|
|
28
|
+
return "\x1b[?25l"
|
|
34
29
|
|
|
35
30
|
|
|
36
31
|
def show_cursor() -> str:
|
|
37
32
|
"""Return ANSI escape sequence to show the cursor."""
|
|
38
|
-
return "\
|
|
33
|
+
return "\x1b[?25h"
|
asyncprogress-0.3.0/README.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# asyncprogress
|
|
2
|
-
|
|
3
|
-
Async-aware progress bars for Python's asyncio ecosystem. Zero dependencies, native `async for` support, accurate ETA with EWMA, and concurrent task tracking.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Native `async for` support** — wrap any sync or async iterable
|
|
8
|
-
- **`async with` context manager** — manual progress tracking
|
|
9
|
-
- **`gather()` replacement** — drop-in for `asyncio.gather()` with progress
|
|
10
|
-
- **`aprogress_as_completed()`** — progress-tracked `as_completed` wrapper
|
|
11
|
-
- **Spinner mode** — automatic for indeterminate progress
|
|
12
|
-
- **EWMA-based ETA** — accurate estimates for bursty async workloads
|
|
13
|
-
- **Multiple concurrent bars** — `MultiProgressBar` for parallel streams
|
|
14
|
-
- **Zero mandatory dependencies** — stdlib only
|
|
15
|
-
|
|
16
|
-
## Installation
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pip install asyncprogress
|
|
22
|
-
```
|
|
@@ -1,73 +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, matching asyncio.gather() semantics.
|
|
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 results
|
|
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 the same order as inputs.
|
|
34
|
-
|
|
35
|
-
Example:
|
|
36
|
-
results = await gather(
|
|
37
|
-
*[fetch(url) for url in urls],
|
|
38
|
-
description="Downloading",
|
|
39
|
-
)
|
|
40
|
-
"""
|
|
41
|
-
if not coros_or_tasks:
|
|
42
|
-
return []
|
|
43
|
-
|
|
44
|
-
total = len(coros_or_tasks)
|
|
45
|
-
|
|
46
|
-
async with ProgressBar(
|
|
47
|
-
total=total, description=description, **progress_kwargs
|
|
48
|
-
) as bar:
|
|
49
|
-
# Wrap each coroutine/task to update the bar on completion
|
|
50
|
-
results: list[Any] = [None] * total
|
|
51
|
-
|
|
52
|
-
async def _run_and_update(idx: int, coro: Any) -> None:
|
|
53
|
-
try:
|
|
54
|
-
results[idx] = await coro
|
|
55
|
-
except Exception as exc:
|
|
56
|
-
if return_exceptions:
|
|
57
|
-
results[idx] = exc
|
|
58
|
-
else:
|
|
59
|
-
raise
|
|
60
|
-
finally:
|
|
61
|
-
await bar.update()
|
|
62
|
-
|
|
63
|
-
wrapped = [
|
|
64
|
-
asyncio.create_task(_run_and_update(i, coro))
|
|
65
|
-
for i, coro in enumerate(coros_or_tasks)
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
if return_exceptions:
|
|
69
|
-
await asyncio.gather(*wrapped, return_exceptions=True)
|
|
70
|
-
else:
|
|
71
|
-
await asyncio.gather(*wrapped)
|
|
72
|
-
|
|
73
|
-
return results
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|