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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asyncprogress
3
- Version: 0.3.0
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, native `async for` support, accurate ETA with EWMA, and concurrent task tracking.
31
+ Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies.
30
32
 
31
- ## Features
33
+ [![PyPI](https://img.shields.io/pypi/v/asyncprogress)](https://pypi.org/project/asyncprogress/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/asyncprogress)](https://pypi.org/project/asyncprogress/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- - **`async with` context manager** — manual progress tracking
35
- - **`gather()` replacement** — drop-in for `asyncio.gather()` with progress
36
- - **`aprogress_as_completed()`**progress-tracked `as_completed` wrapper
37
- - **Spinner mode** — automatic for indeterminate progress
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
+ [![PyPI](https://img.shields.io/pypi/v/asyncprogress)](https://pypi.org/project/asyncprogress/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/asyncprogress)](https://pypi.org/project/asyncprogress/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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.0"
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
- packages = [{include = "asyncprogress", from = "src"}]
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=src/asyncprogress --cov-report=term-missing --cov-fail-under=80"
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, native async for support, accurate EWMA-based ETA,
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 timestamps rather than simple linear regression,
15
- which better accounts for burst patterns common in async I/O workloads.
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, skip this sample (same timestamp)
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
- """Return smoothed items/second, or None if insufficient data."""
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
- """Return estimated seconds to completion, or None if unknown."""
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 = ANSI_COLORS.get(color.lower(), "")
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}{ANSI_RESET}"
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
- No I/O performed here — returns strings only.
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 zero total."""
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 = "it",
81
- unit_scale: bool = False,
82
- unit_divisor: int = 1000,
79
+ unit: str,
80
+ unit_scale: bool,
81
+ unit_divisor: int,
83
82
  ) -> str:
84
- """Format the count display string."""
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 = "it",
99
- unit_scale: bool = False,
100
- unit_divisor: int = 1000,
97
+ unit: str,
98
+ unit_scale: bool,
99
+ unit_divisor: int,
101
100
  ) -> str:
102
- """Format the rate display string."""
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
- val, prefix = _scale_value(rate, unit_divisor)
108
- return f"{val:.1f} {prefix}{unit}/s"
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
- bar = fill_char * filled + empty_char * (bar_width - filled)
147
- parts.append(f" {bar} ")
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(self._format_count(completed, total, unit, unit_scale, unit_divisor))
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(self._format_count(completed, None, unit, unit_scale, unit_divisor))
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: TTY detection, ANSI cursor control sequences.
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
- try:
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
- if n <= 0:
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 "\033[2K\r"
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 "\033[?25l"
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 "\033[?25h"
33
+ return "\x1b[?25h"
@@ -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