python-async-aware-progress-bar 0.1.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.
@@ -0,0 +1,25 @@
1
+ """
2
+ asyncprogress — Async-aware progress bar for Python asyncio applications.
3
+
4
+ Provides:
5
+ - aprogress: async generator wrapper for iterables
6
+ - ProgressBar: manual async context manager
7
+ - MultiProgressBar: concurrent progress bars manager
8
+ - gather: drop-in asyncio.gather replacement with progress tracking
9
+ - EWMACalculator: exponential weighted moving average ETA engine
10
+ """
11
+
12
+ from asyncprogress._bar import ProgressBar
13
+ from asyncprogress._eta import EWMACalculator
14
+ from asyncprogress._gather import gather
15
+ from asyncprogress._iterator import aprogress
16
+ from asyncprogress._multi import MultiProgressBar
17
+
18
+ __version__ = "0.1.0"
19
+ __all__ = [
20
+ "aprogress",
21
+ "ProgressBar",
22
+ "MultiProgressBar",
23
+ "gather",
24
+ "EWMACalculator",
25
+ ]
asyncprogress/_bar.py ADDED
@@ -0,0 +1,212 @@
1
+ """Core ProgressBar class: async context manager with update scheduling."""
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
13
+
14
+
15
+ class ProgressBar:
16
+ """
17
+ Async context manager progress bar with EWMA-based ETA estimation.
18
+
19
+ Args:
20
+ total: Total number of items, or None for indeterminate.
21
+ description: Label shown before the bar.
22
+ bar_width: Width of the bar graphic in characters.
23
+ fill_char: Character for completed portion.
24
+ empty_char: Character for remaining portion.
25
+ color: ANSI color name (e.g., "green", "cyan") or None.
26
+ show_eta: Whether to show ETA.
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.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ total: int | None = None,
39
+ description: str = "",
40
+ bar_width: int = 40,
41
+ fill_char: str = "█",
42
+ empty_char: str = "░",
43
+ color: str | None = None,
44
+ show_eta: bool = True,
45
+ show_elapsed: bool = True,
46
+ show_count: bool = True,
47
+ show_percentage: bool = True,
48
+ update_interval: float = 0.1,
49
+ ewma_alpha: float = 0.3,
50
+ file: TextIO = sys.stderr,
51
+ disable: bool = False,
52
+ _manager: object = None,
53
+ ) -> None:
54
+ self.total = total
55
+ self.description = description
56
+ self.bar_width = bar_width
57
+ self.fill_char = fill_char
58
+ self.empty_char = empty_char
59
+ self.color = color
60
+ self.show_eta = show_eta
61
+ self.show_elapsed = show_elapsed
62
+ self.show_count = show_count
63
+ self.show_percentage = show_percentage
64
+ self.update_interval = update_interval
65
+ self.file = file
66
+ self.disable = disable
67
+ self._manager = _manager
68
+
69
+ self._completed: int = 0
70
+ self._start_time: float = 0.0
71
+ self._finished: bool = False
72
+ self._ewma = EWMACalculator(alpha=ewma_alpha)
73
+ self._renderer = BarRenderer()
74
+ self._render_task: asyncio.Task | None = None # type: ignore[type-arg]
75
+ self._last_render_time: float = 0.0
76
+ self._is_tty = is_tty(file)
77
+
78
+ async def __aenter__(self) -> "ProgressBar":
79
+ self._start_time = time.monotonic()
80
+ self._completed = 0
81
+ self._finished = False
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
+ )
87
+ return self
88
+
89
+ async def __aexit__(self, *args: object) -> None:
90
+ await self.finish()
91
+
92
+ async def _render_loop(self) -> None:
93
+ """Background task that periodically redraws the progress bar."""
94
+ try:
95
+ while not self._finished:
96
+ self._render()
97
+ await asyncio.sleep(self.update_interval)
98
+ # Final render after finish
99
+ self._render(final=True)
100
+ except asyncio.CancelledError:
101
+ self._render(final=True)
102
+
103
+ def _render(self, final: bool = False) -> None:
104
+ """Render the progress bar to the output file."""
105
+ if self.disable:
106
+ return
107
+ line = self._renderer.render(
108
+ description=self.description,
109
+ completed=self._completed,
110
+ total=self.total,
111
+ elapsed=self.elapsed,
112
+ eta=self.eta,
113
+ rate=self.rate,
114
+ bar_width=self.bar_width,
115
+ fill_char=self.fill_char,
116
+ empty_char=self.empty_char,
117
+ color=self.color,
118
+ show_eta=self.show_eta,
119
+ show_elapsed=self.show_elapsed,
120
+ show_count=self.show_count,
121
+ show_percentage=self.show_percentage,
122
+ )
123
+ if self._is_tty:
124
+ self.file.write(f"{ERASE_LINE}{line}")
125
+ if final:
126
+ self.file.write("\n")
127
+ else:
128
+ # Non-TTY: print milestone percentages
129
+ if self.total and self.total > 0:
130
+ pct = int(self._completed / self.total * 100)
131
+ milestones = [10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 100]
132
+ if pct in milestones or final:
133
+ self.file.write(line + "\n")
134
+ elif final:
135
+ self.file.write(line + "\n")
136
+ self.file.flush()
137
+
138
+ async def update(self, n: int = 1) -> None:
139
+ """
140
+ Increment progress by n steps.
141
+
142
+ Args:
143
+ n: Number of steps to increment.
144
+ """
145
+ self._completed += n
146
+ now = time.monotonic()
147
+ self._ewma.record(now, self._completed)
148
+ if self._manager is not None:
149
+ # Managed bars delegate rendering to the manager
150
+ return
151
+ # Throttled direct render for non-managed bars without render loop
152
+ # (render loop handles periodic rendering; this is a no-op here)
153
+
154
+ async def set(self, value: int) -> None:
155
+ """
156
+ Set absolute progress to value.
157
+
158
+ Args:
159
+ value: Absolute progress value.
160
+ """
161
+ self._completed = value
162
+ now = time.monotonic()
163
+ self._ewma.record(now, self._completed)
164
+
165
+ async def set_description(self, description: str) -> None:
166
+ """
167
+ Update the description text dynamically.
168
+
169
+ Args:
170
+ description: New description string.
171
+ """
172
+ self.description = description
173
+
174
+ async def finish(self) -> None:
175
+ """Mark as complete regardless of current count."""
176
+ if self._finished:
177
+ return
178
+ self._finished = True
179
+ if self._render_task is not None:
180
+ self._render_task.cancel()
181
+ try:
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)
188
+
189
+ @property
190
+ def elapsed(self) -> float:
191
+ """Seconds since bar was started."""
192
+ if self._start_time == 0.0:
193
+ return 0.0
194
+ return time.monotonic() - self._start_time
195
+
196
+ @property
197
+ def eta(self) -> float | None:
198
+ """Estimated seconds remaining, or None if unknown."""
199
+ if self.total is None:
200
+ return None
201
+ remaining = max(0, self.total - self._completed)
202
+ return self._ewma.eta(remaining)
203
+
204
+ @property
205
+ def rate(self) -> float | None:
206
+ """Current EWMA-smoothed items/second rate."""
207
+ return self._ewma.rate()
208
+
209
+ @property
210
+ def completed(self) -> int:
211
+ """Number of completed items."""
212
+ return self._completed
asyncprogress/_eta.py ADDED
@@ -0,0 +1,77 @@
1
+ """EWMA-based ETA calculator for asyncprogress."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class EWMACalculator:
7
+ """
8
+ Exponential Weighted Moving Average calculator for ETA estimation.
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.
12
+
13
+ Args:
14
+ alpha: Smoothing factor. Higher values weight recent samples more.
15
+ Recommended range: 0.1 (smooth) to 0.5 (reactive).
16
+ """
17
+
18
+ def __init__(self, alpha: float = 0.3) -> None:
19
+ if not (0 < alpha <= 1):
20
+ raise ValueError(f"alpha must be in (0, 1], got {alpha}")
21
+ self.alpha = alpha
22
+ self._ewma_rate: float | None = None
23
+ self._last_timestamp: float | None = None
24
+ self._last_completed: int | None = None
25
+
26
+ def record(self, timestamp: float, completed: int) -> None:
27
+ """
28
+ Record a completion event.
29
+
30
+ Args:
31
+ timestamp: Current time in seconds (e.g., from time.monotonic()).
32
+ completed: Total number of items completed so far.
33
+ """
34
+ if self._last_timestamp is not None and self._last_completed is not None:
35
+ dt = timestamp - self._last_timestamp
36
+ delta = completed - self._last_completed
37
+ if dt > 0 and delta >= 0:
38
+ instant_rate = delta / dt
39
+ if self._ewma_rate is None:
40
+ self._ewma_rate = instant_rate
41
+ else:
42
+ self._ewma_rate = (
43
+ self.alpha * instant_rate
44
+ + (1 - self.alpha) * self._ewma_rate
45
+ )
46
+ self._last_timestamp = timestamp
47
+ self._last_completed = completed
48
+
49
+ def rate(self) -> float | None:
50
+ """
51
+ Return smoothed items/second rate, or None if insufficient data.
52
+
53
+ Returns:
54
+ Smoothed rate in items/second, or None.
55
+ """
56
+ return self._ewma_rate
57
+
58
+ def eta(self, remaining: int) -> float | None:
59
+ """
60
+ Return estimated seconds to completion.
61
+
62
+ Args:
63
+ remaining: Number of items remaining.
64
+
65
+ Returns:
66
+ Estimated seconds remaining, or None if rate is unknown.
67
+ """
68
+ r = self._ewma_rate
69
+ if r is None or r <= 0:
70
+ return None
71
+ return remaining / r
72
+
73
+ def reset(self) -> None:
74
+ """Reset all state."""
75
+ self._ewma_rate = None
76
+ self._last_timestamp = None
77
+ self._last_completed = None
@@ -0,0 +1,69 @@
1
+ """gather(): drop-in asyncio.gather replacement with progress tracking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Coroutine
7
+ from typing import Any
8
+
9
+ from asyncprogress._bar import ProgressBar
10
+
11
+
12
+ async def gather(
13
+ *coros_or_tasks: Coroutine[Any, Any, Any] | asyncio.Task[Any],
14
+ description: str = "",
15
+ return_exceptions: bool = False,
16
+ **progress_kwargs: Any,
17
+ ) -> list[Any]:
18
+ """
19
+ Drop-in replacement for asyncio.gather() with progress tracking.
20
+
21
+ Returns results in the same order as inputs.
22
+
23
+ Args:
24
+ *coros_or_tasks: Coroutines or Tasks to execute concurrently.
25
+ description: Label shown on the progress bar.
26
+ return_exceptions: If True, exceptions are returned as results
27
+ rather than raised.
28
+ **progress_kwargs: Additional kwargs forwarded to ProgressBar.
29
+
30
+ Returns:
31
+ List of results in the same order as the input coroutines/tasks.
32
+ """
33
+ n = len(coros_or_tasks)
34
+ bar = ProgressBar(
35
+ total=n,
36
+ description=description,
37
+ **progress_kwargs,
38
+ )
39
+
40
+ results: list[Any] = [None] * n
41
+
42
+ async with bar:
43
+ tasks: list[asyncio.Task[Any]] = []
44
+ for coro_or_task in coros_or_tasks:
45
+ if isinstance(coro_or_task, asyncio.Task):
46
+ tasks.append(coro_or_task)
47
+ else:
48
+ tasks.append(asyncio.create_task(coro_or_task))
49
+
50
+ async def _run_and_update(
51
+ idx: int, task: asyncio.Task[Any]
52
+ ) -> None:
53
+ try:
54
+ results[idx] = await task
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
+ wrappers = [
64
+ asyncio.create_task(_run_and_update(i, t))
65
+ for i, t in enumerate(tasks)
66
+ ]
67
+ await asyncio.gather(*wrappers, return_exceptions=return_exceptions)
68
+
69
+ return results
@@ -0,0 +1,90 @@
1
+ """aprogress(): async generator wrapper for sync and async iterables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import AsyncIterable, AsyncIterator, Iterable
7
+ from typing import TypeVar, Union
8
+
9
+ from asyncprogress._bar import ProgressBar
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ async def aprogress(
15
+ iterable: Union[AsyncIterable[T], Iterable[T]],
16
+ *,
17
+ total: int | None = None,
18
+ description: str = "",
19
+ bar_width: int = 40,
20
+ fill_char: str = "█",
21
+ empty_char: str = "░",
22
+ color: str | None = None,
23
+ show_eta: bool = True,
24
+ show_elapsed: bool = True,
25
+ show_count: bool = True,
26
+ show_percentage: bool = True,
27
+ update_interval: float = 0.1,
28
+ ewma_alpha: float = 0.3,
29
+ file: object = sys.stderr,
30
+ disable: bool = False,
31
+ ) -> AsyncIterator[T]:
32
+ """
33
+ Async generator wrapper that tracks progress over an iterable.
34
+
35
+ Supports both sync iterables (e.g., lists, ranges) and async iterables
36
+ (e.g., async generators). Auto-detects total from ``__len__`` if not
37
+ provided.
38
+
39
+ Args:
40
+ iterable: Any sync or async iterable to wrap.
41
+ total: Total items. Auto-detected from ``__len__`` if available.
42
+ description: Label shown before the bar.
43
+ bar_width: Width of the bar graphic in characters.
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 (0 < alpha ≤ 1).
53
+ file: Output file (default: sys.stderr).
54
+ disable: If True, suppress all output.
55
+
56
+ Yields:
57
+ Items from the wrapped iterable.
58
+ """
59
+ # Auto-detect total from __len__
60
+ if total is None and hasattr(iterable, "__len__"):
61
+ total = len(iterable) # type: ignore[arg-type]
62
+
63
+ from typing import TextIO # noqa: PLC0415
64
+
65
+ bar = ProgressBar(
66
+ total=total,
67
+ description=description,
68
+ bar_width=bar_width,
69
+ fill_char=fill_char,
70
+ empty_char=empty_char,
71
+ color=color,
72
+ show_eta=show_eta,
73
+ show_elapsed=show_elapsed,
74
+ show_count=show_count,
75
+ show_percentage=show_percentage,
76
+ update_interval=update_interval,
77
+ ewma_alpha=ewma_alpha,
78
+ file=file, # type: ignore[arg-type]
79
+ disable=disable,
80
+ )
81
+
82
+ async with bar:
83
+ if isinstance(iterable, AsyncIterable):
84
+ async for item in iterable:
85
+ yield item
86
+ await bar.update()
87
+ else:
88
+ for item in iterable: # type: ignore[union-attr]
89
+ yield item
90
+ await bar.update()
@@ -0,0 +1,144 @@
1
+ """MultiProgressBar: manages multiple concurrent progress bars."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ from typing import TextIO
8
+
9
+ from asyncprogress._bar import ProgressBar
10
+ from asyncprogress._terminal import ERASE_LINE, is_tty, move_cursor_up
11
+
12
+
13
+ class MultiProgressBar:
14
+ """
15
+ Manages multiple concurrent progress bars with a shared render loop.
16
+
17
+ Args:
18
+ file: Output file (default: sys.stderr).
19
+ update_interval: Seconds between terminal redraws.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ file: TextIO = sys.stderr,
25
+ update_interval: float = 0.1,
26
+ ) -> None:
27
+ self.file = file
28
+ self.update_interval = update_interval
29
+ self._bars: list[ProgressBar] = []
30
+ self._render_task: asyncio.Task | None = None # type: ignore[type-arg]
31
+ self._is_tty = is_tty(file)
32
+ self._lines_written: int = 0
33
+
34
+ async def __aenter__(self) -> "MultiProgressBar":
35
+ self._bars = []
36
+ self._lines_written = 0
37
+ self._render_task = asyncio.get_event_loop().create_task(
38
+ self._render_loop()
39
+ )
40
+ return self
41
+
42
+ async def __aexit__(self, *args: object) -> None:
43
+ if self._render_task is not None:
44
+ self._render_task.cancel()
45
+ try:
46
+ await self._render_task
47
+ except asyncio.CancelledError:
48
+ pass
49
+ self._render_task = None
50
+ # Final render
51
+ self._render_all(final=True)
52
+
53
+ async def _render_loop(self) -> None:
54
+ """Background task that periodically redraws all progress bars."""
55
+ try:
56
+ while True:
57
+ self._render_all()
58
+ await asyncio.sleep(self.update_interval)
59
+ except asyncio.CancelledError:
60
+ pass
61
+
62
+ def _render_all(self, final: bool = False) -> None:
63
+ """Render all bars to the output file."""
64
+ if not self._bars:
65
+ return
66
+
67
+ lines: list[str] = []
68
+ for bar in self._bars:
69
+ import time # noqa: PLC0415
70
+
71
+ bar._start_time = bar._start_time or time.monotonic()
72
+ line = bar._renderer.render(
73
+ description=bar.description,
74
+ completed=bar._completed,
75
+ total=bar.total,
76
+ elapsed=bar.elapsed,
77
+ eta=bar.eta,
78
+ rate=bar.rate,
79
+ bar_width=bar.bar_width,
80
+ fill_char=bar.fill_char,
81
+ empty_char=bar.empty_char,
82
+ color=bar.color,
83
+ show_eta=bar.show_eta,
84
+ show_elapsed=bar.show_elapsed,
85
+ show_count=bar.show_count,
86
+ show_percentage=bar.show_percentage,
87
+ )
88
+ lines.append(line)
89
+
90
+ if self._is_tty and self._lines_written > 0:
91
+ # Move cursor up to overwrite previous output
92
+ self.file.write(move_cursor_up(self._lines_written))
93
+
94
+ output = ""
95
+ for line in lines:
96
+ if self._is_tty:
97
+ output += f"{ERASE_LINE}{line}\n"
98
+ else:
99
+ output += line + "\n"
100
+
101
+ self.file.write(output)
102
+ self.file.flush()
103
+ self._lines_written = len(lines)
104
+
105
+ def add(
106
+ self,
107
+ description: str = "",
108
+ total: int | None = None,
109
+ **bar_kwargs: object,
110
+ ) -> ProgressBar:
111
+ """
112
+ Register a new progress bar.
113
+
114
+ Args:
115
+ description: Label for this bar.
116
+ total: Total items, or None for indeterminate.
117
+ **bar_kwargs: Additional kwargs forwarded to ProgressBar.
118
+
119
+ Returns:
120
+ A ProgressBar instance managed by this MultiProgressBar.
121
+ """
122
+ import time # noqa: PLC0415
123
+
124
+ bar = ProgressBar(
125
+ total=total,
126
+ description=description,
127
+ file=self.file,
128
+ disable=False,
129
+ _manager=self,
130
+ **bar_kwargs,
131
+ )
132
+ bar._start_time = time.monotonic()
133
+ self._bars.append(bar)
134
+ return bar
135
+
136
+ def remove(self, bar: ProgressBar) -> None:
137
+ """
138
+ Remove a bar from the display.
139
+
140
+ Args:
141
+ bar: The ProgressBar instance to remove.
142
+ """
143
+ if bar in self._bars:
144
+ self._bars.remove(bar)
@@ -0,0 +1,102 @@
1
+ """Bar rendering: string formatting for progress bars."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from asyncprogress._terminal import colorize
6
+
7
+
8
+ def _format_time(seconds: float) -> str:
9
+ """Format seconds as H:MM:SS or M:SS."""
10
+ seconds = int(seconds)
11
+ h = seconds // 3600
12
+ m = (seconds % 3600) // 60
13
+ s = seconds % 60
14
+ if h > 0:
15
+ return f"{h}:{m:02d}:{s:02d}"
16
+ return f"{m}:{s:02d}"
17
+
18
+
19
+ class BarRenderer:
20
+ """
21
+ Renders a single-line progress bar string.
22
+
23
+ All methods are pure (no I/O), making this fully unit-testable.
24
+ """
25
+
26
+ def render(
27
+ self,
28
+ *,
29
+ description: str,
30
+ completed: int,
31
+ total: int | None,
32
+ elapsed: float,
33
+ eta: float | None,
34
+ rate: float | None,
35
+ bar_width: int,
36
+ fill_char: str,
37
+ empty_char: str,
38
+ color: str | None,
39
+ show_eta: bool,
40
+ show_elapsed: bool,
41
+ show_count: bool,
42
+ show_percentage: bool,
43
+ ) -> str:
44
+ """
45
+ Render a single-line progress bar string.
46
+
47
+ Args:
48
+ description: Label text shown before the bar.
49
+ completed: Number of completed items.
50
+ total: Total items, or None for indeterminate.
51
+ elapsed: Seconds elapsed since start.
52
+ eta: Estimated seconds remaining, or None.
53
+ rate: Current items/second rate, or None.
54
+ bar_width: Width of the bar graphic in characters.
55
+ fill_char: Character used for completed portion.
56
+ empty_char: Character used for remaining portion.
57
+ color: ANSI color name or None.
58
+ show_eta: Whether to show ETA.
59
+ show_elapsed: Whether to show elapsed time.
60
+ show_count: Whether to show item count.
61
+ show_percentage: Whether to show percentage.
62
+
63
+ Returns:
64
+ A single-line string ready for terminal output.
65
+ """
66
+ parts: list[str] = []
67
+
68
+ if description:
69
+ parts.append(description)
70
+
71
+ if total is not None and total > 0:
72
+ fraction = min(completed / total, 1.0)
73
+ filled = int(bar_width * fraction)
74
+ empty = bar_width - filled
75
+ bar_str = fill_char * filled + empty_char * empty
76
+ bar_str = colorize(f"[{bar_str}]", color)
77
+ parts.append(bar_str)
78
+
79
+ if show_percentage:
80
+ parts.append(f"{fraction * 100:.0f}%")
81
+
82
+ if show_count:
83
+ parts.append(f"[{completed}/{total}]")
84
+ else:
85
+ # Indeterminate mode — spinning indicator
86
+ spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
87
+ idx = int(elapsed * 10) % len(spinner_chars)
88
+ spinner = spinner_chars[idx]
89
+ parts.append(colorize(spinner, color))
90
+ if show_count:
91
+ parts.append(f"[{completed}]")
92
+
93
+ if show_elapsed:
94
+ parts.append(f"⏱ {_format_time(elapsed)}")
95
+
96
+ if show_eta:
97
+ if eta is not None:
98
+ parts.append(f"ETA: {_format_time(eta)}")
99
+ elif total is not None:
100
+ parts.append("ETA: ?")
101
+
102
+ return " ".join(parts)
@@ -0,0 +1,71 @@
1
+ """Terminal utilities: TTY detection, ANSI escape sequences, cursor control."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import TextIO
7
+
8
+ # ANSI color codes
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
+ CURSOR_UP = "\033[{n}A"
30
+ ERASE_LINE = "\033[2K\r"
31
+ CURSOR_TO_COL0 = "\r"
32
+
33
+
34
+ def is_tty(file: TextIO = sys.stderr) -> bool:
35
+ """Return True if the given file is a TTY."""
36
+ return hasattr(file, "isatty") and file.isatty()
37
+
38
+
39
+ def colorize(text: str, color: str | None) -> str:
40
+ """
41
+ Wrap text with ANSI color codes.
42
+
43
+ Args:
44
+ text: The text to colorize.
45
+ color: Color name (e.g., "green", "cyan") or None for no color.
46
+
47
+ Returns:
48
+ Colorized string, or original string if color is None/unknown.
49
+ """
50
+ if color is None:
51
+ return text
52
+ code = ANSI_COLORS.get(color.lower())
53
+ if code is None:
54
+ return text
55
+ return f"{code}{text}{ANSI_RESET}"
56
+
57
+
58
+ def move_cursor_up(n: int) -> str:
59
+ """Return ANSI escape sequence to move cursor up n lines."""
60
+ return CURSOR_UP.format(n=n)
61
+
62
+
63
+ def erase_line() -> str:
64
+ """Return ANSI escape sequence to erase current line."""
65
+ return ERASE_LINE
66
+
67
+
68
+ def write_line(file: TextIO, text: str) -> None:
69
+ """Write a line to file, handling TTY vs non-TTY."""
70
+ file.write(text)
71
+ file.flush()
asyncprogress/py.typed ADDED
File without changes
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-async-aware-progress-bar
3
+ Version: 0.1.0
4
+ Summary: Async-aware progress bar for Python asyncio applications
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: asyncio,progress,progress-bar,async,tqdm
8
+ Author: AgentSoft
9
+ Author-email: agentsoft@example.com
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: Development Status :: 3 - Alpha
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 :: Python Modules
23
+ Classifier: Topic :: Utilities
24
+ Provides-Extra: color
25
+ Requires-Dist: colorama (>=0.4.4) ; extra == "color"
26
+ Description-Content-Type: text/markdown
27
+
28
+ # asyncprogress
29
+
30
+ An async-aware progress bar library for Python's asyncio ecosystem.
31
+
32
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue)](https://python.org)
33
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
34
+
35
+ ## Overview
36
+
37
+ `asyncprogress` provides first-class progress bar support for async Python code. Unlike existing solutions (tqdm, rich, alive-progress), it is designed from the ground up for `asyncio` with:
38
+
39
+ - Native `async for` support
40
+ - Accurate ETA using Exponential Weighted Moving Average (EWMA)
41
+ - Concurrent task pool tracking
42
+ - `await`-able context managers
43
+ - Multiple concurrent progress bars
44
+ - Zero mandatory runtime dependencies
45
+
46
+ ## Installation
47
+
48
+ ### Using pip
49
+
50
+
@@ -0,0 +1,13 @@
1
+ asyncprogress/__init__.py,sha256=IDj9XPQWal7ek25D5aD9jufJObsqIsaogWBcu6nypFc,756
2
+ asyncprogress/_bar.py,sha256=d1alXhxYsGnOsTqkRJ5VkV_9yCls7IMx8kFMTaEV5Dc,7061
3
+ asyncprogress/_eta.py,sha256=Myx6LRU8FB2XV0jqfoLRvWn-Wd0tHJx5X4qWQEggpYU,2536
4
+ asyncprogress/_gather.py,sha256=KpsV7cbs8UDGc8L1S07ajn9I8wbrafAS0Yx1ZoKHTio,2021
5
+ asyncprogress/_iterator.py,sha256=ehkP1exBYKl38xo9HpBaujXm5KLLENf40ujybybrhe8,2874
6
+ asyncprogress/_multi.py,sha256=r5O2UqqTsbf6s_Of_rKzIONo3Ui1f1OnbFU_eTSrDbw,4282
7
+ asyncprogress/_renderer.py,sha256=y9fWpn0ZI6isl_bEN5cS1qbjbusuqS1JNbVwvZ5uSbs,3205
8
+ asyncprogress/_terminal.py,sha256=8Z59rt5-nBZxA8HzXDOZaB8nljVg6mygnJT_lKhU8B4,1774
9
+ asyncprogress/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ python_async_aware_progress_bar-0.1.0.dist-info/METADATA,sha256=1fN_RyrIQRydjVqYJNq_JxnztMM462CypMtT9PvUyf4,1762
11
+ python_async_aware_progress_bar-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
+ python_async_aware_progress_bar-0.1.0.dist-info/licenses/LICENSE,sha256=6Y47RSQ-wXvZrjkkdspmKQ7pAlT-Qq6N6eWBjFqOjoo,1066
13
+ python_async_aware_progress_bar-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 AgentSoft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.