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.
- {python_utils-4.1.2 → python_utils-4.1.4}/PKG-INFO +1 -1
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_generators.py +69 -5
- {python_utils-4.1.2 → python_utils-4.1.4}/pyproject.toml +2 -2
- {python_utils-4.1.2 → python_utils-4.1.4}/pyproject.toml.orig +2 -2
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/generators.py +15 -2
- python_utils-4.1.2/_python_utils_tests/requirements.txt +0 -1
- {python_utils-4.1.2 → python_utils-4.1.4}/LICENSE +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/README.md +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/__init__.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/clock.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_aio.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_aliases.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_containers.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_converters.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_decorators.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_formatters.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_import.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_import_footprint.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_lazy_imports.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_logged.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_logger.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_python_utils.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_time.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/_python_utils_tests/test_types.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/conftest.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/__about__.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/__init__.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/_aliases.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/aio.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/containers.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/converters.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/decorators.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/exceptions.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/formatters.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/import_.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/logger.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/loguru.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/py.typed +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/terminal.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/time.py +0 -0
- {python_utils-4.1.2 → python_utils-4.1.4}/python_utils/types.py +0 -0
- {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.
|
|
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
|
-
(
|
|
29
|
-
|
|
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,
|
|
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 ==
|
|
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,<
|
|
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.
|
|
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,<
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|