asyncprogress 0.3.0__tar.gz → 0.3.1__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.1}/PKG-INFO +15 -10
- asyncprogress-0.3.1/README.md +25 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/pyproject.toml +16 -7
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/__init__.py +7 -8
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/_eta.py +5 -5
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/_renderer.py +46 -50
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/_terminal.py +6 -11
- asyncprogress-0.3.0/README.md +0 -22
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/LICENSE +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/_as_completed.py +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/_bar.py +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/_gather.py +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/_iterator.py +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/_multi.py +0 -0
- {asyncprogress-0.3.0 → asyncprogress-0.3.1}/src/asyncprogress/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asyncprogress
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Async-aware progress bars for Python's asyncio ecosystem
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -22,22 +22,27 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Provides-Extra: color
|
|
24
24
|
Requires-Dist: colorama (>=0.4.4) ; extra == "color"
|
|
25
|
+
Project-URL: Homepage, https://github.com/example/asyncprogress
|
|
26
|
+
Project-URL: Repository, https://github.com/example/asyncprogress
|
|
25
27
|
Description-Content-Type: text/markdown
|
|
26
28
|
|
|
27
29
|
# asyncprogress
|
|
28
30
|
|
|
29
|
-
Async-aware progress bars for Python's asyncio ecosystem. Zero dependencies
|
|
31
|
+
Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies.
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
[](https://pypi.org/project/asyncprogress/)
|
|
34
|
+
[](https://pypi.org/project/asyncprogress/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
## Why asyncprogress?
|
|
38
|
+
|
|
39
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were built for synchronous code. `asyncprogress` is designed from the ground up for `asyncio`:
|
|
32
40
|
|
|
33
41
|
- **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
|
|
42
|
+
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
43
|
+
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
44
|
+
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
45
|
+
- **Zero mandatory dependencies** — pure stdlib `asyncio`
|
|
41
46
|
|
|
42
47
|
## Installation
|
|
43
48
|
|
|
@@ -0,0 +1,25 @@
|
|
|
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 code. `asyncprogress` is designed from the ground up for `asyncio`:
|
|
12
|
+
|
|
13
|
+
- **Native `async for` support** — wrap any sync or async iterable
|
|
14
|
+
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
15
|
+
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
16
|
+
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
17
|
+
- **Zero mandatory dependencies** — pure stdlib `asyncio`
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install asyncprogress
|
|
25
|
+
```
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "asyncprogress"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Async-aware progress bars for Python's asyncio ecosystem"
|
|
5
5
|
authors = ["AgentSoft <agentsoft@example.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
8
|
-
|
|
8
|
+
homepage = "https://github.com/example/asyncprogress"
|
|
9
|
+
repository = "https://github.com/example/asyncprogress"
|
|
9
10
|
keywords = ["asyncio", "progress", "progress-bar", "async", "tqdm"]
|
|
10
11
|
classifiers = [
|
|
11
12
|
"Development Status :: 4 - Beta",
|
|
@@ -19,6 +20,7 @@ classifiers = [
|
|
|
19
20
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
21
|
"Framework :: AsyncIO",
|
|
21
22
|
]
|
|
23
|
+
packages = [{include = "asyncprogress", from = "src"}]
|
|
22
24
|
|
|
23
25
|
[tool.poetry.dependencies]
|
|
24
26
|
python = "^3.9"
|
|
@@ -40,7 +42,18 @@ build-backend = "poetry.core.masonry.api"
|
|
|
40
42
|
[tool.pytest.ini_options]
|
|
41
43
|
asyncio_mode = "auto"
|
|
42
44
|
testpaths = ["tests"]
|
|
43
|
-
addopts = "--cov=
|
|
45
|
+
addopts = "--cov=asyncprogress --cov-report=term-missing --cov-fail-under=80"
|
|
46
|
+
|
|
47
|
+
[tool.coverage.run]
|
|
48
|
+
source = ["src/asyncprogress"]
|
|
49
|
+
|
|
50
|
+
[tool.coverage.report]
|
|
51
|
+
exclude_lines = [
|
|
52
|
+
"pragma: no cover",
|
|
53
|
+
"def __repr__",
|
|
54
|
+
"if TYPE_CHECKING:",
|
|
55
|
+
"raise NotImplementedError",
|
|
56
|
+
]
|
|
44
57
|
|
|
45
58
|
[tool.ruff]
|
|
46
59
|
line-length = 100
|
|
@@ -49,7 +62,3 @@ target-version = "py39"
|
|
|
49
62
|
[tool.ruff.lint]
|
|
50
63
|
select = ["E", "F", "W", "I", "UP"]
|
|
51
64
|
ignore = ["E501"]
|
|
52
|
-
|
|
53
|
-
[tool.coverage.run]
|
|
54
|
-
source = ["src/asyncprogress"]
|
|
55
|
-
omit = ["*/tests/*"]
|
|
@@ -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 stdlib asyncio.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
from __future__ import annotations
|
|
@@ -14,13 +13,13 @@ from asyncprogress._gather import gather
|
|
|
14
13
|
from asyncprogress._iterator import aprogress
|
|
15
14
|
from asyncprogress._multi import MultiProgressBar
|
|
16
15
|
|
|
16
|
+
__version__ = "0.3.1"
|
|
17
|
+
|
|
17
18
|
__all__ = [
|
|
18
19
|
"aprogress",
|
|
19
|
-
"ProgressBar",
|
|
20
|
-
"MultiProgressBar",
|
|
21
|
-
"gather",
|
|
22
|
-
"EWMACalculator",
|
|
23
20
|
"aprogress_as_completed",
|
|
21
|
+
"EWMACalculator",
|
|
22
|
+
"gather",
|
|
23
|
+
"MultiProgressBar",
|
|
24
|
+
"ProgressBar",
|
|
24
25
|
]
|
|
25
|
-
|
|
26
|
-
__version__ = "0.3.0"
|
|
@@ -11,8 +11,8 @@ class EWMACalculator:
|
|
|
11
11
|
"""
|
|
12
12
|
Exponential Weighted Moving Average calculator for ETA estimation.
|
|
13
13
|
|
|
14
|
-
Uses EWMA over completion
|
|
15
|
-
which
|
|
14
|
+
Uses EWMA over completion rates rather than simple linear regression,
|
|
15
|
+
which handles bursty async I/O patterns more accurately.
|
|
16
16
|
|
|
17
17
|
Args:
|
|
18
18
|
alpha: Smoothing factor. Higher = more weight on recent samples.
|
|
@@ -40,16 +40,16 @@ class EWMACalculator:
|
|
|
40
40
|
self._ewma_rate = (
|
|
41
41
|
self._alpha * instant_rate + (1 - self._alpha) * self._ewma_rate
|
|
42
42
|
)
|
|
43
|
-
# If dt == 0,
|
|
43
|
+
# If dt == 0, don't update rate (avoid division by zero)
|
|
44
44
|
self._last_timestamp = timestamp
|
|
45
45
|
self._last_completed = completed
|
|
46
46
|
|
|
47
47
|
def rate(self) -> float | None:
|
|
48
|
-
"""
|
|
48
|
+
"""Returns smoothed items/second, or None if insufficient data."""
|
|
49
49
|
return self._ewma_rate
|
|
50
50
|
|
|
51
51
|
def eta(self, remaining: int) -> float | None:
|
|
52
|
-
"""
|
|
52
|
+
"""Returns estimated seconds to completion, or None if unknown."""
|
|
53
53
|
if remaining == 0:
|
|
54
54
|
return 0.0
|
|
55
55
|
r = self.rate()
|
|
@@ -6,39 +6,38 @@ String formatting only; no I/O. Fully unit-testable.
|
|
|
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 reset at end."""
|
|
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:
|
|
@@ -64,24 +63,24 @@ class BarRenderer:
|
|
|
64
63
|
"""
|
|
65
64
|
Renders progress bar strings for terminal output.
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
String formatting only — no I/O performed here.
|
|
68
67
|
"""
|
|
69
68
|
|
|
70
69
|
def _compute_percentage(self, completed: int, total: int) -> float:
|
|
71
|
-
"""Compute percentage, handling
|
|
70
|
+
"""Compute percentage, handling total=0 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
|
-
"""Format the count
|
|
83
|
+
"""Format the count portion of the bar."""
|
|
85
84
|
if not unit_scale:
|
|
86
85
|
if total is not None:
|
|
87
86
|
return f"[{completed}/{total} {unit}]"
|
|
@@ -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
|
-
"""Format the rate
|
|
101
|
+
"""Format the rate portion of the bar."""
|
|
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,
|
|
@@ -135,44 +134,41 @@ class BarRenderer:
|
|
|
135
134
|
"""
|
|
136
135
|
parts: list[str] = []
|
|
137
136
|
|
|
138
|
-
# Description
|
|
139
137
|
if description:
|
|
140
138
|
parts.append(description)
|
|
141
139
|
|
|
142
|
-
# Bar graphic
|
|
143
140
|
if total is not None:
|
|
144
141
|
pct = self._compute_percentage(completed, total)
|
|
145
|
-
filled = int(bar_width * pct / 100)
|
|
146
|
-
|
|
147
|
-
parts.append(f" {
|
|
142
|
+
filled = int(bar_width * pct / 100) if bar_width > 0 else 0
|
|
143
|
+
bar_str = fill_char * filled + empty_char * (bar_width - filled)
|
|
144
|
+
parts.append(f" {bar_str} ")
|
|
148
145
|
|
|
149
|
-
# Percentage
|
|
150
146
|
if show_percentage:
|
|
151
147
|
parts.append(f"{pct:.0f}%")
|
|
152
148
|
|
|
153
|
-
# Count
|
|
154
149
|
if show_count:
|
|
155
|
-
parts.append(
|
|
150
|
+
parts.append(
|
|
151
|
+
self._format_count(completed, total, unit, unit_scale, unit_divisor)
|
|
152
|
+
)
|
|
156
153
|
else:
|
|
157
154
|
# Indeterminate — no bar graphic
|
|
158
155
|
if show_count:
|
|
159
|
-
parts.append(
|
|
156
|
+
parts.append(
|
|
157
|
+
self._format_count(completed, None, unit, unit_scale, unit_divisor)
|
|
158
|
+
)
|
|
160
159
|
|
|
161
|
-
# Elapsed
|
|
162
160
|
if show_elapsed:
|
|
163
161
|
parts.append(f"⏱ {_format_duration(elapsed)}")
|
|
164
162
|
|
|
165
|
-
# ETA
|
|
166
163
|
if show_eta and eta is not None:
|
|
167
164
|
parts.append(f"ETA: {_format_duration(eta)}")
|
|
168
165
|
|
|
169
|
-
# Rate
|
|
170
166
|
if rate is not None:
|
|
171
167
|
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
172
168
|
if rate_str:
|
|
173
169
|
parts.append(rate_str)
|
|
174
170
|
|
|
175
|
-
line = " ".join(parts)
|
|
171
|
+
line = " ".join(p for p in parts if p)
|
|
176
172
|
if color:
|
|
177
173
|
line = _apply_color(line, color)
|
|
178
174
|
return line
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Terminal utilities
|
|
2
|
+
Terminal utilities — TTY detection and ANSI escape sequences.
|
|
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
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|