limits 4.0.1__py3-none-any.whl → 4.2__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.
- limits/__init__.py +3 -1
- limits/_version.py +4 -4
- limits/aio/__init__.py +2 -0
- limits/aio/storage/__init__.py +4 -1
- limits/aio/storage/base.py +70 -24
- limits/aio/storage/etcd.py +8 -2
- limits/aio/storage/memcached.py +159 -33
- limits/aio/storage/memory.py +100 -13
- limits/aio/storage/mongodb.py +217 -9
- limits/aio/storage/redis/__init__.py +341 -0
- limits/aio/storage/redis/bridge.py +121 -0
- limits/aio/storage/redis/coredis.py +209 -0
- limits/aio/storage/redis/redispy.py +257 -0
- limits/aio/strategies.py +124 -1
- limits/errors.py +2 -0
- limits/limits.py +10 -11
- limits/resources/redis/lua_scripts/acquire_sliding_window.lua +45 -0
- limits/resources/redis/lua_scripts/sliding_window.lua +17 -0
- limits/storage/__init__.py +6 -3
- limits/storage/base.py +92 -24
- limits/storage/etcd.py +8 -2
- limits/storage/memcached.py +143 -34
- limits/storage/memory.py +99 -12
- limits/storage/mongodb.py +204 -11
- limits/storage/redis.py +159 -138
- limits/storage/redis_cluster.py +5 -3
- limits/storage/redis_sentinel.py +14 -35
- limits/storage/registry.py +3 -3
- limits/strategies.py +121 -5
- limits/typing.py +55 -19
- limits/util.py +29 -18
- limits-4.2.dist-info/METADATA +268 -0
- limits-4.2.dist-info/RECORD +42 -0
- limits/aio/storage/redis.py +0 -470
- limits-4.0.1.dist-info/METADATA +0 -192
- limits-4.0.1.dist-info/RECORD +0 -37
- {limits-4.0.1.dist-info → limits-4.2.dist-info}/LICENSE.txt +0 -0
- {limits-4.0.1.dist-info → limits-4.2.dist-info}/WHEEL +0 -0
- {limits-4.0.1.dist-info → limits-4.2.dist-info}/top_level.txt +0 -0
limits/storage/redis_sentinel.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import urllib.parse
|
|
2
4
|
from typing import TYPE_CHECKING
|
|
3
5
|
|
|
@@ -5,10 +7,10 @@ from packaging.version import Version
|
|
|
5
7
|
|
|
6
8
|
from limits.errors import ConfigurationError
|
|
7
9
|
from limits.storage.redis import RedisStorage
|
|
8
|
-
from limits.typing import
|
|
10
|
+
from limits.typing import Optional, RedisClient, Type, Union
|
|
9
11
|
|
|
10
12
|
if TYPE_CHECKING:
|
|
11
|
-
|
|
13
|
+
pass
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class RedisSentinelStorage(RedisStorage):
|
|
@@ -21,14 +23,14 @@ class RedisSentinelStorage(RedisStorage):
|
|
|
21
23
|
STORAGE_SCHEME = ["redis+sentinel"]
|
|
22
24
|
"""The storage scheme for redis accessed via a redis sentinel installation"""
|
|
23
25
|
|
|
24
|
-
DEPENDENCIES = {"redis.sentinel": Version("3.0")}
|
|
26
|
+
DEPENDENCIES = {"redis": Version("3.0"), "redis.sentinel": Version("3.0")}
|
|
25
27
|
|
|
26
28
|
def __init__(
|
|
27
29
|
self,
|
|
28
30
|
uri: str,
|
|
29
31
|
service_name: Optional[str] = None,
|
|
30
32
|
use_replicas: bool = True,
|
|
31
|
-
sentinel_kwargs: Optional[
|
|
33
|
+
sentinel_kwargs: Optional[dict[str, Union[float, str, bool]]] = None,
|
|
32
34
|
wrap_exceptions: bool = False,
|
|
33
35
|
**options: Union[float, str, bool],
|
|
34
36
|
) -> None:
|
|
@@ -56,7 +58,7 @@ class RedisSentinelStorage(RedisStorage):
|
|
|
56
58
|
sentinel_configuration = []
|
|
57
59
|
sentinel_options = sentinel_kwargs.copy() if sentinel_kwargs else {}
|
|
58
60
|
|
|
59
|
-
parsed_auth:
|
|
61
|
+
parsed_auth: dict[str, Union[float, str, bool]] = {}
|
|
60
62
|
|
|
61
63
|
if parsed.username:
|
|
62
64
|
parsed_auth["username"] = parsed.username
|
|
@@ -76,44 +78,21 @@ class RedisSentinelStorage(RedisStorage):
|
|
|
76
78
|
raise ConfigurationError("'service_name' not provided")
|
|
77
79
|
|
|
78
80
|
sentinel_dep = self.dependencies["redis.sentinel"].module
|
|
79
|
-
self.sentinel
|
|
81
|
+
self.sentinel = sentinel_dep.Sentinel(
|
|
80
82
|
sentinel_configuration,
|
|
81
83
|
sentinel_kwargs={**parsed_auth, **sentinel_options},
|
|
82
84
|
**{**parsed_auth, **options},
|
|
83
85
|
)
|
|
84
|
-
self.storage = self.sentinel.master_for(self.service_name)
|
|
85
|
-
self.storage_slave = self.sentinel.slave_for(self.service_name)
|
|
86
|
+
self.storage: RedisClient = self.sentinel.master_for(self.service_name)
|
|
87
|
+
self.storage_slave: RedisClient = self.sentinel.slave_for(self.service_name)
|
|
86
88
|
self.use_replicas = use_replicas
|
|
87
89
|
self.initialize_storage(uri)
|
|
88
90
|
|
|
89
91
|
@property
|
|
90
92
|
def base_exceptions(
|
|
91
93
|
self,
|
|
92
|
-
) -> Union[Type[Exception],
|
|
93
|
-
return self.dependencies["redis"].RedisError # type: ignore[no-any-return
|
|
94
|
-
|
|
95
|
-
def get(self, key: str) -> int:
|
|
96
|
-
"""
|
|
97
|
-
:param key: the key to get the counter value for
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
return super()._get(
|
|
101
|
-
key, self.storage_slave if self.use_replicas else self.storage
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
def get_expiry(self, key: str) -> float:
|
|
105
|
-
"""
|
|
106
|
-
:param key: the key to get the expiry for
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
return super()._get_expiry(
|
|
110
|
-
key, self.storage_slave if self.use_replicas else self.storage
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
def check(self) -> bool:
|
|
114
|
-
"""
|
|
115
|
-
Check if storage is healthy by calling :class:`aredis.StrictRedis.ping`
|
|
116
|
-
on the slave.
|
|
117
|
-
"""
|
|
94
|
+
) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
|
|
95
|
+
return self.dependencies["redis"].module.RedisError # type: ignore[no-any-return]
|
|
118
96
|
|
|
119
|
-
|
|
97
|
+
def get_connection(self, readonly: bool = False) -> RedisClient:
|
|
98
|
+
return self.storage_slave if (readonly and self.use_replicas) else self.storage
|
limits/storage/registry.py
CHANGED
|
@@ -2,14 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from abc import ABCMeta
|
|
4
4
|
|
|
5
|
-
from limits.typing import
|
|
5
|
+
from limits.typing import Union
|
|
6
6
|
|
|
7
|
-
SCHEMES:
|
|
7
|
+
SCHEMES: dict[str, StorageRegistry] = {}
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class StorageRegistry(ABCMeta):
|
|
11
11
|
def __new__(
|
|
12
|
-
mcs, name: str, bases:
|
|
12
|
+
mcs, name: str, bases: tuple[type, ...], dct: dict[str, Union[str, list[str]]]
|
|
13
13
|
) -> StorageRegistry:
|
|
14
14
|
storage_scheme = dct.get("STORAGE_SCHEME", None)
|
|
15
15
|
cls = super().__new__(mcs, name, bases, dct)
|
limits/strategies.py
CHANGED
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
Rate limiting strategies
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import time
|
|
5
8
|
from abc import ABCMeta, abstractmethod
|
|
6
|
-
from
|
|
9
|
+
from math import floor, inf
|
|
10
|
+
|
|
11
|
+
from deprecated.sphinx import deprecated, versionadded
|
|
12
|
+
|
|
13
|
+
from limits.storage.base import SlidingWindowCounterSupport
|
|
7
14
|
|
|
8
15
|
from .limits import RateLimitItem
|
|
9
16
|
from .storage import MovingWindowSupport, Storage, StorageTypes
|
|
17
|
+
from .typing import Union, cast
|
|
10
18
|
from .util import WindowStats
|
|
11
19
|
|
|
12
20
|
|
|
@@ -173,6 +181,112 @@ class FixedWindowRateLimiter(RateLimiter):
|
|
|
173
181
|
return WindowStats(reset, remaining)
|
|
174
182
|
|
|
175
183
|
|
|
184
|
+
@versionadded(version="4.1")
|
|
185
|
+
class SlidingWindowCounterRateLimiter(RateLimiter):
|
|
186
|
+
"""
|
|
187
|
+
Reference: :ref:`strategies:sliding window counter`
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, storage: StorageTypes):
|
|
191
|
+
if not hasattr(storage, "get_sliding_window") or not hasattr(
|
|
192
|
+
storage, "acquire_sliding_window_entry"
|
|
193
|
+
):
|
|
194
|
+
raise NotImplementedError(
|
|
195
|
+
"SlidingWindowCounterRateLimiting is not implemented for storage "
|
|
196
|
+
"of type %s" % storage.__class__
|
|
197
|
+
)
|
|
198
|
+
super().__init__(storage)
|
|
199
|
+
|
|
200
|
+
def _weighted_count(
|
|
201
|
+
self,
|
|
202
|
+
item: RateLimitItem,
|
|
203
|
+
previous_count: int,
|
|
204
|
+
previous_expires_in: float,
|
|
205
|
+
current_count: int,
|
|
206
|
+
) -> float:
|
|
207
|
+
"""
|
|
208
|
+
Return the approximated by weighting the previous window count and adding the current window count.
|
|
209
|
+
"""
|
|
210
|
+
return previous_count * previous_expires_in / item.get_expiry() + current_count
|
|
211
|
+
|
|
212
|
+
def hit(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
213
|
+
"""
|
|
214
|
+
Consume the rate limit
|
|
215
|
+
|
|
216
|
+
:param item: The rate limit item
|
|
217
|
+
:param identifiers: variable list of strings to uniquely identify this
|
|
218
|
+
instance of the limit
|
|
219
|
+
:param cost: The cost of this hit, default 1
|
|
220
|
+
"""
|
|
221
|
+
return cast(
|
|
222
|
+
SlidingWindowCounterSupport, self.storage
|
|
223
|
+
).acquire_sliding_window_entry(
|
|
224
|
+
item.key_for(*identifiers),
|
|
225
|
+
item.amount,
|
|
226
|
+
item.get_expiry(),
|
|
227
|
+
cost,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Check if the rate limit can be consumed
|
|
233
|
+
|
|
234
|
+
:param item: The rate limit item
|
|
235
|
+
:param identifiers: variable list of strings to uniquely identify this
|
|
236
|
+
instance of the limit
|
|
237
|
+
:param cost: The expected cost to be consumed, default 1
|
|
238
|
+
"""
|
|
239
|
+
previous_count, previous_expires_in, current_count, _ = cast(
|
|
240
|
+
SlidingWindowCounterSupport, self.storage
|
|
241
|
+
).get_sliding_window(item.key_for(*identifiers), item.get_expiry())
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
self._weighted_count(
|
|
245
|
+
item, previous_count, previous_expires_in, current_count
|
|
246
|
+
)
|
|
247
|
+
< item.amount - cost + 1
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def get_window_stats(self, item: RateLimitItem, *identifiers: str) -> WindowStats:
|
|
251
|
+
"""
|
|
252
|
+
Query the reset time and remaining amount for the limit.
|
|
253
|
+
|
|
254
|
+
:param item: The rate limit item
|
|
255
|
+
:param identifiers: variable list of strings to uniquely identify this
|
|
256
|
+
instance of the limit
|
|
257
|
+
:return: WindowStats(reset time, remaining)
|
|
258
|
+
"""
|
|
259
|
+
previous_count, previous_expires_in, current_count, current_expires_in = cast(
|
|
260
|
+
SlidingWindowCounterSupport, self.storage
|
|
261
|
+
).get_sliding_window(item.key_for(*identifiers), item.get_expiry())
|
|
262
|
+
|
|
263
|
+
remaining = max(
|
|
264
|
+
0,
|
|
265
|
+
item.amount
|
|
266
|
+
- floor(
|
|
267
|
+
self._weighted_count(
|
|
268
|
+
item, previous_count, previous_expires_in, current_count
|
|
269
|
+
)
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
now = time.time()
|
|
274
|
+
|
|
275
|
+
if not (previous_count or current_count):
|
|
276
|
+
return WindowStats(now, remaining)
|
|
277
|
+
|
|
278
|
+
expiry = item.get_expiry()
|
|
279
|
+
|
|
280
|
+
previous_reset_in, current_reset_in = inf, inf
|
|
281
|
+
if previous_count:
|
|
282
|
+
previous_reset_in = previous_expires_in % (expiry / previous_count)
|
|
283
|
+
if current_count:
|
|
284
|
+
current_reset_in = current_expires_in % expiry
|
|
285
|
+
|
|
286
|
+
return WindowStats(now + min(previous_reset_in, current_reset_in), remaining)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@deprecated(version="4.1", action="always")
|
|
176
290
|
class FixedWindowElasticExpiryRateLimiter(FixedWindowRateLimiter):
|
|
177
291
|
"""
|
|
178
292
|
Reference: :ref:`strategies:fixed window with elastic expiry`
|
|
@@ -200,12 +314,14 @@ class FixedWindowElasticExpiryRateLimiter(FixedWindowRateLimiter):
|
|
|
200
314
|
|
|
201
315
|
|
|
202
316
|
KnownStrategy = Union[
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
317
|
+
type[SlidingWindowCounterRateLimiter],
|
|
318
|
+
type[FixedWindowRateLimiter],
|
|
319
|
+
type[FixedWindowElasticExpiryRateLimiter],
|
|
320
|
+
type[MovingWindowRateLimiter],
|
|
206
321
|
]
|
|
207
322
|
|
|
208
|
-
STRATEGIES:
|
|
323
|
+
STRATEGIES: dict[str, KnownStrategy] = {
|
|
324
|
+
"sliding-window-counter": SlidingWindowCounterRateLimiter,
|
|
209
325
|
"fixed-window": FixedWindowRateLimiter,
|
|
210
326
|
"fixed-window-elastic-expiry": FixedWindowElasticExpiryRateLimiter,
|
|
211
327
|
"moving-window": MovingWindowRateLimiter,
|
limits/typing.py
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from collections.abc import Awaitable, Iterable
|
|
1
5
|
from typing import (
|
|
2
6
|
TYPE_CHECKING,
|
|
3
7
|
Any,
|
|
4
|
-
Awaitable,
|
|
5
8
|
Callable,
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
ClassVar,
|
|
10
|
+
Literal,
|
|
8
11
|
NamedTuple,
|
|
9
12
|
Optional,
|
|
10
|
-
Tuple,
|
|
11
13
|
Type,
|
|
12
14
|
TypeVar,
|
|
13
15
|
Union,
|
|
16
|
+
cast,
|
|
14
17
|
)
|
|
15
18
|
|
|
16
|
-
from typing_extensions import
|
|
19
|
+
from typing_extensions import ParamSpec, Protocol, TypeAlias
|
|
17
20
|
|
|
18
21
|
Serializable = Union[int, str, float]
|
|
19
22
|
|
|
@@ -24,8 +27,8 @@ P = ParamSpec("P")
|
|
|
24
27
|
|
|
25
28
|
if TYPE_CHECKING:
|
|
26
29
|
import coredis
|
|
27
|
-
import
|
|
28
|
-
import pymongo
|
|
30
|
+
import pymongo.collection
|
|
31
|
+
import pymongo.database
|
|
29
32
|
import redis
|
|
30
33
|
|
|
31
34
|
|
|
@@ -48,12 +51,18 @@ class EmcacheClientP(Protocol):
|
|
|
48
51
|
|
|
49
52
|
async def get(self, key: bytes, return_flags: bool = False) -> Optional[ItemP]: ...
|
|
50
53
|
|
|
54
|
+
async def get_many(self, keys: Iterable[bytes]) -> dict[bytes, ItemP]: ...
|
|
55
|
+
|
|
51
56
|
async def gets(self, key: bytes, return_flags: bool = False) -> Optional[ItemP]: ...
|
|
52
57
|
|
|
53
58
|
async def increment(
|
|
54
59
|
self, key: bytes, value: int, *, noreply: bool = False
|
|
55
60
|
) -> Optional[int]: ...
|
|
56
61
|
|
|
62
|
+
async def decrement(
|
|
63
|
+
self, key: bytes, value: int, *, noreply: bool = False
|
|
64
|
+
) -> Optional[int]: ...
|
|
65
|
+
|
|
57
66
|
async def delete(self, key: bytes, *, noreply: bool = False) -> None: ...
|
|
58
67
|
|
|
59
68
|
async def set(
|
|
@@ -83,7 +92,18 @@ class MemcachedClientP(Protocol):
|
|
|
83
92
|
|
|
84
93
|
def get(self, key: str, default: Optional[str] = None) -> bytes: ...
|
|
85
94
|
|
|
86
|
-
def
|
|
95
|
+
def get_many(self, keys: Iterable[str]) -> dict[str, Any]: ... # type:ignore[explicit-any]
|
|
96
|
+
|
|
97
|
+
def incr(
|
|
98
|
+
self, key: str, value: int, noreply: Optional[bool] = False
|
|
99
|
+
) -> Optional[int]: ...
|
|
100
|
+
|
|
101
|
+
def decr(
|
|
102
|
+
self,
|
|
103
|
+
key: str,
|
|
104
|
+
value: int,
|
|
105
|
+
noreply: Optional[bool] = False,
|
|
106
|
+
) -> Optional[int]: ...
|
|
87
107
|
|
|
88
108
|
def delete(self, key: str, noreply: Optional[bool] = None) -> Optional[bool]: ...
|
|
89
109
|
|
|
@@ -101,28 +121,44 @@ class MemcachedClientP(Protocol):
|
|
|
101
121
|
) -> bool: ...
|
|
102
122
|
|
|
103
123
|
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
class RedisClientP(Protocol):
|
|
125
|
+
def incrby(self, key: str, amount: int) -> int: ...
|
|
126
|
+
def get(self, key: str) -> Optional[bytes]: ...
|
|
127
|
+
def delete(self, key: str) -> int: ...
|
|
128
|
+
def ttl(self, key: str) -> int: ...
|
|
129
|
+
def expire(self, key: str, seconds: int) -> bool: ...
|
|
130
|
+
def ping(self) -> bool: ...
|
|
131
|
+
def register_script(self, script: bytes) -> "redis.commands.core.Script": ...
|
|
132
|
+
|
|
106
133
|
|
|
134
|
+
class AsyncRedisClientP(Protocol):
|
|
135
|
+
async def incrby(self, key: str, amount: int) -> int: ...
|
|
136
|
+
async def get(self, key: str) -> Optional[bytes]: ...
|
|
137
|
+
async def delete(self, key: str) -> int: ...
|
|
138
|
+
async def ttl(self, key: str) -> int: ...
|
|
139
|
+
async def expire(self, key: str, seconds: int) -> bool: ...
|
|
140
|
+
async def ping(self) -> bool: ...
|
|
141
|
+
def register_script(self, script: bytes) -> "redis.commands.core.Script": ...
|
|
107
142
|
|
|
108
|
-
class ScriptP(Protocol[R_co]):
|
|
109
|
-
def __call__(self, keys: List[Serializable], args: List[Serializable]) -> R_co: ...
|
|
110
143
|
|
|
144
|
+
RedisClient = RedisClientP
|
|
145
|
+
AsyncRedisClient = AsyncRedisClientP
|
|
146
|
+
AsyncCoRedisClient = Union["coredis.Redis[bytes]", "coredis.RedisCluster[bytes]"]
|
|
111
147
|
|
|
112
|
-
MongoClient: TypeAlias = "pymongo.MongoClient[
|
|
113
|
-
MongoDatabase: TypeAlias = "pymongo.database.Database[
|
|
114
|
-
MongoCollection: TypeAlias = "pymongo.collection.Collection[
|
|
148
|
+
MongoClient: TypeAlias = "pymongo.MongoClient[dict[str, Any]]" # type:ignore[explicit-any]
|
|
149
|
+
MongoDatabase: TypeAlias = "pymongo.database.Database[dict[str, Any]]" # type:ignore[explicit-any]
|
|
150
|
+
MongoCollection: TypeAlias = "pymongo.collection.Collection[dict[str, Any]]" # type:ignore[explicit-any]
|
|
115
151
|
|
|
116
152
|
__all__ = [
|
|
153
|
+
"Any",
|
|
117
154
|
"AsyncRedisClient",
|
|
118
155
|
"Awaitable",
|
|
119
156
|
"Callable",
|
|
120
157
|
"ClassVar",
|
|
121
158
|
"Counter",
|
|
122
|
-
"Dict",
|
|
123
159
|
"EmcacheClientP",
|
|
124
160
|
"ItemP",
|
|
125
|
-
"
|
|
161
|
+
"Literal",
|
|
126
162
|
"MemcachedClientP",
|
|
127
163
|
"MongoClient",
|
|
128
164
|
"MongoCollection",
|
|
@@ -132,14 +168,14 @@ __all__ = [
|
|
|
132
168
|
"P",
|
|
133
169
|
"ParamSpec",
|
|
134
170
|
"Protocol",
|
|
135
|
-
"ScriptP",
|
|
136
171
|
"Serializable",
|
|
137
172
|
"TypeVar",
|
|
138
173
|
"R",
|
|
139
174
|
"R_co",
|
|
140
175
|
"RedisClient",
|
|
141
|
-
"Tuple",
|
|
142
176
|
"Type",
|
|
143
177
|
"TypeVar",
|
|
178
|
+
"TYPE_CHECKING",
|
|
144
179
|
"Union",
|
|
180
|
+
"cast",
|
|
145
181
|
]
|
limits/util.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
""" """
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import dataclasses
|
|
4
6
|
import importlib.resources
|
|
5
7
|
import re
|
|
@@ -10,7 +12,7 @@ from typing import TYPE_CHECKING
|
|
|
10
12
|
|
|
11
13
|
from packaging.version import Version
|
|
12
14
|
|
|
13
|
-
from limits.typing import
|
|
15
|
+
from limits.typing import NamedTuple, Optional, Type, Union
|
|
14
16
|
|
|
15
17
|
from .errors import ConfigurationError
|
|
16
18
|
from .limits import GRANULARITIES, RateLimitItem
|
|
@@ -34,7 +36,7 @@ EXPR = re.compile(
|
|
|
34
36
|
|
|
35
37
|
class WindowStats(NamedTuple):
|
|
36
38
|
"""
|
|
37
|
-
|
|
39
|
+
tuple to describe a rate limited window
|
|
38
40
|
"""
|
|
39
41
|
|
|
40
42
|
#: Time as seconds since the Epoch when this window will be reset
|
|
@@ -51,6 +53,9 @@ class Dependency:
|
|
|
51
53
|
module: ModuleType
|
|
52
54
|
|
|
53
55
|
|
|
56
|
+
MissingModule = ModuleType("Missing")
|
|
57
|
+
|
|
58
|
+
|
|
54
59
|
if TYPE_CHECKING:
|
|
55
60
|
_UserDict = UserDict[str, Dependency]
|
|
56
61
|
else:
|
|
@@ -58,20 +63,29 @@ else:
|
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
class DependencyDict(_UserDict):
|
|
61
|
-
Missing = Dependency("Missing", None, None, ModuleType("Missing"))
|
|
62
|
-
|
|
63
66
|
def __getitem__(self, key: str) -> Dependency:
|
|
64
67
|
dependency = super().__getitem__(key)
|
|
65
68
|
|
|
66
|
-
if dependency
|
|
67
|
-
|
|
69
|
+
if dependency.module is MissingModule:
|
|
70
|
+
message = f"'{dependency.name}' prerequisite not available."
|
|
71
|
+
if dependency.version_required:
|
|
72
|
+
message += (
|
|
73
|
+
f" A minimum version of {dependency.version_required} is required."
|
|
74
|
+
if dependency.version_required
|
|
75
|
+
else ""
|
|
76
|
+
)
|
|
77
|
+
message += (
|
|
78
|
+
" See https://limits.readthedocs.io/en/stable/storage.html#supported-versions"
|
|
79
|
+
" for more details."
|
|
80
|
+
)
|
|
81
|
+
raise ConfigurationError(message)
|
|
68
82
|
elif dependency.version_required and (
|
|
69
83
|
not dependency.version_found
|
|
70
84
|
or dependency.version_found < dependency.version_required
|
|
71
85
|
):
|
|
72
86
|
raise ConfigurationError(
|
|
73
87
|
f"The minimum version of {dependency.version_required}"
|
|
74
|
-
f"
|
|
88
|
+
f" for '{dependency.name}' could not be found. Found version: {dependency.version_found}"
|
|
75
89
|
)
|
|
76
90
|
|
|
77
91
|
return dependency
|
|
@@ -84,7 +98,7 @@ class LazyDependency:
|
|
|
84
98
|
without having to import them explicitly.
|
|
85
99
|
"""
|
|
86
100
|
|
|
87
|
-
DEPENDENCIES: Union[
|
|
101
|
+
DEPENDENCIES: Union[dict[str, Optional[Version]], list[str]] = []
|
|
88
102
|
"""
|
|
89
103
|
The python modules this class has a dependency on.
|
|
90
104
|
Used to lazily populate the :attr:`dependencies`
|
|
@@ -105,7 +119,7 @@ class LazyDependency:
|
|
|
105
119
|
|
|
106
120
|
if not getattr(self, "_dependencies", None):
|
|
107
121
|
dependencies = DependencyDict()
|
|
108
|
-
mapping:
|
|
122
|
+
mapping: dict[str, Optional[Version]]
|
|
109
123
|
|
|
110
124
|
if isinstance(self.DEPENDENCIES, list):
|
|
111
125
|
mapping = {dependency: None for dependency in self.DEPENDENCIES}
|
|
@@ -115,18 +129,15 @@ class LazyDependency:
|
|
|
115
129
|
for name, minimum_version in mapping.items():
|
|
116
130
|
dependency, version = get_dependency(name)
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
dependencies[name] = Dependency(
|
|
122
|
-
name, minimum_version, version, dependency
|
|
123
|
-
)
|
|
132
|
+
dependencies[name] = Dependency(
|
|
133
|
+
name, minimum_version, version, dependency
|
|
134
|
+
)
|
|
124
135
|
self._dependencies = dependencies
|
|
125
136
|
|
|
126
137
|
return self._dependencies
|
|
127
138
|
|
|
128
139
|
|
|
129
|
-
def get_dependency(module_path: str) ->
|
|
140
|
+
def get_dependency(module_path: str) -> tuple[ModuleType, Optional[Version]]:
|
|
130
141
|
"""
|
|
131
142
|
safe function to import a module at runtime
|
|
132
143
|
"""
|
|
@@ -138,14 +149,14 @@ def get_dependency(module_path: str) -> Tuple[Optional[ModuleType], Optional[Ver
|
|
|
138
149
|
|
|
139
150
|
return sys.modules[module_path], Version(version)
|
|
140
151
|
except ImportError: # pragma: no cover
|
|
141
|
-
return
|
|
152
|
+
return MissingModule, None
|
|
142
153
|
|
|
143
154
|
|
|
144
155
|
def get_package_data(path: str) -> bytes:
|
|
145
156
|
return importlib.resources.files("limits").joinpath(path).read_bytes()
|
|
146
157
|
|
|
147
158
|
|
|
148
|
-
def parse_many(limit_string: str) ->
|
|
159
|
+
def parse_many(limit_string: str) -> list[RateLimitItem]:
|
|
149
160
|
"""
|
|
150
161
|
parses rate limits in string notation containing multiple rate limits
|
|
151
162
|
(e.g. ``1/second; 5/minute``)
|