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.
- python_async_aware_progress_bar-0.2.0/PKG-INFO +51 -0
- python_async_aware_progress_bar-0.2.0/README.md +24 -0
- {python_async_aware_progress_bar-0.1.0 → python_async_aware_progress_bar-0.2.0}/pyproject.toml +14 -14
- python_async_aware_progress_bar-0.2.0/src/asyncprogress/__init__.py +30 -0
- python_async_aware_progress_bar-0.2.0/src/asyncprogress/_as_completed.py +93 -0
- python_async_aware_progress_bar-0.2.0/src/asyncprogress/_bar.py +235 -0
- python_async_aware_progress_bar-0.2.0/src/asyncprogress/_eta.py +84 -0
- python_async_aware_progress_bar-0.2.0/src/asyncprogress/_gather.py +112 -0
- {python_async_aware_progress_bar-0.1.0 → python_async_aware_progress_bar-0.2.0}/src/asyncprogress/_iterator.py +30 -22
- python_async_aware_progress_bar-0.2.0/src/asyncprogress/_multi.py +150 -0
- python_async_aware_progress_bar-0.2.0/src/asyncprogress/_renderer.py +190 -0
- python_async_aware_progress_bar-0.2.0/src/asyncprogress/_terminal.py +50 -0
- python_async_aware_progress_bar-0.1.0/PKG-INFO +0 -50
- python_async_aware_progress_bar-0.1.0/README.md +0 -22
- python_async_aware_progress_bar-0.1.0/src/asyncprogress/__init__.py +0 -25
- python_async_aware_progress_bar-0.1.0/src/asyncprogress/_bar.py +0 -212
- python_async_aware_progress_bar-0.1.0/src/asyncprogress/_eta.py +0 -77
- python_async_aware_progress_bar-0.1.0/src/asyncprogress/_gather.py +0 -69
- python_async_aware_progress_bar-0.1.0/src/asyncprogress/_multi.py +0 -144
- python_async_aware_progress_bar-0.1.0/src/asyncprogress/_renderer.py +0 -102
- python_async_aware_progress_bar-0.1.0/src/asyncprogress/_terminal.py +0 -71
- {python_async_aware_progress_bar-0.1.0 → python_async_aware_progress_bar-0.2.0}/LICENSE +0 -0
- {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
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](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
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](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
|
+
```
|
{python_async_aware_progress_bar-0.1.0 → python_async_aware_progress_bar-0.2.0}/pyproject.toml
RENAMED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-async-aware-progress-bar"
|
|
3
|
-
version = "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", "
|
|
9
|
+
keywords = ["async", "asyncio", "progress", "progress-bar", "cli"]
|
|
10
10
|
classifiers = [
|
|
11
|
-
"Development Status ::
|
|
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
|
|
21
|
-
"Topic ::
|
|
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-
|
|
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 =
|
|
49
|
-
src = ["src"]
|
|
48
|
+
line-length = 100
|
|
50
49
|
|
|
51
50
|
[tool.ruff.lint]
|
|
52
|
-
select = ["E", "F", "UP", "I"
|
|
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
|
-
|
|
61
|
-
|
|
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
|