asyncprogress 0.3.1__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.1 → asyncprogress-0.3.4}/PKG-INFO +5 -6
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/README.md +3 -2
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/pyproject.toml +11 -17
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/src/asyncprogress/__init__.py +7 -7
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/src/asyncprogress/_bar.py +158 -111
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/src/asyncprogress/_eta.py +23 -18
- asyncprogress-0.3.4/src/asyncprogress/_gather.py +111 -0
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/src/asyncprogress/_iterator.py +21 -19
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/src/asyncprogress/_renderer.py +24 -18
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/src/asyncprogress/_terminal.py +1 -1
- asyncprogress-0.3.1/src/asyncprogress/_gather.py +0 -73
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/LICENSE +0 -0
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/src/asyncprogress/_as_completed.py +0 -0
- {asyncprogress-0.3.1 → asyncprogress-0.3.4}/src/asyncprogress/_multi.py +0 -0
- {asyncprogress-0.3.1 → 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
|
|
@@ -22,8 +22,6 @@ 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
|
|
27
25
|
Description-Content-Type: text/markdown
|
|
28
26
|
|
|
29
27
|
# asyncprogress
|
|
@@ -36,13 +34,14 @@ Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory depende
|
|
|
36
34
|
|
|
37
35
|
## Why asyncprogress?
|
|
38
36
|
|
|
39
|
-
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were built for synchronous
|
|
37
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were built for synchronous
|
|
38
|
+
code. `asyncprogress` is designed from the ground up for `asyncio`:
|
|
40
39
|
|
|
41
40
|
- **Native `async for` support** — wrap any sync or async iterable
|
|
42
41
|
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
43
42
|
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
44
43
|
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
45
|
-
- **Zero mandatory dependencies** — pure stdlib `asyncio`
|
|
44
|
+
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
46
45
|
|
|
47
46
|
## Installation
|
|
48
47
|
|
|
@@ -8,13 +8,14 @@ Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory depende
|
|
|
8
8
|
|
|
9
9
|
## Why asyncprogress?
|
|
10
10
|
|
|
11
|
-
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were built for synchronous
|
|
11
|
+
Existing progress bar libraries (`tqdm`, `rich`, `alive-progress`) were built for synchronous
|
|
12
|
+
code. `asyncprogress` is designed from the ground up for `asyncio`:
|
|
12
13
|
|
|
13
14
|
- **Native `async for` support** — wrap any sync or async iterable
|
|
14
15
|
- **Accurate ETA** — EWMA-based estimation handles bursty async I/O patterns
|
|
15
16
|
- **Concurrent task tracking** — `gather()` and `aprogress_as_completed()` for task pools
|
|
16
17
|
- **Spinner mode** — automatic fallback for unknown-length streams
|
|
17
|
-
- **Zero mandatory dependencies** — pure stdlib `asyncio`
|
|
18
|
+
- **Zero mandatory dependencies** — pure Python stdlib (`asyncio`, `sys`, `time`)
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
|
@@ -1,12 +1,11 @@
|
|
|
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"
|
|
8
|
-
|
|
9
|
-
repository = "https://github.com/example/asyncprogress"
|
|
8
|
+
packages = [{include = "asyncprogress", from = "src"}]
|
|
10
9
|
keywords = ["asyncio", "progress", "progress-bar", "async", "tqdm"]
|
|
11
10
|
classifiers = [
|
|
12
11
|
"Development Status :: 4 - Beta",
|
|
@@ -20,7 +19,6 @@ classifiers = [
|
|
|
20
19
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
20
|
"Framework :: AsyncIO",
|
|
22
21
|
]
|
|
23
|
-
packages = [{include = "asyncprogress", from = "src"}]
|
|
24
22
|
|
|
25
23
|
[tool.poetry.dependencies]
|
|
26
24
|
python = "^3.9"
|
|
@@ -42,18 +40,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
42
40
|
[tool.pytest.ini_options]
|
|
43
41
|
asyncio_mode = "auto"
|
|
44
42
|
testpaths = ["tests"]
|
|
45
|
-
addopts = "--
|
|
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
|
-
]
|
|
43
|
+
addopts = "--tb=short"
|
|
57
44
|
|
|
58
45
|
[tool.ruff]
|
|
59
46
|
line-length = 100
|
|
@@ -62,3 +49,10 @@ target-version = "py39"
|
|
|
62
49
|
[tool.ruff.lint]
|
|
63
50
|
select = ["E", "F", "W", "I", "UP"]
|
|
64
51
|
ignore = ["E501"]
|
|
52
|
+
|
|
53
|
+
[tool.coverage.run]
|
|
54
|
+
source = ["src/asyncprogress"]
|
|
55
|
+
omit = ["tests/*"]
|
|
56
|
+
|
|
57
|
+
[tool.coverage.report]
|
|
58
|
+
show_missing = true
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
asyncprogress — Async-aware progress bars for Python's asyncio ecosystem.
|
|
3
3
|
|
|
4
|
-
Zero mandatory dependencies. Pure stdlib
|
|
4
|
+
Zero mandatory dependencies. Pure Python stdlib.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -13,13 +13,13 @@ from asyncprogress._gather import gather
|
|
|
13
13
|
from asyncprogress._iterator import aprogress
|
|
14
14
|
from asyncprogress._multi import MultiProgressBar
|
|
15
15
|
|
|
16
|
-
__version__ = "0.3.1"
|
|
17
|
-
|
|
18
16
|
__all__ = [
|
|
19
17
|
"aprogress",
|
|
20
|
-
"aprogress_as_completed",
|
|
21
|
-
"EWMACalculator",
|
|
22
|
-
"gather",
|
|
23
|
-
"MultiProgressBar",
|
|
24
18
|
"ProgressBar",
|
|
19
|
+
"MultiProgressBar",
|
|
20
|
+
"gather",
|
|
21
|
+
"EWMACalculator",
|
|
22
|
+
"aprogress_as_completed",
|
|
25
23
|
]
|
|
24
|
+
|
|
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
|
|
@@ -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 handles bursty async I/O
|
|
14
|
+
Uses EWMA over completion timestamps rather than simple linear regression,
|
|
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,22 +25,27 @@ 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
|
|
|
@@ -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,7 +1,7 @@
|
|
|
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
|
|
@@ -33,7 +33,7 @@ _BINARY_PREFIXES = ["", "Ki", "Mi", "Gi", "Ti", "Pi"]
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def _apply_color(text: str, color: str) -> str:
|
|
36
|
-
"""Apply ANSI color to text, returning
|
|
36
|
+
"""Apply ANSI color to text, returning the colored string."""
|
|
37
37
|
code = _ANSI_COLORS.get(color.lower(), "")
|
|
38
38
|
if not code:
|
|
39
39
|
return text
|
|
@@ -41,7 +41,7 @@ def _apply_color(text: str, color: str) -> str:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def _format_duration(seconds: float) -> str:
|
|
44
|
-
"""Format seconds as H:MM:SS or M:SS."""
|
|
44
|
+
"""Format a duration in seconds as H:MM:SS or M:SS."""
|
|
45
45
|
m, s = divmod(int(seconds), 60)
|
|
46
46
|
h, m = divmod(m, 60)
|
|
47
47
|
if h:
|
|
@@ -63,11 +63,11 @@ class BarRenderer:
|
|
|
63
63
|
"""
|
|
64
64
|
Renders progress bar strings for terminal output.
|
|
65
65
|
|
|
66
|
-
String formatting only — no I/O
|
|
66
|
+
String formatting only — no I/O side effects.
|
|
67
67
|
"""
|
|
68
68
|
|
|
69
69
|
def _compute_percentage(self, completed: int, total: int) -> float:
|
|
70
|
-
"""Compute percentage, handling total
|
|
70
|
+
"""Compute percentage, handling zero total gracefully."""
|
|
71
71
|
if total == 0:
|
|
72
72
|
return 100.0 # Empty collection is trivially complete
|
|
73
73
|
return min(100.0, (completed / total) * 100.0)
|
|
@@ -80,7 +80,7 @@ class BarRenderer:
|
|
|
80
80
|
unit_scale: bool,
|
|
81
81
|
unit_divisor: int,
|
|
82
82
|
) -> str:
|
|
83
|
-
"""Format the count
|
|
83
|
+
"""Format the count display string."""
|
|
84
84
|
if not unit_scale:
|
|
85
85
|
if total is not None:
|
|
86
86
|
return f"[{completed}/{total} {unit}]"
|
|
@@ -98,7 +98,7 @@ class BarRenderer:
|
|
|
98
98
|
unit_scale: bool,
|
|
99
99
|
unit_divisor: int,
|
|
100
100
|
) -> str:
|
|
101
|
-
"""Format the rate
|
|
101
|
+
"""Format the rate display string."""
|
|
102
102
|
if rate is None:
|
|
103
103
|
return ""
|
|
104
104
|
if not unit_scale:
|
|
@@ -134,41 +134,47 @@ class BarRenderer:
|
|
|
134
134
|
"""
|
|
135
135
|
parts: list[str] = []
|
|
136
136
|
|
|
137
|
+
# Description
|
|
137
138
|
if description:
|
|
138
139
|
parts.append(description)
|
|
139
140
|
|
|
141
|
+
# Bar graphic
|
|
140
142
|
if total is not None:
|
|
141
143
|
pct = self._compute_percentage(completed, total)
|
|
142
|
-
filled = int(bar_width * pct / 100)
|
|
143
|
-
|
|
144
|
-
parts.append(f" {
|
|
144
|
+
filled = int(bar_width * pct / 100)
|
|
145
|
+
bar = fill_char * filled + empty_char * (bar_width - filled)
|
|
146
|
+
parts.append(f" {bar} ")
|
|
145
147
|
|
|
148
|
+
# Percentage
|
|
146
149
|
if show_percentage:
|
|
147
150
|
parts.append(f"{pct:.0f}%")
|
|
148
151
|
|
|
152
|
+
# Count
|
|
149
153
|
if show_count:
|
|
150
154
|
parts.append(
|
|
151
155
|
self._format_count(completed, total, unit, unit_scale, unit_divisor)
|
|
152
156
|
)
|
|
153
157
|
else:
|
|
154
|
-
#
|
|
158
|
+
# No total — just show count
|
|
155
159
|
if show_count:
|
|
156
160
|
parts.append(
|
|
157
161
|
self._format_count(completed, None, unit, unit_scale, unit_divisor)
|
|
158
162
|
)
|
|
159
163
|
|
|
164
|
+
# Elapsed
|
|
160
165
|
if show_elapsed:
|
|
161
166
|
parts.append(f"⏱ {_format_duration(elapsed)}")
|
|
162
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
|
+
|
|
173
|
+
# ETA
|
|
163
174
|
if show_eta and eta is not None:
|
|
164
175
|
parts.append(f"ETA: {_format_duration(eta)}")
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
|
|
168
|
-
if rate_str:
|
|
169
|
-
parts.append(rate_str)
|
|
170
|
-
|
|
171
|
-
line = " ".join(p for p in parts if p)
|
|
177
|
+
line = " ".join(parts)
|
|
172
178
|
if color:
|
|
173
179
|
line = _apply_color(line, color)
|
|
174
180
|
return line
|
|
@@ -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
|