python-utils 4.1.2__tar.gz → 4.1.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.
Files changed (42) hide show
  1. {python_utils-4.1.2 → python_utils-4.1.4}/PKG-INFO +1 -1
  2. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_generators.py +69 -5
  3. {python_utils-4.1.2 → python_utils-4.1.4}/pyproject.toml +2 -2
  4. {python_utils-4.1.2 → python_utils-4.1.4}/pyproject.toml.orig +2 -2
  5. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/generators.py +15 -2
  6. python_utils-4.1.2/_python_utils_tests/requirements.txt +0 -1
  7. {python_utils-4.1.2 → python_utils-4.1.4}/LICENSE +0 -0
  8. {python_utils-4.1.2 → python_utils-4.1.4}/README.md +0 -0
  9. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/__init__.py +0 -0
  10. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/clock.py +0 -0
  11. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_aio.py +0 -0
  12. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_aliases.py +0 -0
  13. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_containers.py +0 -0
  14. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_converters.py +0 -0
  15. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_decorators.py +0 -0
  16. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_formatters.py +0 -0
  17. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_import.py +0 -0
  18. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_import_footprint.py +0 -0
  19. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_lazy_imports.py +0 -0
  20. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_logged.py +0 -0
  21. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_logger.py +0 -0
  22. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_python_utils.py +0 -0
  23. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_time.py +0 -0
  24. {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_types.py +0 -0
  25. {python_utils-4.1.2 → python_utils-4.1.4}/conftest.py +0 -0
  26. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/__about__.py +0 -0
  27. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/__init__.py +0 -0
  28. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/_aliases.py +0 -0
  29. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/aio.py +0 -0
  30. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/containers.py +0 -0
  31. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/converters.py +0 -0
  32. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/decorators.py +0 -0
  33. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/exceptions.py +0 -0
  34. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/formatters.py +0 -0
  35. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/import_.py +0 -0
  36. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/logger.py +0 -0
  37. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/loguru.py +0 -0
  38. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/py.typed +0 -0
  39. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/terminal.py +0 -0
  40. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/time.py +0 -0
  41. {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/types.py +0 -0
  42. {python_utils-4.1.2 → python_utils-4.1.4}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-utils
3
- Version: 4.1.2
3
+ Version: 4.1.4
4
4
  Summary: Python Utils is a module with some convenient utilities not included with the standard Python install
5
5
  Keywords: utils,utilities,helpers,typing,typed,async,lazy-imports
6
6
  Author: Rick van Hattem
@@ -1,6 +1,7 @@
1
1
  """Tests for the batching helpers in ``python_utils.generators``."""
2
2
 
3
3
  import asyncio
4
+ import datetime
4
5
  import gc
5
6
  import sys
6
7
  from types import SimpleNamespace
@@ -23,14 +24,22 @@ async def test_abatcher() -> None:
23
24
 
24
25
  @pytest.mark.asyncio
25
26
  @pytest.mark.parametrize(
26
- 'arrival_times',
27
+ ('arrival_times', 'expected'),
27
28
  [
28
- (0, 8, 16, 24, 32, 40, 48, 56, 64, 72),
29
- (0, 10, 11, 21, 22, 32, 33, 43, 44, 54),
29
+ (
30
+ (0, 8, 16, 24, 32, 40, 48, 56, 64, 72),
31
+ [[0, 1, 2], [3, 4], [5, 6], [7, 8], [9]],
32
+ ),
33
+ (
34
+ (0, 10, 11, 21, 22, 32, 33, 43, 44, 54),
35
+ [[0, 1, 2], [3, 4], [5, 6], [7, 8], [9]],
36
+ ),
30
37
  ],
31
38
  )
32
39
  async def test_abatcher_timed(
33
- monkeypatch: pytest.MonkeyPatch, arrival_times: tuple[int, ...]
40
+ monkeypatch: pytest.MonkeyPatch,
41
+ arrival_times: tuple[int, ...],
42
+ expected: list[list[int]],
34
43
  ) -> None:
35
44
  """Group async items into batches by time interval."""
36
45
  now: float = 0.0
@@ -52,7 +61,7 @@ async def test_abatcher_timed(
52
61
  async for batch in python_utils.abatcher(generator(), interval=10):
53
62
  batches.append(batch)
54
63
 
55
- assert batches == [[0, 1, 2], [3, 4], [5, 6], [7, 8], [9]]
64
+ assert batches == expected
56
65
  assert len(batches) == 5
57
66
 
58
67
 
@@ -89,6 +98,61 @@ async def test_abatcher_timed_with_timeout() -> None:
89
98
  await batcher.__anext__()
90
99
 
91
100
 
101
+ @pytest.mark.asyncio
102
+ @pytest.mark.parametrize('batch_size', [None, 3])
103
+ @pytest.mark.parametrize('interval', [10, datetime.timedelta(seconds=10)])
104
+ @pytest.mark.parametrize('arrival', [8.0, 10.0])
105
+ async def test_abatcher_partial_batch_keeps_deadline(
106
+ monkeypatch: pytest.MonkeyPatch,
107
+ batch_size: int | None,
108
+ interval: int | datetime.timedelta,
109
+ arrival: float,
110
+ ) -> None:
111
+ """Flush a partial batch at its deadline, including an exact arrival."""
112
+ now: float = 0.0
113
+ monkeypatch.setattr(
114
+ python_utils.generators,
115
+ 'time',
116
+ SimpleNamespace(perf_counter=lambda: now),
117
+ )
118
+ original_wait = asyncio.wait
119
+
120
+ async def wait(
121
+ pending: set[asyncio.Future[int]],
122
+ *,
123
+ timeout: float,
124
+ return_when: str,
125
+ ) -> tuple[set[asyncio.Future[int]], set[asyncio.Future[int]]]:
126
+ """Run ready items, or let the requested timeout elapse."""
127
+ nonlocal now
128
+ done, remaining = await original_wait(
129
+ pending, timeout=0, return_when=return_when
130
+ )
131
+ if not done:
132
+ now += timeout
133
+ return done, remaining
134
+
135
+ monkeypatch.setattr(asyncio, 'wait', wait)
136
+
137
+ async def generator() -> types.AsyncGenerator[int, None]:
138
+ """Yield twice on the fake clock, then leave one item pending."""
139
+ nonlocal now
140
+ yield 0
141
+ now = arrival
142
+ yield 1
143
+ await asyncio.Event().wait()
144
+
145
+ batcher = python_utils.abatcher(
146
+ generator(), batch_size=batch_size, interval=interval
147
+ )
148
+ try:
149
+ first = await batcher.__anext__()
150
+ assert first == [0, 1]
151
+ assert now == 10.0
152
+ finally:
153
+ await batcher.aclose()
154
+
155
+
92
156
  async def blocked_generator(
93
157
  started: asyncio.Event, closed: types.List[str]
94
158
  ) -> types.AsyncIterator[int]:
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.11,<0.13"]
2
+ requires = ["uv_build>=0.11,<1"]
3
3
  build-backend = "uv_build"
4
4
 
5
5
  [tool.mypy]
@@ -125,7 +125,7 @@ source-include = [
125
125
 
126
126
  [project]
127
127
  name = "python-utils"
128
- version = "4.1.2"
128
+ version = "4.1.4"
129
129
  description = "Python Utils is a module with some convenient utilities not included with the standard Python install"
130
130
  readme = "README.md"
131
131
  requires-python = ">=3.10"
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ['uv_build>=0.11,<0.13']
2
+ requires = ['uv_build>=0.11,<1']
3
3
  build-backend = 'uv_build'
4
4
 
5
5
  [tool.uv.build-backend]
@@ -12,7 +12,7 @@ source-include = ['_python_utils_tests/**/*.py', 'conftest.py', 'tox.ini']
12
12
 
13
13
  [project]
14
14
  name = 'python-utils'
15
- version = "4.1.2" # static; the single source of truth. Bump via `uv version`.
15
+ version = "4.1.4" # static; the single source of truth. Bump via `uv version`.
16
16
  description = 'Python Utils is a module with some convenient utilities not included with the standard Python install'
17
17
  readme = 'README.md'
18
18
  requires-python = '>=3.10'
@@ -72,6 +72,9 @@ async def abatcher(
72
72
  request for it. A source that is an async generator ends at that point.
73
73
  A source with nothing on its way stays usable.
74
74
 
75
+ Items arriving within an interval share its deadline. Waiting for the
76
+ next item does not restart the interval of a nonempty batch.
77
+
75
78
  Args:
76
79
  generator: The async generator or iterator to batch.
77
80
  batch_size (typing.Optional[int], optional): The number of items per
@@ -110,9 +113,13 @@ async def abatcher(
110
113
  # `create_task` insists on a coroutine.
111
114
  pending = {asyncio.ensure_future(generator.__anext__())}
112
115
 
116
+ timeout = interval_s
117
+ if interval and batch:
118
+ timeout = max(0.0, next_yield - time.perf_counter())
119
+
113
120
  done, pending = await asyncio.wait(
114
121
  pending,
115
- timeout=interval_s,
122
+ timeout=timeout,
116
123
  return_when=asyncio.FIRST_COMPLETED,
117
124
  )
118
125
 
@@ -132,7 +139,13 @@ async def abatcher(
132
139
  # next item is flushed on its own once the old one ran out.
133
140
  next_yield = time.perf_counter() + interval_s
134
141
 
135
- if interval and batch and time.perf_counter() > next_yield:
142
+ # A wait that ends without an item ran out of the deadline, so the
143
+ # batch is due. An item that arrives at the deadline joins it.
144
+ if (
145
+ interval
146
+ and batch
147
+ and (not done or time.perf_counter() > next_yield)
148
+ ):
136
149
  yield batch
137
150
  batch = []
138
151
  # Always set the next yield time to the current time. If the
@@ -1 +0,0 @@
1
- -e .[tests]
File without changes
File without changes
File without changes
File without changes