python-async-aware-progress-bar 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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/__init__.py +12 -7
- asyncprogress/_as_completed.py +93 -0
- asyncprogress/_bar.py +123 -100
- asyncprogress/_eta.py +41 -34
- asyncprogress/_gather.py +79 -36
- asyncprogress/_iterator.py +30 -22
- asyncprogress/_multi.py +87 -81
- asyncprogress/_renderer.py +131 -43
- asyncprogress/_terminal.py +31 -52
- python_async_aware_progress_bar-0.2.0.dist-info/METADATA +51 -0
- python_async_aware_progress_bar-0.2.0.dist-info/RECORD +14 -0
- python_async_aware_progress_bar-0.1.0.dist-info/METADATA +0 -50
- python_async_aware_progress_bar-0.1.0.dist-info/RECORD +0 -13
- {python_async_aware_progress_bar-0.1.0.dist-info → python_async_aware_progress_bar-0.2.0.dist-info}/WHEEL +0 -0
- {python_async_aware_progress_bar-0.1.0.dist-info → python_async_aware_progress_bar-0.2.0.dist-info}/licenses/LICENSE +0 -0
asyncprogress/__init__.py
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
"""
|
|
2
2
|
asyncprogress — Async-aware progress bar for Python asyncio applications.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
10
11
|
"""
|
|
11
12
|
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from asyncprogress._as_completed import aprogress_as_completed
|
|
12
16
|
from asyncprogress._bar import ProgressBar
|
|
13
17
|
from asyncprogress._eta import EWMACalculator
|
|
14
18
|
from asyncprogress._gather import gather
|
|
15
19
|
from asyncprogress._iterator import aprogress
|
|
16
20
|
from asyncprogress._multi import MultiProgressBar
|
|
17
21
|
|
|
18
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "0.2.0"
|
|
19
23
|
__all__ = [
|
|
20
24
|
"aprogress",
|
|
21
25
|
"ProgressBar",
|
|
22
26
|
"MultiProgressBar",
|
|
23
27
|
"gather",
|
|
24
28
|
"EWMACalculator",
|
|
29
|
+
"aprogress_as_completed",
|
|
25
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
|
asyncprogress/_bar.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Core ProgressBar class
|
|
1
|
+
"""Core ProgressBar class — async context manager with state machine."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -9,28 +9,21 @@ from typing import TextIO
|
|
|
9
9
|
|
|
10
10
|
from asyncprogress._eta import EWMACalculator
|
|
11
11
|
from asyncprogress._renderer import BarRenderer
|
|
12
|
-
from asyncprogress._terminal import
|
|
12
|
+
from asyncprogress._terminal import erase_line, is_tty, move_cursor_up
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class ProgressBar:
|
|
16
16
|
"""
|
|
17
|
-
Async context manager progress bar
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
show_elapsed: Whether to show elapsed time.
|
|
28
|
-
show_count: Whether to show item count.
|
|
29
|
-
show_percentage: Whether to show percentage.
|
|
30
|
-
update_interval: Seconds between terminal redraws.
|
|
31
|
-
ewma_alpha: EWMA smoothing factor (0 < alpha ≤ 1).
|
|
32
|
-
file: Output file (default: sys.stderr).
|
|
33
|
-
disable: If True, suppress all output.
|
|
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()
|
|
34
27
|
"""
|
|
35
28
|
|
|
36
29
|
def __init__(
|
|
@@ -49,156 +42,176 @@ class ProgressBar:
|
|
|
49
42
|
ewma_alpha: float = 0.3,
|
|
50
43
|
file: TextIO = sys.stderr,
|
|
51
44
|
disable: bool = False,
|
|
52
|
-
_manager: object = None,
|
|
53
45
|
) -> None:
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
56
|
-
self.
|
|
57
|
-
self.
|
|
58
|
-
self.
|
|
59
|
-
self.
|
|
60
|
-
self.
|
|
61
|
-
self.
|
|
62
|
-
self.
|
|
63
|
-
self.
|
|
64
|
-
self.
|
|
65
|
-
self.
|
|
66
|
-
self.
|
|
67
|
-
self._manager = _manager
|
|
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
|
|
68
59
|
|
|
69
60
|
self._completed: int = 0
|
|
70
|
-
self._start_time: float =
|
|
61
|
+
self._start_time: float | None = None
|
|
71
62
|
self._finished: bool = False
|
|
63
|
+
self._spinner_frame: int = 0
|
|
64
|
+
|
|
72
65
|
self._ewma = EWMACalculator(alpha=ewma_alpha)
|
|
73
66
|
self._renderer = BarRenderer()
|
|
67
|
+
|
|
74
68
|
self._render_task: asyncio.Task | None = None # type: ignore[type-arg]
|
|
75
|
-
self.
|
|
76
|
-
self._is_tty = is_tty(file)
|
|
69
|
+
self._last_line_count: int = 0
|
|
77
70
|
|
|
78
|
-
|
|
71
|
+
# Whether this bar is managed by a MultiProgressBar
|
|
72
|
+
self._managed: bool = False
|
|
73
|
+
|
|
74
|
+
async def __aenter__(self) -> ProgressBar:
|
|
79
75
|
self._start_time = time.monotonic()
|
|
80
|
-
self.
|
|
81
|
-
|
|
82
|
-
self._ewma.reset()
|
|
83
|
-
if not self.disable and self._manager is None:
|
|
84
|
-
self._render_task = asyncio.get_event_loop().create_task(
|
|
85
|
-
self._render_loop()
|
|
86
|
-
)
|
|
76
|
+
if not self._disable and not self._managed:
|
|
77
|
+
self._render_task = asyncio.create_task(self._render_loop())
|
|
87
78
|
return self
|
|
88
79
|
|
|
89
80
|
async def __aexit__(self, *args: object) -> None:
|
|
90
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
|
|
91
89
|
|
|
92
90
|
async def _render_loop(self) -> None:
|
|
93
91
|
"""Background task that periodically redraws the progress bar."""
|
|
94
92
|
try:
|
|
95
93
|
while not self._finished:
|
|
96
|
-
self.
|
|
97
|
-
await asyncio.sleep(self.
|
|
98
|
-
# Final render after finish
|
|
99
|
-
self._render(final=True)
|
|
94
|
+
self._write_frame()
|
|
95
|
+
await asyncio.sleep(self._update_interval)
|
|
100
96
|
except asyncio.CancelledError:
|
|
101
|
-
|
|
97
|
+
pass
|
|
102
98
|
|
|
103
|
-
def
|
|
104
|
-
"""
|
|
105
|
-
if self.
|
|
99
|
+
def _write_frame(self) -> None:
|
|
100
|
+
"""Write a single frame to the output file."""
|
|
101
|
+
if self._disable:
|
|
106
102
|
return
|
|
107
|
-
line = self.
|
|
108
|
-
|
|
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,
|
|
109
131
|
completed=self._completed,
|
|
110
|
-
total=self.
|
|
132
|
+
total=self._total,
|
|
111
133
|
elapsed=self.elapsed,
|
|
112
134
|
eta=self.eta,
|
|
113
135
|
rate=self.rate,
|
|
114
|
-
bar_width=self.
|
|
115
|
-
fill_char=self.
|
|
116
|
-
empty_char=self.
|
|
117
|
-
color=self.
|
|
118
|
-
show_eta=self.
|
|
119
|
-
show_elapsed=self.
|
|
120
|
-
show_count=self.
|
|
121
|
-
show_percentage=self.
|
|
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,
|
|
122
144
|
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
self.file.write(line + "\n")
|
|
134
|
-
elif final:
|
|
135
|
-
self.file.write(line + "\n")
|
|
136
|
-
self.file.flush()
|
|
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
|
|
137
155
|
|
|
138
156
|
async def update(self, n: int = 1) -> None:
|
|
139
157
|
"""
|
|
140
158
|
Increment progress by n steps.
|
|
141
159
|
|
|
142
160
|
Args:
|
|
143
|
-
n: Number of steps to increment.
|
|
161
|
+
n: Number of steps to increment. Default is 1.
|
|
144
162
|
"""
|
|
145
163
|
self._completed += n
|
|
146
164
|
now = time.monotonic()
|
|
147
165
|
self._ewma.record(now, self._completed)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
# Throttled direct render for non-managed bars without render loop
|
|
152
|
-
# (render loop handles periodic rendering; this is a no-op here)
|
|
166
|
+
|
|
167
|
+
if self._total is not None and self._completed >= self._total:
|
|
168
|
+
await self.finish()
|
|
153
169
|
|
|
154
170
|
async def set(self, value: int) -> None:
|
|
155
171
|
"""
|
|
156
172
|
Set absolute progress to value.
|
|
157
173
|
|
|
158
174
|
Args:
|
|
159
|
-
value: Absolute progress value.
|
|
175
|
+
value: Absolute progress value to set.
|
|
160
176
|
"""
|
|
161
177
|
self._completed = value
|
|
162
178
|
now = time.monotonic()
|
|
163
179
|
self._ewma.record(now, self._completed)
|
|
164
180
|
|
|
181
|
+
if self._total is not None and self._completed >= self._total:
|
|
182
|
+
await self.finish()
|
|
183
|
+
|
|
165
184
|
async def set_description(self, description: str) -> None:
|
|
166
185
|
"""
|
|
167
186
|
Update the description text dynamically.
|
|
168
187
|
|
|
169
188
|
Args:
|
|
170
|
-
description: New description
|
|
189
|
+
description: New description label.
|
|
171
190
|
"""
|
|
172
|
-
self.
|
|
191
|
+
self._description = description
|
|
173
192
|
|
|
174
193
|
async def finish(self) -> None:
|
|
175
194
|
"""Mark as complete regardless of current count."""
|
|
176
195
|
if self._finished:
|
|
177
196
|
return
|
|
178
197
|
self._finished = True
|
|
179
|
-
if self.
|
|
180
|
-
self.
|
|
181
|
-
|
|
182
|
-
await self._render_task
|
|
183
|
-
except asyncio.CancelledError:
|
|
184
|
-
pass
|
|
185
|
-
self._render_task = None
|
|
186
|
-
if not self.disable and self._manager is None:
|
|
187
|
-
self._render(final=True)
|
|
198
|
+
if self._total is not None:
|
|
199
|
+
self._completed = self._total
|
|
200
|
+
await self._render_final()
|
|
188
201
|
|
|
189
202
|
@property
|
|
190
203
|
def elapsed(self) -> float:
|
|
191
204
|
"""Seconds since bar was started."""
|
|
192
|
-
if self._start_time
|
|
205
|
+
if self._start_time is None:
|
|
193
206
|
return 0.0
|
|
194
207
|
return time.monotonic() - self._start_time
|
|
195
208
|
|
|
196
209
|
@property
|
|
197
210
|
def eta(self) -> float | None:
|
|
198
211
|
"""Estimated seconds remaining, or None if unknown."""
|
|
199
|
-
if self.
|
|
212
|
+
if self._total is None:
|
|
200
213
|
return None
|
|
201
|
-
remaining =
|
|
214
|
+
remaining = self._total - self._completed
|
|
202
215
|
return self._ewma.eta(remaining)
|
|
203
216
|
|
|
204
217
|
@property
|
|
@@ -206,7 +219,17 @@ class ProgressBar:
|
|
|
206
219
|
"""Current EWMA-smoothed items/second rate."""
|
|
207
220
|
return self._ewma.rate()
|
|
208
221
|
|
|
222
|
+
@property
|
|
223
|
+
def description(self) -> str:
|
|
224
|
+
"""The current description label for this progress bar."""
|
|
225
|
+
return self._description
|
|
226
|
+
|
|
209
227
|
@property
|
|
210
228
|
def completed(self) -> int:
|
|
211
|
-
"""Number of completed
|
|
229
|
+
"""Number of steps completed so far."""
|
|
212
230
|
return self._completed
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def total(self) -> int | None:
|
|
234
|
+
"""Total steps, or None if indeterminate."""
|
|
235
|
+
return self._total
|
asyncprogress/_eta.py
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
"""EWMA
|
|
1
|
+
"""Exponential Weighted Moving Average (EWMA) calculator for ETA estimation."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class EWMACalculator:
|
|
7
7
|
"""
|
|
8
|
-
Exponential Weighted Moving Average
|
|
9
|
-
|
|
10
|
-
Uses EWMA over completion timestamps to produce smooth rate estimates
|
|
11
|
-
that handle bursty async I/O patterns better than simple linear regression.
|
|
8
|
+
Calculates smoothed rate and ETA using Exponential Weighted Moving Average.
|
|
12
9
|
|
|
13
10
|
Args:
|
|
14
|
-
alpha: Smoothing factor. Higher values
|
|
15
|
-
Recommended range: 0.1 (smooth)
|
|
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.
|
|
16
14
|
"""
|
|
17
15
|
|
|
18
16
|
def __init__(self, alpha: float = 0.3) -> None:
|
|
19
17
|
if not (0 < alpha <= 1):
|
|
20
18
|
raise ValueError(f"alpha must be in (0, 1], got {alpha}")
|
|
21
|
-
self.
|
|
19
|
+
self._alpha = alpha
|
|
22
20
|
self._ewma_rate: float | None = None
|
|
23
21
|
self._last_timestamp: float | None = None
|
|
24
|
-
self._last_completed: int
|
|
22
|
+
self._last_completed: int = 0
|
|
25
23
|
|
|
26
24
|
def record(self, timestamp: float, completed: int) -> None:
|
|
27
25
|
"""
|
|
@@ -31,47 +29,56 @@ class EWMACalculator:
|
|
|
31
29
|
timestamp: Current time in seconds (e.g., from time.monotonic()).
|
|
32
30
|
completed: Total number of items completed so far.
|
|
33
31
|
"""
|
|
34
|
-
if self._last_timestamp is
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
|
|
46
58
|
self._last_timestamp = timestamp
|
|
47
59
|
self._last_completed = completed
|
|
48
60
|
|
|
49
61
|
def rate(self) -> float | None:
|
|
50
62
|
"""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
Smoothed rate in items/second, or None.
|
|
63
|
+
Returns smoothed items/second rate, or None if insufficient data.
|
|
55
64
|
"""
|
|
56
65
|
return self._ewma_rate
|
|
57
66
|
|
|
58
67
|
def eta(self, remaining: int) -> float | None:
|
|
59
68
|
"""
|
|
60
|
-
|
|
69
|
+
Returns estimated seconds to completion, or None if rate is unknown.
|
|
61
70
|
|
|
62
71
|
Args:
|
|
63
|
-
remaining: Number of items remaining.
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
Estimated seconds remaining, or None if rate is unknown.
|
|
72
|
+
remaining: Number of items remaining to complete.
|
|
67
73
|
"""
|
|
68
|
-
|
|
69
|
-
if r is None or r <= 0:
|
|
74
|
+
if self._ewma_rate is None or self._ewma_rate <= 0:
|
|
70
75
|
return None
|
|
71
|
-
|
|
76
|
+
if remaining <= 0:
|
|
77
|
+
return 0.0
|
|
78
|
+
return remaining / self._ewma_rate
|
|
72
79
|
|
|
73
80
|
def reset(self) -> None:
|
|
74
81
|
"""Reset all state."""
|
|
75
82
|
self._ewma_rate = None
|
|
76
83
|
self._last_timestamp = None
|
|
77
|
-
self._last_completed =
|
|
84
|
+
self._last_completed = 0
|