limits 4.1__py3-none-any.whl → 4.3__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 +8 -6
- limits/_version.py +4 -4
- limits/aio/__init__.py +2 -0
- limits/aio/storage/__init__.py +6 -4
- limits/aio/storage/base.py +5 -8
- limits/aio/storage/etcd.py +6 -4
- limits/aio/storage/memcached.py +6 -4
- limits/aio/storage/memory.py +42 -26
- limits/aio/storage/mongodb.py +4 -7
- limits/aio/storage/redis/__init__.py +402 -0
- limits/aio/storage/redis/bridge.py +120 -0
- limits/aio/storage/redis/coredis.py +209 -0
- limits/aio/storage/redis/redispy.py +257 -0
- limits/aio/storage/redis/valkey.py +9 -0
- limits/aio/strategies.py +4 -2
- limits/errors.py +2 -0
- limits/storage/__init__.py +14 -11
- limits/storage/base.py +5 -10
- limits/storage/etcd.py +6 -4
- limits/storage/memcached.py +6 -7
- limits/storage/memory.py +42 -31
- limits/storage/mongodb.py +7 -10
- limits/storage/redis.py +48 -18
- limits/storage/redis_cluster.py +31 -11
- limits/storage/redis_sentinel.py +35 -11
- limits/storage/registry.py +1 -3
- limits/strategies.py +11 -9
- limits/typing.py +45 -42
- limits/util.py +12 -12
- {limits-4.1.dist-info → limits-4.3.dist-info}/METADATA +52 -36
- limits-4.3.dist-info/RECORD +43 -0
- {limits-4.1.dist-info → limits-4.3.dist-info}/WHEEL +1 -1
- limits/aio/storage/redis.py +0 -555
- limits-4.1.dist-info/RECORD +0 -39
- {limits-4.1.dist-info → limits-4.3.dist-info}/LICENSE.txt +0 -0
- {limits-4.1.dist-info → limits-4.3.dist-info}/top_level.txt +0 -0
limits/__init__.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Rate limiting with commonly used storage backends
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
from . import _version, aio, storage, strategies
|
|
6
8
|
from .limits import (
|
|
7
9
|
RateLimitItem,
|
|
@@ -16,18 +18,18 @@ from .util import WindowStats, parse, parse_many
|
|
|
16
18
|
|
|
17
19
|
__all__ = [
|
|
18
20
|
"RateLimitItem",
|
|
19
|
-
"RateLimitItemPerYear",
|
|
20
|
-
"RateLimitItemPerMonth",
|
|
21
21
|
"RateLimitItemPerDay",
|
|
22
22
|
"RateLimitItemPerHour",
|
|
23
23
|
"RateLimitItemPerMinute",
|
|
24
|
+
"RateLimitItemPerMonth",
|
|
24
25
|
"RateLimitItemPerSecond",
|
|
26
|
+
"RateLimitItemPerYear",
|
|
27
|
+
"WindowStats",
|
|
25
28
|
"aio",
|
|
26
|
-
"storage",
|
|
27
|
-
"strategies",
|
|
28
29
|
"parse",
|
|
29
30
|
"parse_many",
|
|
30
|
-
"
|
|
31
|
+
"storage",
|
|
32
|
+
"strategies",
|
|
31
33
|
]
|
|
32
34
|
|
|
33
|
-
__version__ = _version.get_versions()["version"]
|
|
35
|
+
__version__ = _version.get_versions()["version"]
|
limits/_version.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
# This file was generated by 'versioneer.py' (0.
|
|
2
|
+
# This file was generated by 'versioneer.py' (0.29) from
|
|
3
3
|
# revision-control system data, or from the parent directory name of an
|
|
4
4
|
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
|
5
5
|
# of this file.
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-03-
|
|
11
|
+
"date": "2025-03-14T16:19:10-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "4.
|
|
14
|
+
"full-revisionid": "0bcebd7b69d035e3df82779a50fb2d1e901b9ef9",
|
|
15
|
+
"version": "4.3"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
limits/aio/__init__.py
CHANGED
limits/aio/storage/__init__.py
CHANGED
|
@@ -3,6 +3,8 @@ Implementations of storage backends to be used with
|
|
|
3
3
|
:class:`limits.aio.strategies.RateLimiter` strategies
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
|
|
7
9
|
from .etcd import EtcdStorage
|
|
8
10
|
from .memcached import MemcachedStorage
|
|
@@ -11,14 +13,14 @@ from .mongodb import MongoDBStorage
|
|
|
11
13
|
from .redis import RedisClusterStorage, RedisSentinelStorage, RedisStorage
|
|
12
14
|
|
|
13
15
|
__all__ = [
|
|
14
|
-
"Storage",
|
|
15
|
-
"MovingWindowSupport",
|
|
16
|
-
"SlidingWindowCounterSupport",
|
|
17
16
|
"EtcdStorage",
|
|
18
17
|
"MemcachedStorage",
|
|
19
18
|
"MemoryStorage",
|
|
20
19
|
"MongoDBStorage",
|
|
21
|
-
"
|
|
20
|
+
"MovingWindowSupport",
|
|
22
21
|
"RedisClusterStorage",
|
|
23
22
|
"RedisSentinelStorage",
|
|
23
|
+
"RedisStorage",
|
|
24
|
+
"SlidingWindowCounterSupport",
|
|
25
|
+
"Storage",
|
|
24
26
|
]
|
limits/aio/storage/base.py
CHANGED
|
@@ -11,11 +11,8 @@ from limits.typing import (
|
|
|
11
11
|
Any,
|
|
12
12
|
Awaitable,
|
|
13
13
|
Callable,
|
|
14
|
-
Optional,
|
|
15
14
|
P,
|
|
16
15
|
R,
|
|
17
|
-
Type,
|
|
18
|
-
Union,
|
|
19
16
|
cast,
|
|
20
17
|
)
|
|
21
18
|
from limits.util import LazyDependency
|
|
@@ -43,7 +40,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
43
40
|
Base class to extend when implementing an async storage backend.
|
|
44
41
|
"""
|
|
45
42
|
|
|
46
|
-
STORAGE_SCHEME:
|
|
43
|
+
STORAGE_SCHEME: list[str] | None
|
|
47
44
|
"""The storage schemes to register against this implementation"""
|
|
48
45
|
|
|
49
46
|
def __init_subclass__(cls, **kwargs: Any) -> None: # type:ignore[explicit-any]
|
|
@@ -61,9 +58,9 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
61
58
|
|
|
62
59
|
def __init__(
|
|
63
60
|
self,
|
|
64
|
-
uri:
|
|
61
|
+
uri: str | None = None,
|
|
65
62
|
wrap_exceptions: bool = False,
|
|
66
|
-
**options:
|
|
63
|
+
**options: float | str | bool,
|
|
67
64
|
) -> None:
|
|
68
65
|
"""
|
|
69
66
|
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
@@ -74,7 +71,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
74
71
|
|
|
75
72
|
@property
|
|
76
73
|
@abstractmethod
|
|
77
|
-
def base_exceptions(self) ->
|
|
74
|
+
def base_exceptions(self) -> type[Exception] | tuple[type[Exception], ...]:
|
|
78
75
|
raise NotImplementedError
|
|
79
76
|
|
|
80
77
|
@abstractmethod
|
|
@@ -114,7 +111,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
114
111
|
raise NotImplementedError
|
|
115
112
|
|
|
116
113
|
@abstractmethod
|
|
117
|
-
async def reset(self) ->
|
|
114
|
+
async def reset(self) -> int | None:
|
|
118
115
|
"""
|
|
119
116
|
reset storage to clear limits
|
|
120
117
|
"""
|
limits/aio/storage/etcd.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import time
|
|
3
5
|
import urllib.parse
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
5
7
|
|
|
6
8
|
from limits.aio.storage.base import Storage
|
|
7
9
|
from limits.errors import ConcurrentUpdateError
|
|
@@ -44,7 +46,7 @@ class EtcdStorage(Storage):
|
|
|
44
46
|
"""
|
|
45
47
|
parsed = urllib.parse.urlparse(uri)
|
|
46
48
|
self.lib = self.dependencies["aetcd"].module
|
|
47
|
-
self.storage:
|
|
49
|
+
self.storage: aetcd.Client = self.lib.Client(
|
|
48
50
|
host=parsed.hostname, port=parsed.port, **options
|
|
49
51
|
)
|
|
50
52
|
self.max_retries = max_retries
|
|
@@ -53,7 +55,7 @@ class EtcdStorage(Storage):
|
|
|
53
55
|
@property
|
|
54
56
|
def base_exceptions(
|
|
55
57
|
self,
|
|
56
|
-
) ->
|
|
58
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
57
59
|
return self.lib.ClientError # type: ignore[no-any-return]
|
|
58
60
|
|
|
59
61
|
def prefixed_key(self, key: str) -> bytes:
|
|
@@ -134,7 +136,7 @@ class EtcdStorage(Storage):
|
|
|
134
136
|
except: # noqa
|
|
135
137
|
return False
|
|
136
138
|
|
|
137
|
-
async def reset(self) ->
|
|
139
|
+
async def reset(self) -> int | None:
|
|
138
140
|
return (await self.storage.delete_prefix(f"{self.PREFIX}/".encode())).deleted
|
|
139
141
|
|
|
140
142
|
async def clear(self, key: str) -> None:
|
limits/aio/storage/memcached.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import time
|
|
2
4
|
import urllib.parse
|
|
3
5
|
from collections.abc import Iterable
|
|
@@ -7,7 +9,7 @@ from deprecated.sphinx import versionadded
|
|
|
7
9
|
|
|
8
10
|
from limits.aio.storage.base import SlidingWindowCounterSupport, Storage
|
|
9
11
|
from limits.storage.base import TimestampedSlidingWindow
|
|
10
|
-
from limits.typing import EmcacheClientP, ItemP
|
|
12
|
+
from limits.typing import EmcacheClientP, ItemP
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@versionadded(version="2.1")
|
|
@@ -27,7 +29,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
27
29
|
self,
|
|
28
30
|
uri: str,
|
|
29
31
|
wrap_exceptions: bool = False,
|
|
30
|
-
**options:
|
|
32
|
+
**options: float | str | bool,
|
|
31
33
|
) -> None:
|
|
32
34
|
"""
|
|
33
35
|
:param uri: memcached location of the form
|
|
@@ -54,7 +56,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
54
56
|
@property
|
|
55
57
|
def base_exceptions(
|
|
56
58
|
self,
|
|
57
|
-
) ->
|
|
59
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
58
60
|
return (
|
|
59
61
|
self.dependency.ClusterNoAvailableNodes,
|
|
60
62
|
self.dependency.CommandError,
|
|
@@ -204,7 +206,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
204
206
|
except: # noqa
|
|
205
207
|
return False
|
|
206
208
|
|
|
207
|
-
async def reset(self) ->
|
|
209
|
+
async def reset(self) -> int | None:
|
|
208
210
|
raise NotImplementedError
|
|
209
211
|
|
|
210
212
|
async def acquire_sliding_window_entry(
|
limits/aio/storage/memory.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import time
|
|
3
5
|
from collections import Counter, defaultdict
|
|
@@ -12,14 +14,12 @@ from limits.aio.storage.base import (
|
|
|
12
14
|
Storage,
|
|
13
15
|
)
|
|
14
16
|
from limits.storage.base import TimestampedSlidingWindow
|
|
15
|
-
from limits.typing import Optional, Type, Union
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class
|
|
19
|
+
class Entry:
|
|
19
20
|
def __init__(self, expiry: int) -> None:
|
|
20
21
|
self.atime = time.time()
|
|
21
22
|
self.expiry = self.atime + expiry
|
|
22
|
-
super().__init__()
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
@versionadded(version="2.1")
|
|
@@ -39,27 +39,36 @@ class MemoryStorage(
|
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
def __init__(
|
|
42
|
-
self, uri:
|
|
42
|
+
self, uri: str | None = None, wrap_exceptions: bool = False, **_: str
|
|
43
43
|
) -> None:
|
|
44
44
|
self.storage: limits.typing.Counter[str] = Counter()
|
|
45
45
|
self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
|
46
46
|
self.expirations: dict[str, float] = {}
|
|
47
|
-
self.events: dict[str, list[
|
|
48
|
-
self.timer:
|
|
47
|
+
self.events: dict[str, list[Entry]] = {}
|
|
48
|
+
self.timer: asyncio.Task[None] | None = None
|
|
49
49
|
super().__init__(uri, wrap_exceptions=wrap_exceptions, **_)
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
51
|
+
def __getstate__(self) -> dict[str, limits.typing.Any]: # type: ignore[explicit-any]
|
|
52
|
+
state = self.__dict__.copy()
|
|
53
|
+
del state["timer"]
|
|
54
|
+
del state["locks"]
|
|
55
|
+
return state
|
|
56
|
+
|
|
57
|
+
def __setstate__(self, state: dict[str, limits.typing.Any]) -> None: # type: ignore[explicit-any]
|
|
58
|
+
self.__dict__.update(state)
|
|
59
|
+
self.timer = None
|
|
60
|
+
self.locks = defaultdict(asyncio.Lock)
|
|
61
|
+
asyncio.ensure_future(self.__schedule_expiry())
|
|
56
62
|
|
|
57
63
|
async def __expire_events(self) -> None:
|
|
58
64
|
for key in self.events.keys():
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
async with self.locks[key]:
|
|
66
|
+
for event in list(self.events[key]):
|
|
61
67
|
if event.expiry <= time.time() and event in self.events[key]:
|
|
62
68
|
self.events[key].remove(event)
|
|
69
|
+
if not self.events.get(key, None):
|
|
70
|
+
self.events.pop(key, None)
|
|
71
|
+
self.locks.pop(key, None)
|
|
63
72
|
|
|
64
73
|
for key in list(self.expirations.keys()):
|
|
65
74
|
if self.expirations[key] <= time.time():
|
|
@@ -71,6 +80,12 @@ class MemoryStorage(
|
|
|
71
80
|
if not self.timer or self.timer.done():
|
|
72
81
|
self.timer = asyncio.create_task(self.__expire_events())
|
|
73
82
|
|
|
83
|
+
@property
|
|
84
|
+
def base_exceptions(
|
|
85
|
+
self,
|
|
86
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
87
|
+
return ValueError
|
|
88
|
+
|
|
74
89
|
async def incr(
|
|
75
90
|
self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
|
|
76
91
|
) -> int:
|
|
@@ -138,18 +153,19 @@ class MemoryStorage(
|
|
|
138
153
|
if amount > limit:
|
|
139
154
|
return False
|
|
140
155
|
|
|
141
|
-
self.events.setdefault(key, [])
|
|
142
156
|
await self.__schedule_expiry()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
async with self.locks[key]:
|
|
158
|
+
self.events.setdefault(key, [])
|
|
159
|
+
timestamp = time.time()
|
|
160
|
+
try:
|
|
161
|
+
entry: Entry | None = self.events[key][limit - amount]
|
|
162
|
+
except IndexError:
|
|
163
|
+
entry = None
|
|
164
|
+
|
|
165
|
+
if entry and entry.atime >= timestamp - expiry:
|
|
166
|
+
return False
|
|
167
|
+
else:
|
|
168
|
+
self.events[key][:0] = [Entry(expiry) for _ in range(amount)]
|
|
153
169
|
|
|
154
170
|
return True
|
|
155
171
|
|
|
@@ -170,7 +186,7 @@ class MemoryStorage(
|
|
|
170
186
|
timestamp = time.time()
|
|
171
187
|
|
|
172
188
|
return (
|
|
173
|
-
len([k for k in self.events[
|
|
189
|
+
len([k for k in self.events.get(key, []) if k.atime >= timestamp - expiry])
|
|
174
190
|
if self.events.get(key)
|
|
175
191
|
else 0
|
|
176
192
|
)
|
|
@@ -262,7 +278,7 @@ class MemoryStorage(
|
|
|
262
278
|
|
|
263
279
|
return True
|
|
264
280
|
|
|
265
|
-
async def reset(self) ->
|
|
281
|
+
async def reset(self) -> int | None:
|
|
266
282
|
num_items = max(len(self.storage), len(self.events))
|
|
267
283
|
self.storage.clear()
|
|
268
284
|
self.expirations.clear()
|
limits/aio/storage/mongodb.py
CHANGED
|
@@ -12,11 +12,8 @@ from limits.aio.storage.base import (
|
|
|
12
12
|
Storage,
|
|
13
13
|
)
|
|
14
14
|
from limits.typing import (
|
|
15
|
-
Optional,
|
|
16
15
|
ParamSpec,
|
|
17
|
-
Type,
|
|
18
16
|
TypeVar,
|
|
19
|
-
Union,
|
|
20
17
|
cast,
|
|
21
18
|
)
|
|
22
19
|
from limits.util import get_dependency
|
|
@@ -51,7 +48,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
51
48
|
counter_collection_name: str = "counters",
|
|
52
49
|
window_collection_name: str = "windows",
|
|
53
50
|
wrap_exceptions: bool = False,
|
|
54
|
-
**options:
|
|
51
|
+
**options: float | str | bool,
|
|
55
52
|
) -> None:
|
|
56
53
|
"""
|
|
57
54
|
:param uri: uri of the form ``async+mongodb://[user:password]@host:port?...``,
|
|
@@ -94,7 +91,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
94
91
|
@property
|
|
95
92
|
def base_exceptions(
|
|
96
93
|
self,
|
|
97
|
-
) ->
|
|
94
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
98
95
|
return self.lib_errors.PyMongoError # type: ignore
|
|
99
96
|
|
|
100
97
|
@property
|
|
@@ -113,7 +110,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
113
110
|
)
|
|
114
111
|
self.__indices_created = True
|
|
115
112
|
|
|
116
|
-
async def reset(self) ->
|
|
113
|
+
async def reset(self) -> int | None:
|
|
117
114
|
"""
|
|
118
115
|
Delete all rate limit keys in the rate limit collections (counters, windows)
|
|
119
116
|
"""
|
|
@@ -294,7 +291,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
294
291
|
try:
|
|
295
292
|
updates: dict[
|
|
296
293
|
str,
|
|
297
|
-
dict[str,
|
|
294
|
+
dict[str, datetime.datetime | dict[str, list[float] | int]],
|
|
298
295
|
] = {
|
|
299
296
|
"$push": {
|
|
300
297
|
"entries": {
|