thds.core 1.33.20250407135817__py3-none-any.whl → 1.33.20250408202517__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.
- thds/core/fretry.py +94 -0
- {thds_core-1.33.20250407135817.dist-info → thds_core-1.33.20250408202517.dist-info}/METADATA +1 -1
- {thds_core-1.33.20250407135817.dist-info → thds_core-1.33.20250408202517.dist-info}/RECORD +6 -6
- {thds_core-1.33.20250407135817.dist-info → thds_core-1.33.20250408202517.dist-info}/WHEEL +0 -0
- {thds_core-1.33.20250407135817.dist-info → thds_core-1.33.20250408202517.dist-info}/entry_points.txt +0 -0
- {thds_core-1.33.20250407135817.dist-info → thds_core-1.33.20250408202517.dist-info}/top_level.txt +0 -0
thds/core/fretry.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""A more composable retry decorator."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import random
|
|
4
5
|
import time
|
|
5
6
|
import typing as ty
|
|
@@ -12,7 +13,19 @@ F = ty.TypeVar("F", bound=ty.Callable)
|
|
|
12
13
|
|
|
13
14
|
IsRetryable = ty.Callable[[Exception], bool]
|
|
14
15
|
RetryStrategy = ty.Iterable[IsRetryable]
|
|
16
|
+
AsyncRetryStrategy = ty.AsyncIterable[IsRetryable]
|
|
15
17
|
RetryStrategyFactory = ty.Callable[[], RetryStrategy]
|
|
18
|
+
AsyncRetryStrategyFactory = ty.Callable[[], AsyncRetryStrategy]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def n_times(retries: int) -> ty.Callable[[], ty.Iterator[None]]:
|
|
22
|
+
"""End iteration after yielding 'retries' times."""
|
|
23
|
+
|
|
24
|
+
def n_times_() -> ty.Iterator[None]:
|
|
25
|
+
for _ in range(retries):
|
|
26
|
+
yield None
|
|
27
|
+
|
|
28
|
+
return n_times_
|
|
16
29
|
|
|
17
30
|
|
|
18
31
|
def expo(
|
|
@@ -113,3 +126,84 @@ def is_exc(*exc_types: ty.Type[Exception]) -> IsRetryable:
|
|
|
113
126
|
return isinstance(exc, exc_types)
|
|
114
127
|
|
|
115
128
|
return _is_exc_retryable
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# some async reimplementations of everything :P
|
|
132
|
+
T = ty.TypeVar("T")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def aenumerate(
|
|
136
|
+
asequence: ty.AsyncIterable[T], start: int = 0
|
|
137
|
+
) -> ty.AsyncGenerator[ty.Tuple[int, T], None]:
|
|
138
|
+
# https://stackoverflow.com/questions/48070863/is-python3-6-new-change-async-for-not-compatible-with-enumerate
|
|
139
|
+
"""Asynchronously enumerate an async iterator from a given start value"""
|
|
140
|
+
n = start
|
|
141
|
+
async for elem in asequence:
|
|
142
|
+
yield n, elem
|
|
143
|
+
n += 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def iter_to_async(
|
|
147
|
+
sync_iter_factory: ty.Callable[[], ty.Iterable[T]]
|
|
148
|
+
) -> ty.Callable[[], ty.AsyncIterator[T]]:
|
|
149
|
+
"""Convert a synchronous iterable factory to an async iterator factory."""
|
|
150
|
+
|
|
151
|
+
async def async_wrapper() -> ty.AsyncIterator[T]:
|
|
152
|
+
for item in sync_iter_factory():
|
|
153
|
+
yield item
|
|
154
|
+
|
|
155
|
+
return async_wrapper
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def retry_async(retry_strategy_factory: AsyncRetryStrategyFactory) -> ty.Callable[[F], F]:
|
|
159
|
+
def _retry_decorator(func: F) -> F:
|
|
160
|
+
@wraps(func)
|
|
161
|
+
async def retry_wrapper(*args, **kwargs):
|
|
162
|
+
async for i, is_retryable in aenumerate(retry_strategy_factory(), start=1):
|
|
163
|
+
try:
|
|
164
|
+
return await func(*args, **kwargs)
|
|
165
|
+
except Exception as ex:
|
|
166
|
+
if not is_retryable(ex):
|
|
167
|
+
raise ex
|
|
168
|
+
getLogger(__name__).info("Retry #%d for %s due to exception %s", i, func, ex)
|
|
169
|
+
# one final retry that, if it fails, will not get caught and retried.
|
|
170
|
+
return await func(*args, **kwargs)
|
|
171
|
+
|
|
172
|
+
return ty.cast(F, retry_wrapper)
|
|
173
|
+
|
|
174
|
+
return _retry_decorator
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def retry_regular_async(
|
|
178
|
+
is_retryable: IsRetryable,
|
|
179
|
+
intervals_factory: ty.Callable[[], ty.AsyncIterable[ty.Any]],
|
|
180
|
+
) -> ty.Callable[[F], F]:
|
|
181
|
+
async def _retry_internal() -> ty.AsyncIterable[IsRetryable]:
|
|
182
|
+
async for _ in intervals_factory():
|
|
183
|
+
yield is_retryable
|
|
184
|
+
|
|
185
|
+
return retry_async(_retry_internal)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def sleep_async(
|
|
189
|
+
mk_seconds_iter: ty.Callable[[], ty.Iterable[float]],
|
|
190
|
+
sleeper: ty.Callable[[float], ty.Awaitable[ty.Any]] = asyncio.sleep,
|
|
191
|
+
) -> ty.Callable[[], ty.AsyncGenerator[str, None]]:
|
|
192
|
+
async def sleep_() -> ty.AsyncGenerator[str, None]:
|
|
193
|
+
start = default_timer()
|
|
194
|
+
|
|
195
|
+
so_far = 0.0
|
|
196
|
+
for i, secs in enumerate(mk_seconds_iter(), start=1):
|
|
197
|
+
yield f"attempt {i} after {so_far:.2f}s"
|
|
198
|
+
so_far = default_timer() - start
|
|
199
|
+
await sleeper(secs)
|
|
200
|
+
|
|
201
|
+
return sleep_
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def retry_sleep_async(
|
|
205
|
+
is_retryable: IsRetryable,
|
|
206
|
+
seconds_iter: ty.Callable[[], ty.Iterable[float]],
|
|
207
|
+
) -> ty.Callable[[F], F]:
|
|
208
|
+
"""E.g. retry_sleep(expo(retries=5)) to get max 6 calls to the function."""
|
|
209
|
+
return retry_regular_async(is_retryable, sleep_async(seconds_iter))
|
|
@@ -11,7 +11,7 @@ thds/core/dict_utils.py,sha256=MatsjZC9lchfdaDqNAzL2mkTZytDnCAqg56sMm71wbE,6364
|
|
|
11
11
|
thds/core/env.py,sha256=HkuyFmGpCgdQUB1r2GbpCqB3cs1lCsvp47Ghk1DHBo8,1083
|
|
12
12
|
thds/core/exit_after.py,sha256=0lz63nz2NTiIdyBDYyRa9bQShxQKe7eISy8VhXeW4HU,3485
|
|
13
13
|
thds/core/files.py,sha256=NJlPXj7BejKd_Pa06MOywVv_YapT4bVedfsJHrWX8nI,4579
|
|
14
|
-
thds/core/fretry.py,sha256=
|
|
14
|
+
thds/core/fretry.py,sha256=PKgOxCMjcF4zsFfXFvPXpomv5J6KU6llB1EaKukugig,6942
|
|
15
15
|
thds/core/generators.py,sha256=rcdFpPj0NMJWSaSZTnBfTeZxTTORNB633Lng-BW1284,1939
|
|
16
16
|
thds/core/git.py,sha256=I6kaEvwcvVxCLYHhTTfnHle-GkmgOR9_fHs03QxgBfI,2792
|
|
17
17
|
thds/core/hash_cache.py,sha256=bkdV8HZOl66zNffnIjRvMQH1A0_xNqowMDLBtRXGFl8,3723
|
|
@@ -67,8 +67,8 @@ thds/core/sqlite/structured.py,sha256=SvZ67KcVcVdmpR52JSd52vMTW2ALUXmlHEeD-VrzWV
|
|
|
67
67
|
thds/core/sqlite/types.py,sha256=oUkfoKRYNGDPZRk29s09rc9ha3SCk2SKr_K6WKebBFs,1308
|
|
68
68
|
thds/core/sqlite/upsert.py,sha256=BmKK6fsGVedt43iY-Lp7dnAu8aJ1e9CYlPVEQR2pMj4,5827
|
|
69
69
|
thds/core/sqlite/write.py,sha256=z0219vDkQDCnsV0WLvsj94keItr7H4j7Y_evbcoBrWU,3458
|
|
70
|
-
thds_core-1.33.
|
|
71
|
-
thds_core-1.33.
|
|
72
|
-
thds_core-1.33.
|
|
73
|
-
thds_core-1.33.
|
|
74
|
-
thds_core-1.33.
|
|
70
|
+
thds_core-1.33.20250408202517.dist-info/METADATA,sha256=M6l71hXzPYsYnaC5Wc6LznI2koXmUR26TvQ4kRaLbdQ,2277
|
|
71
|
+
thds_core-1.33.20250408202517.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
72
|
+
thds_core-1.33.20250408202517.dist-info/entry_points.txt,sha256=bOCOVhKZv7azF3FvaWX6uxE6yrjK6FcjqhtxXvLiFY8,161
|
|
73
|
+
thds_core-1.33.20250408202517.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
|
|
74
|
+
thds_core-1.33.20250408202517.dist-info/RECORD,,
|
|
File without changes
|
{thds_core-1.33.20250407135817.dist-info → thds_core-1.33.20250408202517.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{thds_core-1.33.20250407135817.dist-info → thds_core-1.33.20250408202517.dist-info}/top_level.txt
RENAMED
|
File without changes
|