aiomisc 17.5.10__py3-none-any.whl → 17.5.15__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.
- aiomisc/aggregate.py +111 -54
- aiomisc/version.py +2 -2
- {aiomisc-17.5.10.dist-info → aiomisc-17.5.15.dist-info}/METADATA +1 -1
- {aiomisc-17.5.10.dist-info → aiomisc-17.5.15.dist-info}/RECORD +7 -7
- {aiomisc-17.5.10.dist-info → aiomisc-17.5.15.dist-info}/COPYING +0 -0
- {aiomisc-17.5.10.dist-info → aiomisc-17.5.15.dist-info}/WHEEL +0 -0
- {aiomisc-17.5.10.dist-info → aiomisc-17.5.15.dist-info}/entry_points.txt +0 -0
aiomisc/aggregate.py
CHANGED
@@ -1,10 +1,21 @@
|
|
1
1
|
import asyncio
|
2
|
+
import functools
|
2
3
|
import inspect
|
3
4
|
import logging
|
4
5
|
from asyncio import CancelledError, Event, Future, Lock, wait_for
|
5
6
|
from dataclasses import dataclass
|
6
7
|
from inspect import Parameter
|
7
|
-
from typing import
|
8
|
+
from typing import (
|
9
|
+
Any,
|
10
|
+
Awaitable,
|
11
|
+
Callable,
|
12
|
+
Generic,
|
13
|
+
Iterable,
|
14
|
+
List,
|
15
|
+
Optional,
|
16
|
+
Protocol,
|
17
|
+
TypeVar,
|
18
|
+
)
|
8
19
|
|
9
20
|
from .compat import EventLoopMixin
|
10
21
|
from .counters import Statistic
|
@@ -13,19 +24,25 @@ from .counters import Statistic
|
|
13
24
|
log = logging.getLogger(__name__)
|
14
25
|
|
15
26
|
|
27
|
+
V = TypeVar("V")
|
28
|
+
R = TypeVar("R")
|
29
|
+
|
30
|
+
|
16
31
|
@dataclass(frozen=True)
|
17
|
-
class Arg:
|
18
|
-
value:
|
19
|
-
future: Future
|
32
|
+
class Arg(Generic[V, R]):
|
33
|
+
value: V
|
34
|
+
future: "Future[R]"
|
20
35
|
|
21
36
|
|
22
37
|
class ResultNotSetError(Exception):
|
23
38
|
pass
|
24
39
|
|
25
40
|
|
26
|
-
|
27
|
-
|
28
|
-
|
41
|
+
class AggregateAsyncFunc(Protocol, Generic[V, R]):
|
42
|
+
__name__: str
|
43
|
+
|
44
|
+
async def __call__(self, *args: Arg[V, R]) -> None:
|
45
|
+
...
|
29
46
|
|
30
47
|
|
31
48
|
class AggregateStatistic(Statistic):
|
@@ -36,27 +53,30 @@ class AggregateStatistic(Statistic):
|
|
36
53
|
done: int
|
37
54
|
|
38
55
|
|
39
|
-
|
56
|
+
def _has_variadic_positional(func: Callable[..., Any]) -> bool:
|
57
|
+
return any(
|
58
|
+
parameter.kind == Parameter.VAR_POSITIONAL
|
59
|
+
for parameter in inspect.signature(func).parameters.values()
|
60
|
+
)
|
40
61
|
|
41
|
-
|
62
|
+
|
63
|
+
class AggregatorAsync(EventLoopMixin, Generic[V, R]):
|
64
|
+
|
65
|
+
_func: AggregateAsyncFunc[V, R]
|
42
66
|
_max_count: Optional[int]
|
43
67
|
_leeway: float
|
44
68
|
_first_call_at: Optional[float]
|
45
69
|
_args: list
|
46
|
-
_futures: List[Future]
|
70
|
+
_futures: "List[Future[R]]"
|
47
71
|
_event: Event
|
48
72
|
_lock: Lock
|
49
73
|
|
50
74
|
def __init__(
|
51
|
-
self, func:
|
75
|
+
self, func: AggregateAsyncFunc[V, R], *, leeway_ms: float,
|
52
76
|
max_count: Optional[int] = None,
|
53
77
|
statistic_name: Optional[str] = None,
|
54
78
|
):
|
55
|
-
|
56
|
-
parameter.kind == Parameter.VAR_POSITIONAL
|
57
|
-
for parameter in inspect.signature(func).parameters.values()
|
58
|
-
)
|
59
|
-
if not has_variadic_positional:
|
79
|
+
if not _has_variadic_positional(func):
|
60
80
|
raise ValueError(
|
61
81
|
"Function must accept variadic positional arguments",
|
62
82
|
)
|
@@ -94,9 +114,18 @@ class Aggregator(EventLoopMixin):
|
|
94
114
|
def count(self) -> int:
|
95
115
|
return len(self._args)
|
96
116
|
|
97
|
-
async def _execute(
|
117
|
+
async def _execute(
|
118
|
+
self,
|
119
|
+
*,
|
120
|
+
args: List[V],
|
121
|
+
futures: "List[Future[R]]",
|
122
|
+
) -> None:
|
123
|
+
args_ = [
|
124
|
+
Arg(value=arg, future=future)
|
125
|
+
for arg, future in zip(args, futures)
|
126
|
+
]
|
98
127
|
try:
|
99
|
-
|
128
|
+
await self._func(*args_)
|
100
129
|
self._statistic.success += 1
|
101
130
|
except CancelledError:
|
102
131
|
# Other waiting tasks can try to finish the job instead.
|
@@ -108,31 +137,29 @@ class Aggregator(EventLoopMixin):
|
|
108
137
|
finally:
|
109
138
|
self._statistic.done += 1
|
110
139
|
|
111
|
-
|
112
|
-
|
113
|
-
def _set_results(self, results: Iterable, futures: List[Future]) -> None:
|
114
|
-
for future, result in zip(futures, results):
|
140
|
+
# Validate that all results/exceptions are set by the func
|
141
|
+
for future in futures:
|
115
142
|
if not future.done():
|
116
|
-
future.
|
143
|
+
future.set_exception(ResultNotSetError)
|
117
144
|
|
118
145
|
def _set_exception(
|
119
|
-
self, exc: Exception, futures: List[Future],
|
146
|
+
self, exc: Exception, futures: List["Future[R]"],
|
120
147
|
) -> None:
|
121
148
|
for future in futures:
|
122
149
|
if not future.done():
|
123
150
|
future.set_exception(exc)
|
124
151
|
|
125
|
-
async def aggregate(self, arg:
|
152
|
+
async def aggregate(self, arg: V) -> R:
|
126
153
|
if self._first_call_at is None:
|
127
154
|
self._first_call_at = self.loop.time()
|
128
155
|
first_call_at = self._first_call_at
|
129
156
|
|
130
157
|
args: list = self._args
|
131
|
-
futures: List[Future] = self._futures
|
158
|
+
futures: "List[Future[R]]" = self._futures
|
132
159
|
event: Event = self._event
|
133
160
|
lock: Lock = self._lock
|
134
161
|
args.append(arg)
|
135
|
-
future: Future = Future()
|
162
|
+
future: "Future[R]" = Future()
|
136
163
|
futures.append(future)
|
137
164
|
|
138
165
|
if self.count == self.max_count:
|
@@ -165,33 +192,61 @@ class Aggregator(EventLoopMixin):
|
|
165
192
|
return future.result()
|
166
193
|
|
167
194
|
|
168
|
-
|
195
|
+
S = TypeVar("S", contravariant=True)
|
196
|
+
T = TypeVar("T", covariant=True)
|
169
197
|
|
170
|
-
async def _execute(self, *, args: list, futures: List[Future]) -> None:
|
171
|
-
args = [
|
172
|
-
Arg(value=arg, future=future)
|
173
|
-
for arg, future in zip(args, futures)
|
174
|
-
]
|
175
|
-
try:
|
176
|
-
await self._func(*args)
|
177
|
-
self._statistic.success += 1
|
178
|
-
except CancelledError:
|
179
|
-
# Other waiting tasks can try to finish the job instead.
|
180
|
-
raise
|
181
|
-
except Exception as e:
|
182
|
-
self._set_exception(e, futures)
|
183
|
-
self._statistic.error += 1
|
184
|
-
return
|
185
|
-
finally:
|
186
|
-
self._statistic.done += 1
|
187
198
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
199
|
+
class AggregateFunc(Protocol, Generic[S, T]):
|
200
|
+
__name__: str
|
201
|
+
|
202
|
+
async def __call__(self, *args: S) -> Iterable[T]:
|
203
|
+
...
|
204
|
+
|
205
|
+
|
206
|
+
def _to_async_aggregate(func: AggregateFunc[V, R]) -> AggregateAsyncFunc[V, R]:
|
207
|
+
@functools.wraps(
|
208
|
+
func,
|
209
|
+
assigned=tuple(
|
210
|
+
item
|
211
|
+
for item in functools.WRAPPER_ASSIGNMENTS
|
212
|
+
if item != "__annotations__"
|
213
|
+
),
|
214
|
+
)
|
215
|
+
async def wrapper(*args: Arg[V, R]) -> None:
|
216
|
+
args_ = [item.value for item in args]
|
217
|
+
results = await func(*args_)
|
218
|
+
for res, arg in zip(results, args):
|
219
|
+
if not arg.future.done():
|
220
|
+
arg.future.set_result(res)
|
221
|
+
|
222
|
+
return wrapper
|
223
|
+
|
224
|
+
|
225
|
+
class Aggregator(AggregatorAsync[V, R], Generic[V, R]):
|
226
|
+
def __init__(
|
227
|
+
self,
|
228
|
+
func: AggregateFunc[V, R],
|
229
|
+
*,
|
230
|
+
leeway_ms: float,
|
231
|
+
max_count: Optional[int] = None,
|
232
|
+
statistic_name: Optional[str] = None,
|
233
|
+
) -> None:
|
234
|
+
if not _has_variadic_positional(func):
|
235
|
+
raise ValueError(
|
236
|
+
"Function must accept variadic positional arguments",
|
237
|
+
)
|
238
|
+
|
239
|
+
super().__init__(
|
240
|
+
_to_async_aggregate(func),
|
241
|
+
leeway_ms=leeway_ms,
|
242
|
+
max_count=max_count,
|
243
|
+
statistic_name=statistic_name,
|
244
|
+
)
|
192
245
|
|
193
246
|
|
194
|
-
def aggregate(
|
247
|
+
def aggregate(
|
248
|
+
leeway_ms: float, max_count: Optional[int] = None
|
249
|
+
) -> Callable[[AggregateFunc[V, R]], Callable[[V], Awaitable[R]]]:
|
195
250
|
"""
|
196
251
|
Parametric decorator that aggregates multiple
|
197
252
|
(but no more than ``max_count`` defaulting to ``None``) single-argument
|
@@ -220,7 +275,7 @@ def aggregate(leeway_ms: float, max_count: Optional[int] = None) -> Callable:
|
|
220
275
|
|
221
276
|
:return:
|
222
277
|
"""
|
223
|
-
def decorator(func:
|
278
|
+
def decorator(func: AggregateFunc[V, R]) -> Callable[[V], Awaitable[R]]:
|
224
279
|
aggregator = Aggregator(
|
225
280
|
func, max_count=max_count, leeway_ms=leeway_ms,
|
226
281
|
)
|
@@ -229,8 +284,8 @@ def aggregate(leeway_ms: float, max_count: Optional[int] = None) -> Callable:
|
|
229
284
|
|
230
285
|
|
231
286
|
def aggregate_async(
|
232
|
-
|
233
|
-
) -> Callable:
|
287
|
+
leeway_ms: float, max_count: Optional[int] = None,
|
288
|
+
) -> Callable[[AggregateAsyncFunc[V, R]], Callable[[V], Awaitable[R]]]:
|
234
289
|
"""
|
235
290
|
Same as ``aggregate``, but with ``func`` arguments of type ``Arg``
|
236
291
|
containing ``value`` and ``future`` attributes instead. In this setting
|
@@ -241,7 +296,9 @@ def aggregate_async(
|
|
241
296
|
|
242
297
|
:return:
|
243
298
|
"""
|
244
|
-
def decorator(
|
299
|
+
def decorator(
|
300
|
+
func: AggregateAsyncFunc[V, R]
|
301
|
+
) -> Callable[[V], Awaitable[R]]:
|
245
302
|
aggregator = AggregatorAsync(
|
246
303
|
func, max_count=max_count, leeway_ms=leeway_ms,
|
247
304
|
)
|
aiomisc/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
aiomisc/__init__.py,sha256=mgLaoGB3-WTAnrUfEN9nM_0Z_XU6xojkx1ISmW204UA,2253
|
2
2
|
aiomisc/_context_vars.py,sha256=28A7j_NABitKMtpxuFvxQ2wCr-Fq79dAC3YjqVLLeTQ,689
|
3
|
-
aiomisc/aggregate.py,sha256=
|
3
|
+
aiomisc/aggregate.py,sha256=HU_kVLzA-BdySOH3UIfTEuB2Ty1uUC200hZf4upI504,8834
|
4
4
|
aiomisc/backoff.py,sha256=BgdT_MEByFJPyEHjfghC7WhnEen2Lpf4p2VLZV_KVi0,5605
|
5
5
|
aiomisc/circuit_breaker.py,sha256=nZVLuGg4rKxn84CHaIvRfBYumgdwx2VZloF8OprQKuk,12581
|
6
6
|
aiomisc/compat.py,sha256=aYVe0J-2CvAzUHPAULNhrfMLFYNKN-z3Sg7nlBkzfxw,3291
|
@@ -39,7 +39,7 @@ aiomisc/signal.py,sha256=_iiC2jukXg7-LLirIl1YATlKIIsKLbmTNFr1Ezheu7g,1728
|
|
39
39
|
aiomisc/thread_pool.py,sha256=Wx0LskSv1dGWoen1lEFRacjVco62hHn6oDaM_XKH6uo,14035
|
40
40
|
aiomisc/timeout.py,sha256=rTipPBz0GOqDZG2euVxU_6PjI63nFD_bDsy_DOHHBDQ,863
|
41
41
|
aiomisc/utils.py,sha256=6yTfTpeRCVzfZp-MJCmB1oayOHUBVwQwzC3U5PBAjn8,11919
|
42
|
-
aiomisc/version.py,sha256=
|
42
|
+
aiomisc/version.py,sha256=3CjPNeI8EmqIvEF59PTZGYfu6jqFp-DP1QFO8gJ6w_s,156
|
43
43
|
aiomisc/worker_pool.py,sha256=GA91KdOrBlqHthbVSTxu_d6BsBIbl-uKqW2NxqSafG0,11107
|
44
44
|
aiomisc_log/__init__.py,sha256=ZD-Q-YTWoZdxJpMqMNMIraA_gaN0QawgnVpXz6mxd2o,4855
|
45
45
|
aiomisc_log/enum.py,sha256=_zfCZPYCGyI9KL6TqHiYVlOfA5U5MCbsuCuDKxDHdxg,1549
|
@@ -57,8 +57,8 @@ aiomisc_worker/process_inner.py,sha256=8ZtjCSLrgySW57OIbuGrpEWxfysRLYKx1or1YaAqx
|
|
57
57
|
aiomisc_worker/protocol.py,sha256=1smmlBbdreSmnrxuhHaUMUC10FO9xMIEcedhweQJX_A,2705
|
58
58
|
aiomisc_worker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
59
59
|
aiomisc_worker/worker.py,sha256=f8nCFhlKh84UUBUaEgCllwMRvVZiD8_UUXaeit6g3T8,3236
|
60
|
-
aiomisc-17.5.
|
61
|
-
aiomisc-17.5.
|
62
|
-
aiomisc-17.5.
|
63
|
-
aiomisc-17.5.
|
64
|
-
aiomisc-17.5.
|
60
|
+
aiomisc-17.5.15.dist-info/COPYING,sha256=Ky_8CQMaIixfyOreUBsl0hKN6A5fLnPF8KPQ9molMYA,1125
|
61
|
+
aiomisc-17.5.15.dist-info/METADATA,sha256=74u26dLlBO_yNtGagNvQTMN-ZT2KNMl-rUhV0aUJIfY,15664
|
62
|
+
aiomisc-17.5.15.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
63
|
+
aiomisc-17.5.15.dist-info/entry_points.txt,sha256=KRsSPCwKJyGTWrvzpwbS0yIDwzsgDA2X6f0CBWYmNao,55
|
64
|
+
aiomisc-17.5.15.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|