asyncprogress 0.3.5__tar.gz → 0.3.6__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,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asyncprogress
3
- Version: 0.3.5
4
- Summary: Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies.
3
+ Version: 0.3.6
4
+ Summary: Async-aware progress bars for Python's asyncio ecosystem
5
5
  License: MIT
6
6
  License-File: LICENSE
7
- Keywords: asyncio,progress,progress-bar,async,tqdm
7
+ Keywords: async,asyncio,progress,progress-bar,tqdm
8
8
  Author: AgentSoft
9
9
  Author-email: agentsoft@example.com
10
10
  Requires-Python: >=3.9,<4.0
@@ -19,9 +19,8 @@ Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Programming Language :: Python :: 3.14
22
- Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Provides-Extra: color
24
- Requires-Dist: colorama (>=0.4.4) ; extra == "color"
25
24
  Project-URL: Homepage, https://github.com/agentsoft/asyncprogress
26
25
  Project-URL: Repository, https://github.com/agentsoft/asyncprogress
27
26
  Description-Content-Type: text/markdown
@@ -49,7 +48,3 @@ synchronous code. `asyncprogress` is built from the ground up for `asyncio`:
49
48
  ## Installation
50
49
 
51
50
 
52
-
53
- ```bash
54
- pip install asyncprogress
55
- ```
@@ -20,8 +20,3 @@ synchronous code. `asyncprogress` is built from the ground up for `asyncio`:
20
20
 
21
21
  ## Installation
22
22
 
23
-
24
-
25
- ```bash
26
- pip install asyncprogress
27
- ```
@@ -1,13 +1,13 @@
1
1
  [tool.poetry]
2
2
  name = "asyncprogress"
3
- version = "0.3.5"
4
- description = "Async-aware progress bars for Python's asyncio ecosystem. Zero mandatory dependencies."
3
+ version = "0.3.6"
4
+ description = "Async-aware progress bars for Python's asyncio ecosystem"
5
5
  authors = ["AgentSoft <agentsoft@example.com>"]
6
6
  readme = "README.md"
7
7
  license = "MIT"
8
8
  homepage = "https://github.com/agentsoft/asyncprogress"
9
9
  repository = "https://github.com/agentsoft/asyncprogress"
10
- keywords = ["asyncio", "progress", "progress-bar", "async", "tqdm"]
10
+ keywords = ["async", "asyncio", "progress", "progress-bar", "tqdm"]
11
11
  classifiers = [
12
12
  "Development Status :: 4 - Beta",
13
13
  "Intended Audience :: Developers",
@@ -17,14 +17,13 @@ classifiers = [
17
17
  "Programming Language :: Python :: 3.10",
18
18
  "Programming Language :: Python :: 3.11",
19
19
  "Programming Language :: Python :: 3.12",
20
- "Topic :: Software Development :: Libraries",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
21
  "Framework :: AsyncIO",
22
22
  ]
23
23
  packages = [{include = "asyncprogress", from = "src"}]
24
24
 
25
25
  [tool.poetry.dependencies]
26
26
  python = "^3.9"
27
- colorama = {version = ">=0.4.4", optional = true}
28
27
 
29
28
  [tool.poetry.extras]
30
29
  color = ["colorama"]
@@ -42,19 +41,18 @@ build-backend = "poetry.core.masonry.api"
42
41
  [tool.pytest.ini_options]
43
42
  asyncio_mode = "auto"
44
43
  testpaths = ["tests"]
45
- addopts = "--tb=short"
46
44
 
47
45
  [tool.ruff]
48
46
  line-length = 100
49
47
  target-version = "py39"
50
48
 
51
49
  [tool.ruff.lint]
52
- select = ["E", "F", "I", "UP"]
50
+ select = ["E", "F", "UP", "I"]
53
51
  ignore = ["E501"]
54
52
 
55
53
  [tool.coverage.run]
56
- source = ["src/asyncprogress"]
57
- omit = ["tests/*"]
54
+ source = ["asyncprogress"]
55
+ branch = true
58
56
 
59
57
  [tool.coverage.report]
60
58
  show_missing = true
@@ -22,4 +22,4 @@ __all__ = [
22
22
  "aprogress_as_completed",
23
23
  ]
24
24
 
25
- __version__ = "0.3.5"
25
+ __version__ = "0.3.6"
@@ -1,7 +1,5 @@
1
1
  """
2
- EWMA-based ETA calculator for asyncprogress.
3
-
4
- Pure math — no I/O, fully unit-testable.
2
+ EWMA-based ETA calculator for async progress tracking.
5
3
  """
6
4
 
7
5
  from __future__ import annotations
@@ -11,12 +9,11 @@ class EWMACalculator:
11
9
  """
12
10
  Exponential Weighted Moving Average calculator for ETA estimation.
13
11
 
14
- Uses EWMA over completion timestamps rather than simple linear regression,
15
- which better handles the bursty completion patterns common in async I/O.
12
+ Uses EWMA over completion timestamps to estimate items/second rate,
13
+ which handles bursty async I/O patterns better than simple linear regression.
16
14
 
17
15
  Args:
18
- alpha: Smoothing factor (0 < alpha <= 1).
19
- Higher values give more weight to recent samples.
16
+ alpha: Smoothing factor. Higher = more weight on recent samples.
20
17
  Recommended range: 0.1 (smooth) to 0.5 (reactive).
21
18
  """
22
19
 
@@ -33,15 +30,18 @@ class EWMACalculator:
33
30
  if self._last_timestamp is not None and self._last_completed is not None:
34
31
  dt = timestamp - self._last_timestamp
35
32
  dc = completed - self._last_completed
36
- if dt > 0:
33
+ if dt > 0 and dc >= 0:
37
34
  instant_rate = dc / dt
38
35
  if self._ewma_rate is None:
39
36
  self._ewma_rate = instant_rate
40
37
  else:
41
38
  self._ewma_rate = (
42
- self._alpha * instant_rate + (1 - self._alpha) * self._ewma_rate
39
+ self._alpha * instant_rate
40
+ + (1 - self._alpha) * self._ewma_rate
43
41
  )
44
- # If dt == 0, we skip updating the rate (same timestamp)
42
+ elif dt == 0:
43
+ # Same timestamp — rate stays unchanged, don't update
44
+ pass
45
45
  self._last_timestamp = timestamp
46
46
  self._last_completed = completed
47
47
 
@@ -1,37 +1,43 @@
1
1
  """
2
- Terminal rendering for asyncprogress.
3
-
4
- String formatting only — no I/O, fully unit-testable.
2
+ Terminal rendering for progress bars and spinners.
5
3
  """
6
4
 
7
5
  from __future__ import annotations
8
6
 
9
7
  SPINNER_FRAMES: list[str] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
10
8
 
11
- ANSI_COLORS: dict[str, str] = {
12
- "black": "\033[30m",
13
- "red": "\033[31m",
14
- "green": "\033[32m",
15
- "yellow": "\033[33m",
16
- "blue": "\033[34m",
17
- "magenta": "\033[35m",
18
- "cyan": "\033[36m",
19
- "white": "\033[37m",
20
- "bright_black": "\033[90m",
21
- "bright_red": "\033[91m",
22
- "bright_green": "\033[92m",
23
- "bright_yellow": "\033[93m",
24
- "bright_blue": "\033[94m",
25
- "bright_magenta": "\033[95m",
26
- "bright_cyan": "\033[96m",
27
- "bright_white": "\033[97m",
9
+ _ANSI_COLORS: dict[str, str] = {
10
+ "black": "\x1b[30m",
11
+ "red": "\x1b[31m",
12
+ "green": "\x1b[32m",
13
+ "yellow": "\x1b[33m",
14
+ "blue": "\x1b[34m",
15
+ "magenta": "\x1b[35m",
16
+ "cyan": "\x1b[36m",
17
+ "white": "\x1b[37m",
18
+ "bright_black": "\x1b[90m",
19
+ "bright_red": "\x1b[91m",
20
+ "bright_green": "\x1b[92m",
21
+ "bright_yellow": "\x1b[93m",
22
+ "bright_blue": "\x1b[94m",
23
+ "bright_magenta": "\x1b[95m",
24
+ "bright_cyan": "\x1b[96m",
25
+ "bright_white": "\x1b[97m",
28
26
  }
29
- ANSI_RESET = "\033[0m"
27
+ _ANSI_RESET = "\x1b[0m"
30
28
 
31
29
  _SI_PREFIXES = ["", "K", "M", "G", "T", "P"]
32
30
  _BINARY_PREFIXES = ["", "Ki", "Mi", "Gi", "Ti", "Pi"]
33
31
 
34
32
 
33
+ def _apply_color(text: str, color: str) -> str:
34
+ """Apply ANSI color to text."""
35
+ code = _ANSI_COLORS.get(color.lower(), "")
36
+ if not code:
37
+ return text
38
+ return f"{code}{text}{_ANSI_RESET}"
39
+
40
+
35
41
  def _format_duration(seconds: float) -> str:
36
42
  """Format seconds as H:MM:SS or M:SS."""
37
43
  m, s = divmod(int(seconds), 60)
@@ -41,14 +47,6 @@ def _format_duration(seconds: float) -> str:
41
47
  return f"{m}:{s:02d}"
42
48
 
43
49
 
44
- def _apply_color(text: str, color: str) -> str:
45
- """Apply ANSI color to text."""
46
- code = ANSI_COLORS.get(color.lower(), "")
47
- if not code:
48
- return text
49
- return f"{code}{text}{ANSI_RESET}"
50
-
51
-
52
50
  def _scale_value(value: float, divisor: int) -> tuple[float, str]:
53
51
  """Return (scaled_value, prefix_string) for display."""
54
52
  prefixes = _BINARY_PREFIXES if divisor == 1024 else _SI_PREFIXES
@@ -63,13 +61,13 @@ class BarRenderer:
63
61
  """
64
62
  Renders a single-line progress bar string.
65
63
 
66
- No I/O returns strings only. Fully unit-testable.
64
+ No I/O is performed here only string formatting.
67
65
  """
68
66
 
69
67
  def _compute_percentage(self, completed: int, total: int) -> float:
70
68
  """Compute percentage, handling total=0 gracefully."""
71
69
  if total == 0:
72
- return 100.0 # Empty collection is trivially complete
70
+ return 100.0
73
71
  return min(100.0, (completed / total) * 100.0)
74
72
 
75
73
  def _format_count(
@@ -127,11 +125,7 @@ class BarRenderer:
127
125
  unit_scale: bool = False,
128
126
  unit_divisor: int = 1000,
129
127
  ) -> str:
130
- """
131
- Render a single-line deterministic progress bar string.
132
-
133
- Returns a string ready for terminal output (no newline).
134
- """
128
+ """Return a single-line progress bar string ready for terminal output."""
135
129
  parts: list[str] = []
136
130
 
137
131
  if description:
@@ -139,7 +133,7 @@ class BarRenderer:
139
133
 
140
134
  if total is not None:
141
135
  pct = self._compute_percentage(completed, total)
142
- filled = int(bar_width * pct / 100)
136
+ filled = int(bar_width * completed / total) if total > 0 else bar_width
143
137
  bar = fill_char * filled + empty_char * (bar_width - filled)
144
138
  parts.append(f" {bar} ")
145
139
 
@@ -162,9 +156,10 @@ class BarRenderer:
162
156
  if show_eta and eta is not None:
163
157
  parts.append(f"ETA: {_format_duration(eta)}")
164
158
 
165
- rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
166
- if rate_str:
167
- parts.append(rate_str)
159
+ if rate is not None:
160
+ rate_str = self._format_rate(rate, unit, unit_scale, unit_divisor)
161
+ if rate_str:
162
+ parts.append(rate_str)
168
163
 
169
164
  line = " ".join(parts)
170
165
  if color:
@@ -186,11 +181,7 @@ class BarRenderer:
186
181
  unit_scale: bool = False,
187
182
  unit_divisor: int = 1000,
188
183
  ) -> str:
189
- """
190
- Render a single-line spinner for indeterminate progress.
191
-
192
- Activates automatically when total is None.
193
- """
184
+ """Render a single-line spinner for indeterminate progress."""
194
185
  spinner = SPINNER_FRAMES[frame_index % len(SPINNER_FRAMES)]
195
186
  parts: list[str] = []
196
187
  if description:
@@ -15,19 +15,19 @@ def is_tty(file: TextIO = sys.stderr) -> bool:
15
15
 
16
16
  def move_cursor_up(n: int = 1) -> str:
17
17
  """Return ANSI escape sequence to move cursor up n lines."""
18
- return f"\033[{n}A"
18
+ return f"\x1b[{n}A"
19
19
 
20
20
 
21
21
  def erase_line() -> str:
22
22
  """Return ANSI escape sequence to erase the current line."""
23
- return "\033[2K\r"
23
+ return "\x1b[2K\r"
24
24
 
25
25
 
26
26
  def hide_cursor() -> str:
27
27
  """Return ANSI escape sequence to hide the cursor."""
28
- return "\033[?25l"
28
+ return "\x1b[?25l"
29
29
 
30
30
 
31
31
  def show_cursor() -> str:
32
32
  """Return ANSI escape sequence to show the cursor."""
33
- return "\033[?25h"
33
+ return "\x1b[?25h"
File without changes