Coverage for src/extratools_limit/rate_limit.py: 0%
44 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-22 18:39 -0700
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-22 18:39 -0700
1import asyncio
2import functools
3import random
4import time
5from datetime import timedelta
6from pathlib import Path
7from typing import Any
9from extratools_core.typing import PathLike
12class Wait:
13 def __init__(
14 self,
15 lockfile: PathLike | str,
16 *,
17 min_gap: timedelta | float = timedelta(seconds=0),
18 randomness: timedelta | float = timedelta(milliseconds=1),
19 use_async: bool = False,
20 ) -> None:
21 if isinstance(lockfile, str):
22 lockfile = Path(lockfile)
23 if isinstance(min_gap, timedelta):
24 min_gap = min_gap.seconds
25 if isinstance(randomness, timedelta):
26 randomness = randomness.seconds
28 self.__lockfile: PathLike = lockfile
29 self.__min_gap: float = min_gap
30 self.__randomness: float = randomness
31 self.__use_async: bool = use_async
33 def __call__(self, func): # noqa: ANN001, ANN204
34 @functools.wraps(func)
35 def wrapper(*args: Any, **kwargs: Any) -> Any:
36 if not self.__lockfile.is_file():
37 self.__lockfile.touch()
39 while True:
40 gap: float = time.time() - self.__lockfile.stat().st_mtime
41 if (remaining_gap := self.__min_gap - gap) > 0:
42 time.sleep(remaining_gap + random.random() * self.__randomness)
43 continue
45 # Note that since we are not actually locking the file,
46 # there is rare chance that multiple threads can run at the same time.
47 self.__lockfile.touch()
48 return func(*args, **kwargs)
50 @functools.wraps(func)
51 async def wrapper_async(*args: Any, **kwargs: Any) -> Any:
52 if not self.__lockfile.is_file():
53 self.__lockfile.touch()
55 while True:
56 gap: float = time.time() - self.__lockfile.stat().st_mtime
57 if (remaining_gap := self.__min_gap - gap) > 0:
58 await asyncio.sleep(remaining_gap + random.random() * self.__randomness)
59 continue
61 # Note that since we are not actually locking the file,
62 # there is rare chance that multiple threads can run at the same time.
63 self.__lockfile.touch()
64 return await func(*args, **kwargs)
66 return wrapper_async if self.__use_async else wrapper