|
3 | 3 | import asyncio |
4 | 4 | import contextlib |
5 | 5 | import logging |
6 | | -from typing import TYPE_CHECKING, Any, ClassVar, Self |
| 6 | +from collections.abc import Sequence |
| 7 | +from functools import wraps |
| 8 | +from typing import TYPE_CHECKING, Any, ClassVar, ParamSpec, Self, TypeVar |
7 | 9 |
|
8 | 10 | import aiohttp |
9 | | -import tenacity |
10 | 11 | import yarl |
11 | 12 |
|
12 | 13 | from mega.crypto import generate_hashcash_token |
|
15 | 16 | from .errors import RequestError, RetryRequestError |
16 | 17 |
|
17 | 18 | if TYPE_CHECKING: |
18 | | - from collections.abc import AsyncGenerator |
| 19 | + from collections.abc import AsyncGenerator, Callable, Coroutine |
| 20 | + |
| 21 | + _P = ParamSpec("_P") |
| 22 | + _R = TypeVar("_R") |
19 | 23 |
|
20 | 24 |
|
21 | 25 | logger = logging.getLogger(__name__) |
|
27 | 31 | } |
28 | 32 |
|
29 | 33 |
|
| 34 | +def retry( |
| 35 | + *, |
| 36 | + exceptions: Sequence[type[Exception]] | type[Exception], |
| 37 | + attempts: int = 10, |
| 38 | + delay: float = 0.5, |
| 39 | + min_delay: float = 2.0, |
| 40 | + max_delay: float = 30.0, |
| 41 | + backoff: int = 2, |
| 42 | +) -> Callable[[Callable[_P, _R]], Callable[_P, Coroutine[None, None, _R]]]: |
| 43 | + if not isinstance(exceptions, Sequence): |
| 44 | + exceptions = [exceptions] |
| 45 | + |
| 46 | + def wrapper(func: Callable[_P, _R]) -> Callable[_P, Coroutine[None, None, _R]]: |
| 47 | + @wraps(func) |
| 48 | + async def inner_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: |
| 49 | + current_delay = delay |
| 50 | + for attempt in range(1, attempts + 1): |
| 51 | + try: |
| 52 | + return func(*args, **kwargs) |
| 53 | + except tuple(exceptions) as exc: |
| 54 | + if attempt >= attempts: |
| 55 | + raise |
| 56 | + |
| 57 | + logger.warning(f"Retrying {func.__qualname__} after attempt {attempt} ({exc})") |
| 58 | + await asyncio.sleep(current_delay) |
| 59 | + exp = current_delay**backoff |
| 60 | + current_delay = max(min_delay, min(exp, max_delay)) |
| 61 | + else: |
| 62 | + raise RuntimeError |
| 63 | + |
| 64 | + return inner_wrapper |
| 65 | + |
| 66 | + return wrapper |
| 67 | + |
| 68 | + |
30 | 69 | class MegaAPI: |
31 | 70 | __slots__ = ( |
32 | 71 | "__session", |
@@ -63,10 +102,7 @@ def _lazy_session(self) -> aiohttp.ClientSession: |
63 | 102 | self.__session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(sock_connect=160, sock_read=60)) |
64 | 103 | return self.__session |
65 | 104 |
|
66 | | - @tenacity.retry( |
67 | | - retry=tenacity.retry_if_exception_type(RetryRequestError), |
68 | | - wait=tenacity.wait_exponential(multiplier=2, min=2, max=60), |
69 | | - ) |
| 105 | + @retry(exceptions=RetryRequestError, attempts=10, max_delay=60.0) |
70 | 106 | async def request(self, data: dict[str, Any] | list[dict[str, Any]], params: dict[str, Any] | None = None) -> Any: |
71 | 107 | params = {"id": self._request_id} | (params or {}) |
72 | 108 | self._request_id += 1 |
|
0 commit comments