python-async-aware-progress-bar 0.1.0__tar.gz → 0.2.0__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.
Files changed (23) hide show
  1. python_async_aware_progress_bar-0.2.0/PKG-INFO +51 -0
  2. python_async_aware_progress_bar-0.2.0/README.md +24 -0
  3. {python_async_aware_progress_bar-0.1.0 → python_async_aware_progress_bar-0.2.0}/pyproject.toml +14 -14
  4. python_async_aware_progress_bar-0.2.0/src/asyncprogress/__init__.py +30 -0
  5. python_async_aware_progress_bar-0.2.0/src/asyncprogress/_as_completed.py +93 -0
  6. python_async_aware_progress_bar-0.2.0/src/asyncprogress/_bar.py +235 -0
  7. python_async_aware_progress_bar-0.2.0/src/asyncprogress/_eta.py +84 -0
  8. python_async_aware_progress_bar-0.2.0/src/asyncprogress/_gather.py +112 -0
  9. {python_async_aware_progress_bar-0.1.0 → python_async_aware_progress_bar-0.2.0}/src/asyncprogress/_iterator.py +30 -22
  10. python_async_aware_progress_bar-0.2.0/src/asyncprogress/_multi.py +150 -0
  11. python_async_aware_progress_bar-0.2.0/src/asyncprogress/_renderer.py +190 -0
  12. python_async_aware_progress_bar-0.2.0/src/asyncprogress/_terminal.py +50 -0
  13. python_async_aware_progress_bar-0.1.0/PKG-INFO +0 -50
  14. python_async_aware_progress_bar-0.1.0/README.md +0 -22
  15. python_async_aware_progress_bar-0.1.0/src/asyncprogress/__init__.py +0 -25
  16. python_async_aware_progress_bar-0.1.0/src/asyncprogress/_bar.py +0 -212
  17. python_async_aware_progress_bar-0.1.0/src/asyncprogress/_eta.py +0 -77
  18. python_async_aware_progress_bar-0.1.0/src/asyncprogress/_gather.py +0 -69
  19. python_async_aware_progress_bar-0.1.0/src/asyncprogress/_multi.py +0 -144
  20. python_async_aware_progress_bar-0.1.0/src/asyncprogress/_renderer.py +0 -102
  21. python_async_aware_progress_bar-0.1.0/src/asyncprogress/_terminal.py +0 -71
  22. {python_async_aware_progress_bar-0.1.0 → python_async_aware_progress_bar-0.2.0}/LICENSE +0 -0
  23. {python_async_aware_progress_bar-0.1.0 → python_async_aware_progress_bar-0.2.0}/src/asyncprogress/py.typed +0 -0
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-async-aware-progress-bar
3
+ Version: 0.2.0
4
+ Summary: Async-aware progress bar for Python asyncio applications
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: async,asyncio,progress,progress-bar,cli
8
+ Author: AgentSoft
9
+ Author-email: agentsoft@example.com
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Terminals
24
+ Provides-Extra: color
25
+ Requires-Dist: colorama (>=0.4.4) ; extra == "color"
26
+ Description-Content-Type: text/markdown
27
+
28
+ # asyncprogress
29
+
30
+ An async-aware progress bar library for Python's asyncio ecosystem.
31
+
32
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+
35
+ ## Features
36
+
37
+ - **`async for` support**: Wrap async generators and sync iterables naturally
38
+ - **Task pool tracking**: Drop-in replacement for `asyncio.gather()` with progress
39
+ - **Manual progress bars**: Async context manager for complex workflows
40
+ - **Multiple concurrent bars**: Track several operations simultaneously
41
+ - **Accurate ETA**: Exponential Weighted Moving Average (EWMA) for burst-pattern async I/O
42
+ - **Spinner mode**: Animated spinner for indeterminate progress
43
+ - **Zero dependencies**: Pure Python stdlib only (optional `colorama` for Windows colors)
44
+
45
+ ## Installation
46
+
47
+
48
+
49
+ ```bash
50
+ pip install python-async-aware-progress-bar
51
+ ```
@@ -0,0 +1,24 @@
1
+ # asyncprogress
2
+
3
+ An async-aware progress bar library for Python's asyncio ecosystem.
4
+
5
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - **`async for` support**: Wrap async generators and sync iterables naturally
11
+ - **Task pool tracking**: Drop-in replacement for `asyncio.gather()` with progress
12
+ - **Manual progress bars**: Async context manager for complex workflows
13
+ - **Multiple concurrent bars**: Track several operations simultaneously
14
+ - **Accurate ETA**: Exponential Weighted Moving Average (EWMA) for burst-pattern async I/O
15
+ - **Spinner mode**: Animated spinner for indeterminate progress
16
+ - **Zero dependencies**: Pure Python stdlib only (optional `colorama` for Windows colors)
17
+
18
+ ## Installation
19
+
20
+
21
+
22
+ ```bash
23
+ pip install python-async-aware-progress-bar
24
+ ```
@@ -1,15 +1,14 @@
1
1
  [tool.poetry]
2
2
  name = "python-async-aware-progress-bar"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Async-aware progress bar for Python asyncio applications"
5
5
  authors = ["AgentSoft <agentsoft@example.com>"]
6
6
  readme = "README.md"
7
7
  license = "MIT"
8
8
  packages = [{include = "asyncprogress", from = "src"}]
9
- keywords = ["asyncio", "progress", "progress-bar", "async", "tqdm"]
9
+ keywords = ["async", "asyncio", "progress", "progress-bar", "cli"]
10
10
  classifiers = [
11
- "Development Status :: 3 - Alpha",
12
- "Framework :: AsyncIO",
11
+ "Development Status :: 4 - Beta",
13
12
  "Intended Audience :: Developers",
14
13
  "License :: OSI Approved :: MIT License",
15
14
  "Programming Language :: Python :: 3",
@@ -17,8 +16,9 @@ classifiers = [
17
16
  "Programming Language :: Python :: 3.10",
18
17
  "Programming Language :: Python :: 3.11",
19
18
  "Programming Language :: Python :: 3.12",
20
- "Topic :: Software Development :: Libraries :: Python Modules",
21
- "Topic :: Utilities",
19
+ "Topic :: Software Development :: Libraries",
20
+ "Topic :: Terminals",
21
+ "Framework :: AsyncIO",
22
22
  ]
23
23
 
24
24
  [tool.poetry.dependencies]
@@ -41,21 +41,21 @@ build-backend = "poetry.core.masonry.api"
41
41
  [tool.pytest.ini_options]
42
42
  asyncio_mode = "auto"
43
43
  testpaths = ["tests"]
44
- addopts = "--cov=asyncprogress --cov-report=term-missing --cov-report=xml"
44
+ addopts = "--cov=asyncprogress --cov-report=term-missing --cov-fail-under=80"
45
45
 
46
46
  [tool.ruff]
47
47
  target-version = "py39"
48
- line-length = 88
49
- src = ["src"]
48
+ line-length = 100
50
49
 
51
50
  [tool.ruff.lint]
52
- select = ["E", "F", "UP", "I", "W"]
53
- ignore = []
51
+ select = ["E", "F", "UP", "I"]
52
+ ignore = ["E501"]
54
53
 
55
54
  [tool.coverage.run]
56
55
  source = ["src/asyncprogress"]
57
- branch = true
58
56
 
59
57
  [tool.coverage.report]
60
- show_missing = true
61
- skip_covered = false
58
+ exclude_lines = [
59
+ "pragma: no cover",
60
+ "if TYPE_CHECKING:",
61
+ ]
@@ -0,0 +1,30 @@
1
+ """
2
+ asyncprogress — Async-aware progress bar for Python asyncio applications.
3
+
4
+ Public API:
5
+ aprogress: Async generator wrapper for iterables
6
+ ProgressBar: Manual async context manager progress bar
7
+ MultiProgressBar: Concurrent progress bars manager
8
+ gather: Drop-in replacement for asyncio.gather() with progress
9
+ EWMACalculator: Exponential weighted moving average ETA calculator
10
+ aprogress_as_completed: Progress-tracked as_completed wrapper
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from asyncprogress._as_completed import aprogress_as_completed
16
+ from asyncprogress._bar import ProgressBar
17
+ from asyncprogress._eta import EWMACalculator
18
+ from asyncprogress._gather import gather
19
+ from asyncprogress._iterator import aprogress
20
+ from asyncprogress._multi import MultiProgressBar
21
+
22
+ __version__ = "0.2.0"
23
+ __all__ = [
24
+ "aprogress",
25
+ "ProgressBar",
26
+ "MultiProgressBar",
27
+ "gather",
28
+ "EWMACalculator",
29
+ "aprogress_as_completed",
30
+ ]
@@ -0,0 +1,93 @@
1
+ """aprogress_as_completed() — progress-tracked asyncio.as_completed wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ from collections.abc import AsyncIterator, Coroutine
8
+ from typing import Any, TextIO, TypeVar
9
+
10
+ from asyncprogress._bar import ProgressBar
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ async def aprogress_as_completed(
16
+ coros: list[Coroutine[Any, Any, T]],
17
+ *,
18
+ description: str = "",
19
+ return_exceptions: bool = False,
20
+ bar_width: int = 40,
21
+ fill_char: str = "█",
22
+ empty_char: str = "░",
23
+ color: str | None = None,
24
+ show_eta: bool = True,
25
+ show_elapsed: bool = True,
26
+ show_count: bool = True,
27
+ show_percentage: bool = True,
28
+ update_interval: float = 0.1,
29
+ ewma_alpha: float = 0.3,
30
+ file: TextIO = sys.stderr,
31
+ disable: bool = False,
32
+ ) -> AsyncIterator[T]:
33
+ """
34
+ Async generator that yields results as coroutines complete, with a progress bar.
35
+
36
+ Drop-in replacement for ``asyncio.as_completed()`` iteration patterns.
37
+
38
+ Args:
39
+ coros: List of coroutines to run concurrently.
40
+ description: Label shown on the progress bar.
41
+ return_exceptions: If True, exceptions are yielded as values
42
+ rather than raised.
43
+ bar_width: Width of the bar graphic.
44
+ fill_char: Character for completed portion.
45
+ empty_char: Character for remaining portion.
46
+ color: ANSI color name, or None.
47
+ show_eta: Whether to show ETA.
48
+ show_elapsed: Whether to show elapsed time.
49
+ show_count: Whether to show item count.
50
+ show_percentage: Whether to show percentage.
51
+ update_interval: Seconds between terminal redraws.
52
+ ewma_alpha: EWMA smoothing factor.
53
+ file: Output file. Defaults to sys.stderr.
54
+ disable: If True, suppress all output.
55
+
56
+ Yields:
57
+ Results in completion order (not submission order).
58
+
59
+ Example:
60
+ async for result in aprogress_as_completed(coros, description="Fetching"):
61
+ process(result)
62
+ """
63
+ if not coros:
64
+ return
65
+
66
+ total = len(coros)
67
+ async with ProgressBar(
68
+ total=total,
69
+ description=description,
70
+ bar_width=bar_width,
71
+ fill_char=fill_char,
72
+ empty_char=empty_char,
73
+ color=color,
74
+ show_eta=show_eta,
75
+ show_elapsed=show_elapsed,
76
+ show_count=show_count,
77
+ show_percentage=show_percentage,
78
+ update_interval=update_interval,
79
+ ewma_alpha=ewma_alpha,
80
+ file=file,
81
+ disable=disable,
82
+ ) as bar:
83
+ for future in asyncio.as_completed(coros):
84
+ try:
85
+ result = await future
86
+ await bar.update()
87
+ yield result
88
+ except Exception as exc:
89
+ await bar.update()
90
+ if return_exceptions:
91
+ yield exc # type: ignore[misc]
92
+ else:
93
+ raise
@@ -0,0 +1,235 @@
1
+ """Core ProgressBar class — async context manager with state machine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ import time
8
+ from typing import TextIO
9
+
10
+ from asyncprogress._eta import EWMACalculator
11
+ from asyncprogress._renderer import BarRenderer
12
+ from asyncprogress._terminal import erase_line, is_tty, move_cursor_up
13
+
14
+
15
+ class ProgressBar:
16
+ """
17
+ Async context manager progress bar.
18
+
19
+ Tracks progress of async operations and renders a live progress bar
20
+ to the terminal (or any file-like object).
21
+
22
+ Example:
23
+ async with ProgressBar(total=100, description="Processing") as bar:
24
+ for item in items:
25
+ await process(item)
26
+ await bar.update()
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ total: int | None = None,
32
+ description: str = "",
33
+ bar_width: int = 40,
34
+ fill_char: str = "█",
35
+ empty_char: str = "░",
36
+ color: str | None = None,
37
+ show_eta: bool = True,
38
+ show_elapsed: bool = True,
39
+ show_count: bool = True,
40
+ show_percentage: bool = True,
41
+ update_interval: float = 0.1,
42
+ ewma_alpha: float = 0.3,
43
+ file: TextIO = sys.stderr,
44
+ disable: bool = False,
45
+ ) -> None:
46
+ self._total = total
47
+ self._description = description
48
+ self._bar_width = bar_width
49
+ self._fill_char = fill_char
50
+ self._empty_char = empty_char
51
+ self._color = color
52
+ self._show_eta = show_eta
53
+ self._show_elapsed = show_elapsed
54
+ self._show_count = show_count
55
+ self._show_percentage = show_percentage
56
+ self._update_interval = update_interval
57
+ self._file = file
58
+ self._disable = disable
59
+
60
+ self._completed: int = 0
61
+ self._start_time: float | None = None
62
+ self._finished: bool = False
63
+ self._spinner_frame: int = 0
64
+
65
+ self._ewma = EWMACalculator(alpha=ewma_alpha)
66
+ self._renderer = BarRenderer()
67
+
68
+ self._render_task: asyncio.Task | None = None # type: ignore[type-arg]
69
+ self._last_line_count: int = 0
70
+
71
+ # Whether this bar is managed by a MultiProgressBar
72
+ self._managed: bool = False
73
+
74
+ async def __aenter__(self) -> ProgressBar:
75
+ self._start_time = time.monotonic()
76
+ if not self._disable and not self._managed:
77
+ self._render_task = asyncio.create_task(self._render_loop())
78
+ return self
79
+
80
+ async def __aexit__(self, *args: object) -> None:
81
+ await self.finish()
82
+ if self._render_task is not None:
83
+ self._render_task.cancel()
84
+ try:
85
+ await self._render_task
86
+ except asyncio.CancelledError:
87
+ pass
88
+ self._render_task = None
89
+
90
+ async def _render_loop(self) -> None:
91
+ """Background task that periodically redraws the progress bar."""
92
+ try:
93
+ while not self._finished:
94
+ self._write_frame()
95
+ await asyncio.sleep(self._update_interval)
96
+ except asyncio.CancelledError:
97
+ pass
98
+
99
+ def _write_frame(self) -> None:
100
+ """Write a single frame to the output file."""
101
+ if self._disable:
102
+ return
103
+ line = self._get_render_string()
104
+ self._erase_previous()
105
+ self._file.write(line + "\n")
106
+ self._file.flush()
107
+ self._last_line_count = 1
108
+
109
+ def _erase_previous(self) -> None:
110
+ """Erase previously written lines."""
111
+ if self._last_line_count > 0 and is_tty(self._file):
112
+ self._file.write(move_cursor_up(self._last_line_count))
113
+ self._file.write(erase_line())
114
+
115
+ def _get_render_string(self) -> str:
116
+ """Build the render string for the current state."""
117
+ if self._total is None:
118
+ self._spinner_frame += 1
119
+ return self._renderer.render_spinner(
120
+ description=self._description,
121
+ elapsed=self.elapsed,
122
+ completed=self._completed,
123
+ frame_index=self._spinner_frame,
124
+ rate=self.rate,
125
+ color=self._color,
126
+ show_elapsed=self._show_elapsed,
127
+ show_count=self._show_count,
128
+ )
129
+ return self._renderer.render(
130
+ description=self._description,
131
+ completed=self._completed,
132
+ total=self._total,
133
+ elapsed=self.elapsed,
134
+ eta=self.eta,
135
+ rate=self.rate,
136
+ bar_width=self._bar_width,
137
+ fill_char=self._fill_char,
138
+ empty_char=self._empty_char,
139
+ color=self._color,
140
+ show_eta=self._show_eta,
141
+ show_elapsed=self._show_elapsed,
142
+ show_count=self._show_count,
143
+ show_percentage=self._show_percentage,
144
+ )
145
+
146
+ async def _render_final(self) -> None:
147
+ """Write the final state of the progress bar."""
148
+ if self._disable:
149
+ return
150
+ line = self._get_render_string()
151
+ self._erase_previous()
152
+ self._file.write(line + "\n")
153
+ self._file.flush()
154
+ self._last_line_count = 1
155
+
156
+ async def update(self, n: int = 1) -> None:
157
+ """
158
+ Increment progress by n steps.
159
+
160
+ Args:
161
+ n: Number of steps to increment. Default is 1.
162
+ """
163
+ self._completed += n
164
+ now = time.monotonic()
165
+ self._ewma.record(now, self._completed)
166
+
167
+ if self._total is not None and self._completed >= self._total:
168
+ await self.finish()
169
+
170
+ async def set(self, value: int) -> None:
171
+ """
172
+ Set absolute progress to value.
173
+
174
+ Args:
175
+ value: Absolute progress value to set.
176
+ """
177
+ self._completed = value
178
+ now = time.monotonic()
179
+ self._ewma.record(now, self._completed)
180
+
181
+ if self._total is not None and self._completed >= self._total:
182
+ await self.finish()
183
+
184
+ async def set_description(self, description: str) -> None:
185
+ """
186
+ Update the description text dynamically.
187
+
188
+ Args:
189
+ description: New description label.
190
+ """
191
+ self._description = description
192
+
193
+ async def finish(self) -> None:
194
+ """Mark as complete regardless of current count."""
195
+ if self._finished:
196
+ return
197
+ self._finished = True
198
+ if self._total is not None:
199
+ self._completed = self._total
200
+ await self._render_final()
201
+
202
+ @property
203
+ def elapsed(self) -> float:
204
+ """Seconds since bar was started."""
205
+ if self._start_time is None:
206
+ return 0.0
207
+ return time.monotonic() - self._start_time
208
+
209
+ @property
210
+ def eta(self) -> float | None:
211
+ """Estimated seconds remaining, or None if unknown."""
212
+ if self._total is None:
213
+ return None
214
+ remaining = self._total - self._completed
215
+ return self._ewma.eta(remaining)
216
+
217
+ @property
218
+ def rate(self) -> float | None:
219
+ """Current EWMA-smoothed items/second rate."""
220
+ return self._ewma.rate()
221
+
222
+ @property
223
+ def description(self) -> str:
224
+ """The current description label for this progress bar."""
225
+ return self._description
226
+
227
+ @property
228
+ def completed(self) -> int:
229
+ """Number of steps completed so far."""
230
+ return self._completed
231
+
232
+ @property
233
+ def total(self) -> int | None:
234
+ """Total steps, or None if indeterminate."""
235
+ return self._total
@@ -0,0 +1,84 @@
1
+ """Exponential Weighted Moving Average (EWMA) calculator for ETA estimation."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class EWMACalculator:
7
+ """
8
+ Calculates smoothed rate and ETA using Exponential Weighted Moving Average.
9
+
10
+ Args:
11
+ alpha: Smoothing factor between 0 and 1. Higher values give more
12
+ weight to recent samples. Recommended range: 0.1 (smooth)
13
+ to 0.5 (reactive). Default is 0.3.
14
+ """
15
+
16
+ def __init__(self, alpha: float = 0.3) -> None:
17
+ if not (0 < alpha <= 1):
18
+ raise ValueError(f"alpha must be in (0, 1], got {alpha}")
19
+ self._alpha = alpha
20
+ self._ewma_rate: float | None = None
21
+ self._last_timestamp: float | None = None
22
+ self._last_completed: int = 0
23
+
24
+ def record(self, timestamp: float, completed: int) -> None:
25
+ """
26
+ Record a completion event.
27
+
28
+ Args:
29
+ timestamp: Current time in seconds (e.g., from time.monotonic()).
30
+ completed: Total number of items completed so far.
31
+ """
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
+ if dt <= 0:
39
+ return
40
+
41
+ delta_completed = completed - self._last_completed
42
+ if delta_completed < 0:
43
+ # Reset if progress went backwards
44
+ self.reset()
45
+ self._last_timestamp = timestamp
46
+ self._last_completed = completed
47
+ return
48
+
49
+ instant_rate = delta_completed / dt
50
+
51
+ if self._ewma_rate is None:
52
+ self._ewma_rate = instant_rate
53
+ else:
54
+ self._ewma_rate = (
55
+ self._alpha * instant_rate + (1 - self._alpha) * self._ewma_rate
56
+ )
57
+
58
+ self._last_timestamp = timestamp
59
+ self._last_completed = completed
60
+
61
+ def rate(self) -> float | None:
62
+ """
63
+ Returns smoothed items/second rate, or None if insufficient data.
64
+ """
65
+ return self._ewma_rate
66
+
67
+ def eta(self, remaining: int) -> float | None:
68
+ """
69
+ Returns estimated seconds to completion, or None if rate is unknown.
70
+
71
+ Args:
72
+ remaining: Number of items remaining to complete.
73
+ """
74
+ if self._ewma_rate is None or self._ewma_rate <= 0:
75
+ return None
76
+ if remaining <= 0:
77
+ return 0.0
78
+ return remaining / self._ewma_rate
79
+
80
+ def reset(self) -> None:
81
+ """Reset all state."""
82
+ self._ewma_rate = None
83
+ self._last_timestamp = None
84
+ self._last_completed = 0
@@ -0,0 +1,112 @@
1
+ """gather() — drop-in replacement for asyncio.gather() with progress tracking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ from collections.abc import Coroutine
8
+ from typing import Any, TextIO
9
+
10
+ from asyncprogress._bar import ProgressBar
11
+
12
+
13
+ async def gather(
14
+ *coros_or_tasks: Coroutine[Any, Any, Any] | asyncio.Task[Any],
15
+ description: str = "",
16
+ return_exceptions: bool = False,
17
+ bar_width: int = 40,
18
+ fill_char: str = "█",
19
+ empty_char: str = "░",
20
+ color: str | None = None,
21
+ show_eta: bool = True,
22
+ show_elapsed: bool = True,
23
+ show_count: bool = True,
24
+ show_percentage: bool = True,
25
+ update_interval: float = 0.1,
26
+ ewma_alpha: float = 0.3,
27
+ file: TextIO = sys.stderr,
28
+ disable: bool = False,
29
+ ) -> list[Any]:
30
+ """
31
+ Drop-in replacement for asyncio.gather() with progress tracking.
32
+
33
+ Returns results in the same order as inputs.
34
+
35
+ Args:
36
+ *coros_or_tasks: Coroutines or Tasks to run concurrently.
37
+ description: Label shown on the progress bar.
38
+ return_exceptions: If True, exceptions are returned as results
39
+ rather than raised (mirrors asyncio.gather).
40
+ bar_width: Width of the bar graphic.
41
+ fill_char: Character for completed portion.
42
+ empty_char: Character for remaining portion.
43
+ color: ANSI color name, or None.
44
+ show_eta: Whether to show ETA.
45
+ show_elapsed: Whether to show elapsed time.
46
+ show_count: Whether to show item count.
47
+ show_percentage: Whether to show percentage.
48
+ update_interval: Seconds between terminal redraws.
49
+ ewma_alpha: EWMA smoothing factor.
50
+ file: Output file. Defaults to sys.stderr.
51
+ disable: If True, suppress all output.
52
+
53
+ Returns:
54
+ List of results in the same order as inputs.
55
+
56
+ Example:
57
+ results = await gather(
58
+ *[fetch(url) for url in urls],
59
+ description="Downloading",
60
+ color="cyan",
61
+ )
62
+ """
63
+ if not coros_or_tasks:
64
+ return []
65
+
66
+ total = len(coros_or_tasks)
67
+ results: list[Any] = [None] * total
68
+ exceptions: list[BaseException | None] = [None] * total
69
+
70
+ async with ProgressBar(
71
+ total=total,
72
+ description=description,
73
+ bar_width=bar_width,
74
+ fill_char=fill_char,
75
+ empty_char=empty_char,
76
+ color=color,
77
+ show_eta=show_eta,
78
+ show_elapsed=show_elapsed,
79
+ show_count=show_count,
80
+ show_percentage=show_percentage,
81
+ update_interval=update_interval,
82
+ ewma_alpha=ewma_alpha,
83
+ file=file,
84
+ disable=disable,
85
+ ) as bar:
86
+ # Wrap each coroutine/task to capture result and update progress
87
+ async def _run_one(index: int, coro: Coroutine[Any, Any, Any] | asyncio.Task[Any]) -> None:
88
+ try:
89
+ result = await coro
90
+ results[index] = result
91
+ except Exception as exc:
92
+ exceptions[index] = exc
93
+ finally:
94
+ await bar.update()
95
+
96
+ wrapped = [_run_one(i, c) for i, c in enumerate(coros_or_tasks)]
97
+ await asyncio.gather(*wrapped)
98
+
99
+ # Re-raise or collect exceptions
100
+ if not return_exceptions:
101
+ for exc in exceptions:
102
+ if exc is not None:
103
+ raise exc
104
+
105
+ final: list[Any] = []
106
+ for i in range(total):
107
+ if exceptions[i] is not None and return_exceptions:
108
+ final.append(exceptions[i])
109
+ else:
110
+ final.append(results[i])
111
+
112
+ return final