limits 4.7.2__py3-none-any.whl → 5.0.0rc1__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/_version.py +3 -3
- limits/aio/storage/__init__.py +0 -2
- limits/aio/storage/base.py +1 -5
- limits/aio/storage/memcached.py +34 -58
- limits/aio/storage/memory.py +20 -38
- limits/aio/storage/mongodb.py +26 -31
- limits/aio/storage/redis/__init__.py +2 -4
- limits/aio/storage/redis/bridge.py +0 -1
- limits/aio/storage/redis/coredis.py +2 -6
- limits/aio/storage/redis/redispy.py +1 -8
- limits/aio/strategies.py +1 -28
- limits/resources/redis/lua_scripts/acquire_moving_window.lua +5 -2
- limits/resources/redis/lua_scripts/moving_window.lua +23 -14
- limits/storage/__init__.py +0 -2
- limits/storage/base.py +1 -5
- limits/storage/memcached.py +6 -29
- limits/storage/memory.py +16 -35
- limits/storage/mongodb.py +25 -34
- limits/storage/redis.py +1 -7
- limits/strategies.py +1 -31
- limits/typing.py +0 -50
- {limits-4.7.2.dist-info → limits-5.0.0rc1.dist-info}/METADATA +7 -14
- limits-5.0.0rc1.dist-info/RECORD +41 -0
- limits/aio/storage/etcd.py +0 -146
- limits/storage/etcd.py +0 -139
- limits-4.7.2.dist-info/RECORD +0 -43
- {limits-4.7.2.dist-info → limits-5.0.0rc1.dist-info}/WHEEL +0 -0
- {limits-4.7.2.dist-info → limits-5.0.0rc1.dist-info}/licenses/LICENSE.txt +0 -0
- {limits-4.7.2.dist-info → limits-5.0.0rc1.dist-info}/top_level.txt +0 -0
limits/_version.py
CHANGED
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-04-
|
|
11
|
+
"date": "2025-04-09T18:20:47-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "
|
|
14
|
+
"full-revisionid": "4a01f1090a5accfb05b7db6dc6469f7c51d4fa67",
|
|
15
|
+
"version": "5.0.0rc1"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
limits/aio/storage/__init__.py
CHANGED
|
@@ -6,14 +6,12 @@ Implementations of storage backends to be used with
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
|
|
9
|
-
from .etcd import EtcdStorage
|
|
10
9
|
from .memcached import MemcachedStorage
|
|
11
10
|
from .memory import MemoryStorage
|
|
12
11
|
from .mongodb import MongoDBStorage
|
|
13
12
|
from .redis import RedisClusterStorage, RedisSentinelStorage, RedisStorage
|
|
14
13
|
|
|
15
14
|
__all__ = [
|
|
16
|
-
"EtcdStorage",
|
|
17
15
|
"MemcachedStorage",
|
|
18
16
|
"MemoryStorage",
|
|
19
17
|
"MongoDBStorage",
|
limits/aio/storage/base.py
CHANGED
|
@@ -75,16 +75,12 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
75
75
|
raise NotImplementedError
|
|
76
76
|
|
|
77
77
|
@abstractmethod
|
|
78
|
-
async def incr(
|
|
79
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
80
|
-
) -> int:
|
|
78
|
+
async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
81
79
|
"""
|
|
82
80
|
increments the counter for a given rate limit key
|
|
83
81
|
|
|
84
82
|
:param key: the key to increment
|
|
85
83
|
:param expiry: amount in seconds for the key to expire in
|
|
86
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
87
|
-
window every hit.
|
|
88
84
|
:param amount: the number to increment by
|
|
89
85
|
"""
|
|
90
86
|
raise NotImplementedError
|
limits/aio/storage/memcached.py
CHANGED
|
@@ -4,26 +4,33 @@ import time
|
|
|
4
4
|
import urllib.parse
|
|
5
5
|
from collections.abc import Iterable
|
|
6
6
|
from math import ceil, floor
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
|
-
from deprecated.sphinx import versionadded
|
|
9
|
+
from deprecated.sphinx import versionadded, versionchanged
|
|
9
10
|
|
|
10
11
|
from limits.aio.storage.base import SlidingWindowCounterSupport, Storage
|
|
11
12
|
from limits.storage.base import TimestampedSlidingWindow
|
|
12
|
-
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import memcachio
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
@versionadded(version="2.1")
|
|
19
|
+
@versionchanged(
|
|
20
|
+
version="5.0",
|
|
21
|
+
reason="Switched to :pypi:`memcachio` for async memcached support",
|
|
22
|
+
)
|
|
16
23
|
class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingWindow):
|
|
17
24
|
"""
|
|
18
25
|
Rate limit storage with memcached as backend.
|
|
19
26
|
|
|
20
|
-
Depends on :pypi:`
|
|
27
|
+
Depends on :pypi:`memcachio`
|
|
21
28
|
"""
|
|
22
29
|
|
|
23
30
|
STORAGE_SCHEME = ["async+memcached"]
|
|
24
31
|
"""The storage scheme for memcached to be used in an async context"""
|
|
25
32
|
|
|
26
|
-
DEPENDENCIES = ["
|
|
33
|
+
DEPENDENCIES = ["memcachio"]
|
|
27
34
|
|
|
28
35
|
def __init__(
|
|
29
36
|
self,
|
|
@@ -37,8 +44,8 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
37
44
|
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
38
45
|
:exc:`limits.errors.StorageError` before raising it.
|
|
39
46
|
:param options: all remaining keyword arguments are passed
|
|
40
|
-
directly to the constructor of :class:`
|
|
41
|
-
:raise ConfigurationError: when :pypi:`
|
|
47
|
+
directly to the constructor of :class:`memcachio.Client`
|
|
48
|
+
:raise ConfigurationError: when :pypi:`memcachio` is not available
|
|
42
49
|
"""
|
|
43
50
|
parsed = urllib.parse.urlparse(uri)
|
|
44
51
|
self.hosts = []
|
|
@@ -51,21 +58,21 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
51
58
|
self._options = options
|
|
52
59
|
self._storage = None
|
|
53
60
|
super().__init__(uri, wrap_exceptions=wrap_exceptions, **options)
|
|
54
|
-
self.dependency = self.dependencies["
|
|
61
|
+
self.dependency = self.dependencies["memcachio"].module
|
|
55
62
|
|
|
56
63
|
@property
|
|
57
64
|
def base_exceptions(
|
|
58
65
|
self,
|
|
59
66
|
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
60
67
|
return (
|
|
61
|
-
self.dependency.
|
|
62
|
-
self.dependency.
|
|
68
|
+
self.dependency.errors.NoNodeAvailable,
|
|
69
|
+
self.dependency.errors.MemcachioConnectionError,
|
|
63
70
|
)
|
|
64
71
|
|
|
65
|
-
async def get_storage(self) ->
|
|
72
|
+
async def get_storage(self) -> memcachio.Client[bytes]:
|
|
66
73
|
if not self._storage:
|
|
67
|
-
self._storage =
|
|
68
|
-
[
|
|
74
|
+
self._storage = self.dependency.Client(
|
|
75
|
+
[(h, p) for h, p in self.hosts],
|
|
69
76
|
**self._options,
|
|
70
77
|
)
|
|
71
78
|
assert self._storage
|
|
@@ -75,19 +82,18 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
75
82
|
"""
|
|
76
83
|
:param key: the key to get the counter value for
|
|
77
84
|
"""
|
|
78
|
-
item =
|
|
79
|
-
|
|
85
|
+
item = (await self.get_many([key])).get(key.encode("utf-8"), None)
|
|
80
86
|
return item and int(item.value) or 0
|
|
81
87
|
|
|
82
|
-
async def get_many(
|
|
88
|
+
async def get_many(
|
|
89
|
+
self, keys: Iterable[str]
|
|
90
|
+
) -> dict[bytes, memcachio.MemcachedItem[bytes]]:
|
|
83
91
|
"""
|
|
84
92
|
Return multiple counters at once
|
|
85
93
|
|
|
86
94
|
:param keys: the keys to get the counter values for
|
|
87
95
|
"""
|
|
88
|
-
return await (await self.get_storage()).
|
|
89
|
-
[k.encode("utf-8") for k in keys]
|
|
90
|
-
)
|
|
96
|
+
return await (await self.get_storage()).get(*[k.encode("utf-8") for k in keys])
|
|
91
97
|
|
|
92
98
|
async def clear(self, key: str) -> None:
|
|
93
99
|
"""
|
|
@@ -107,17 +113,12 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
107
113
|
"""
|
|
108
114
|
storage = await self.get_storage()
|
|
109
115
|
limit_key = key.encode("utf-8")
|
|
110
|
-
|
|
111
|
-
value = await storage.decrement(limit_key, amount, noreply=noreply) or 0
|
|
112
|
-
except self.dependency.NotFoundCommandError:
|
|
113
|
-
value = 0
|
|
114
|
-
return value
|
|
116
|
+
return await storage.decr(limit_key, amount, noreply=noreply) or 0
|
|
115
117
|
|
|
116
118
|
async def incr(
|
|
117
119
|
self,
|
|
118
120
|
key: str,
|
|
119
121
|
expiry: float,
|
|
120
|
-
elastic_expiry: bool = False,
|
|
121
122
|
amount: int = 1,
|
|
122
123
|
set_expiration_key: bool = True,
|
|
123
124
|
) -> int:
|
|
@@ -126,7 +127,6 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
126
127
|
|
|
127
128
|
:param key: the key to increment
|
|
128
129
|
:param expiry: amount in seconds for the key to expire in
|
|
129
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
130
130
|
window every hit.
|
|
131
131
|
:param amount: the number to increment by
|
|
132
132
|
:param set_expiration_key: if set to False, the expiration time won't be stored but the key will still expire
|
|
@@ -134,53 +134,29 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
134
134
|
storage = await self.get_storage()
|
|
135
135
|
limit_key = key.encode("utf-8")
|
|
136
136
|
expire_key = self._expiration_key(key).encode()
|
|
137
|
-
value
|
|
138
|
-
try:
|
|
139
|
-
value = await storage.increment(limit_key, amount) or amount
|
|
140
|
-
if elastic_expiry:
|
|
141
|
-
await storage.touch(limit_key, exptime=ceil(expiry))
|
|
142
|
-
if set_expiration_key:
|
|
143
|
-
await storage.set(
|
|
144
|
-
expire_key,
|
|
145
|
-
str(expiry + time.time()).encode("utf-8"),
|
|
146
|
-
exptime=ceil(expiry),
|
|
147
|
-
noreply=False,
|
|
148
|
-
)
|
|
149
|
-
return value
|
|
150
|
-
except self.dependency.NotFoundCommandError:
|
|
151
|
-
# Incrementation failed because the key doesn't exist
|
|
137
|
+
if (value := (await storage.incr(limit_key, amount))) is None:
|
|
152
138
|
storage = await self.get_storage()
|
|
153
|
-
|
|
154
|
-
await storage.add(limit_key, f"{amount}".encode(), exptime=ceil(expiry))
|
|
139
|
+
if await storage.add(limit_key, f"{amount}".encode(), expiry=ceil(expiry)):
|
|
155
140
|
if set_expiration_key:
|
|
156
141
|
await storage.set(
|
|
157
142
|
expire_key,
|
|
158
143
|
str(expiry + time.time()).encode("utf-8"),
|
|
159
|
-
|
|
144
|
+
expiry=ceil(expiry),
|
|
160
145
|
noreply=False,
|
|
161
146
|
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# Coult not add the key, probably because a concurrent call has added it
|
|
147
|
+
return amount
|
|
148
|
+
else:
|
|
165
149
|
storage = await self.get_storage()
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
await storage.touch(limit_key, exptime=ceil(expiry))
|
|
169
|
-
if set_expiration_key:
|
|
170
|
-
await storage.set(
|
|
171
|
-
expire_key,
|
|
172
|
-
str(expiry + time.time()).encode("utf-8"),
|
|
173
|
-
exptime=ceil(expiry),
|
|
174
|
-
noreply=False,
|
|
175
|
-
)
|
|
176
|
-
return value
|
|
150
|
+
return await storage.incr(limit_key, amount) or amount
|
|
151
|
+
return value
|
|
177
152
|
|
|
178
153
|
async def get_expiry(self, key: str) -> float:
|
|
179
154
|
"""
|
|
180
155
|
:param key: the key to get the expiry for
|
|
181
156
|
"""
|
|
182
157
|
storage = await self.get_storage()
|
|
183
|
-
|
|
158
|
+
expiration_key = self._expiration_key(key).encode("utf-8")
|
|
159
|
+
item = (await storage.get(expiration_key)).get(expiration_key, None)
|
|
184
160
|
|
|
185
161
|
return item and float(item.value) or time.time()
|
|
186
162
|
|
limits/aio/storage/memory.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import bisect
|
|
4
5
|
import time
|
|
5
6
|
from collections import Counter, defaultdict
|
|
6
7
|
from math import floor
|
|
@@ -28,7 +29,7 @@ class MemoryStorage(
|
|
|
28
29
|
):
|
|
29
30
|
"""
|
|
30
31
|
rate limit storage using :class:`collections.Counter`
|
|
31
|
-
as an in memory storage for fixed
|
|
32
|
+
as an in memory storage for fixed & sliding window strategies,
|
|
32
33
|
and a simple list to implement moving window strategy.
|
|
33
34
|
"""
|
|
34
35
|
|
|
@@ -61,11 +62,16 @@ class MemoryStorage(
|
|
|
61
62
|
asyncio.ensure_future(self.__schedule_expiry())
|
|
62
63
|
|
|
63
64
|
async def __expire_events(self) -> None:
|
|
65
|
+
now = time.time()
|
|
64
66
|
for key in list(self.events.keys()):
|
|
67
|
+
cutoff = await asyncio.to_thread(
|
|
68
|
+
lambda evts: bisect.bisect_left(
|
|
69
|
+
evts, -now, key=lambda event: -event.expiry
|
|
70
|
+
),
|
|
71
|
+
self.events[key],
|
|
72
|
+
)
|
|
65
73
|
async with self.locks[key]:
|
|
66
|
-
|
|
67
|
-
if event.expiry <= time.time() and event in self.events[key]:
|
|
68
|
-
self.events[key].remove(event)
|
|
74
|
+
self.events[key] = self.events[key][:cutoff]
|
|
69
75
|
if not self.events.get(key, None):
|
|
70
76
|
self.events.pop(key, None)
|
|
71
77
|
self.locks.pop(key, None)
|
|
@@ -86,26 +92,20 @@ class MemoryStorage(
|
|
|
86
92
|
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
87
93
|
return ValueError
|
|
88
94
|
|
|
89
|
-
async def incr(
|
|
90
|
-
self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
|
|
91
|
-
) -> int:
|
|
95
|
+
async def incr(self, key: str, expiry: float, amount: int = 1) -> int:
|
|
92
96
|
"""
|
|
93
97
|
increments the counter for a given rate limit key
|
|
94
98
|
|
|
95
99
|
:param key: the key to increment
|
|
96
100
|
:param expiry: amount in seconds for the key to expire in
|
|
97
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
98
|
-
window every hit.
|
|
99
101
|
:param amount: the number to increment by
|
|
100
102
|
"""
|
|
101
103
|
await self.get(key)
|
|
102
104
|
await self.__schedule_expiry()
|
|
103
105
|
async with self.locks[key]:
|
|
104
106
|
self.storage[key] += amount
|
|
105
|
-
|
|
106
|
-
if elastic_expiry or self.storage[key] == amount:
|
|
107
|
+
if self.storage[key] == amount:
|
|
107
108
|
self.expirations[key] = time.time() + expiry
|
|
108
|
-
|
|
109
109
|
return self.storage.get(key, amount)
|
|
110
110
|
|
|
111
111
|
async def decr(self, key: str, amount: int = 1) -> int:
|
|
@@ -165,8 +165,7 @@ class MemoryStorage(
|
|
|
165
165
|
if entry and entry.atime >= timestamp - expiry:
|
|
166
166
|
return False
|
|
167
167
|
else:
|
|
168
|
-
self.events[key][:0] = [Entry(expiry)
|
|
169
|
-
|
|
168
|
+
self.events[key][:0] = [Entry(expiry)] * amount
|
|
170
169
|
return True
|
|
171
170
|
|
|
172
171
|
async def get_expiry(self, key: str) -> float:
|
|
@@ -176,22 +175,6 @@ class MemoryStorage(
|
|
|
176
175
|
|
|
177
176
|
return self.expirations.get(key, time.time())
|
|
178
177
|
|
|
179
|
-
async def get_num_acquired(self, key: str, expiry: int) -> int:
|
|
180
|
-
"""
|
|
181
|
-
returns the number of entries already acquired
|
|
182
|
-
|
|
183
|
-
:param key: rate limit key to acquire an entry in
|
|
184
|
-
:param expiry: expiry of the entry
|
|
185
|
-
"""
|
|
186
|
-
timestamp = time.time()
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
len([k for k in self.events.get(key, []) if k.atime >= timestamp - expiry])
|
|
190
|
-
if self.events.get(key)
|
|
191
|
-
else 0
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
# FIXME: arg limit is not used
|
|
195
178
|
async def get_moving_window(
|
|
196
179
|
self, key: str, limit: int, expiry: int
|
|
197
180
|
) -> tuple[float, int]:
|
|
@@ -203,14 +186,14 @@ class MemoryStorage(
|
|
|
203
186
|
:param expiry: expiry of entry
|
|
204
187
|
:return: (start of window, number of acquired entries)
|
|
205
188
|
"""
|
|
206
|
-
timestamp = time.time()
|
|
207
|
-
acquired = await self.get_num_acquired(key, expiry)
|
|
208
189
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
190
|
+
timestamp = time.time()
|
|
191
|
+
if events := self.events.get(key, []):
|
|
192
|
+
oldest = bisect.bisect_left(
|
|
193
|
+
events, -(timestamp - expiry), key=lambda entry: -entry.atime
|
|
194
|
+
)
|
|
195
|
+
return events[oldest - 1].atime, oldest
|
|
196
|
+
return timestamp, 0
|
|
214
197
|
|
|
215
198
|
async def acquire_sliding_window_entry(
|
|
216
199
|
self,
|
|
@@ -242,7 +225,6 @@ class MemoryStorage(
|
|
|
242
225
|
# Limitation: during high concurrency at the end of the window,
|
|
243
226
|
# the counter is shifted and cannot be decremented, so less requests than expected are allowed.
|
|
244
227
|
await self.decr(current_key, amount)
|
|
245
|
-
# print("Concurrent call, reverting the counter increase")
|
|
246
228
|
return False
|
|
247
229
|
return True
|
|
248
230
|
|
limits/aio/storage/mongodb.py
CHANGED
|
@@ -169,16 +169,12 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
169
169
|
|
|
170
170
|
return counter and counter["count"] or 0
|
|
171
171
|
|
|
172
|
-
async def incr(
|
|
173
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
174
|
-
) -> int:
|
|
172
|
+
async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
175
173
|
"""
|
|
176
174
|
increments the counter for a given rate limit key
|
|
177
175
|
|
|
178
176
|
:param key: the key to increment
|
|
179
177
|
:param expiry: amount in seconds for the key to expire in
|
|
180
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
181
|
-
window every hit.
|
|
182
178
|
:param amount: the number to increment by
|
|
183
179
|
"""
|
|
184
180
|
await self.create_indices()
|
|
@@ -205,7 +201,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
205
201
|
"$cond": {
|
|
206
202
|
"if": {"$lt": ["$expireAt", "$$NOW"]},
|
|
207
203
|
"then": expiration,
|
|
208
|
-
"else":
|
|
204
|
+
"else": "$expireAt",
|
|
209
205
|
}
|
|
210
206
|
},
|
|
211
207
|
}
|
|
@@ -241,15 +237,16 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
241
237
|
:param int expiry: expiry of entry
|
|
242
238
|
:return: (start of window, number of acquired entries)
|
|
243
239
|
"""
|
|
240
|
+
|
|
244
241
|
timestamp = time.time()
|
|
245
|
-
if
|
|
246
|
-
await self.database[self.__collection_mapping["windows"]]
|
|
242
|
+
if (
|
|
243
|
+
result := await self.database[self.__collection_mapping["windows"]]
|
|
247
244
|
.aggregate(
|
|
248
245
|
[
|
|
249
246
|
{"$match": {"_id": key}},
|
|
250
247
|
{
|
|
251
248
|
"$project": {
|
|
252
|
-
"
|
|
249
|
+
"filteredEntries": {
|
|
253
250
|
"$filter": {
|
|
254
251
|
"input": "$entries",
|
|
255
252
|
"as": "entry",
|
|
@@ -258,12 +255,10 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
258
255
|
}
|
|
259
256
|
}
|
|
260
257
|
},
|
|
261
|
-
{"$unwind": "$entries"},
|
|
262
258
|
{
|
|
263
|
-
"$
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"count": {"$sum": 1},
|
|
259
|
+
"$project": {
|
|
260
|
+
"min": {"$min": "$filteredEntries"},
|
|
261
|
+
"count": {"$size": "$filteredEntries"},
|
|
267
262
|
}
|
|
268
263
|
},
|
|
269
264
|
]
|
|
@@ -337,7 +332,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
337
332
|
"$cond": {
|
|
338
333
|
"if": {
|
|
339
334
|
"$lte": [
|
|
340
|
-
{"$subtract": ["$
|
|
335
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
341
336
|
expiry_ms,
|
|
342
337
|
]
|
|
343
338
|
},
|
|
@@ -353,7 +348,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
353
348
|
"$cond": {
|
|
354
349
|
"if": {
|
|
355
350
|
"$lte": [
|
|
356
|
-
{"$subtract": ["$
|
|
351
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
357
352
|
expiry_ms,
|
|
358
353
|
]
|
|
359
354
|
},
|
|
@@ -361,22 +356,22 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
361
356
|
"else": {"$ifNull": ["$currentCount", 0]},
|
|
362
357
|
}
|
|
363
358
|
},
|
|
364
|
-
"
|
|
359
|
+
"expireAt": {
|
|
365
360
|
"$cond": {
|
|
366
361
|
"if": {
|
|
367
362
|
"$lte": [
|
|
368
|
-
{"$subtract": ["$
|
|
363
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
369
364
|
expiry_ms,
|
|
370
365
|
]
|
|
371
366
|
},
|
|
372
367
|
"then": {
|
|
373
368
|
"$cond": {
|
|
374
|
-
"if": {"$gt": ["$
|
|
375
|
-
"then": {"$add": ["$
|
|
369
|
+
"if": {"$gt": ["$expireAt", 0]},
|
|
370
|
+
"then": {"$add": ["$expireAt", expiry_ms]},
|
|
376
371
|
"else": {"$add": ["$$NOW", 2 * expiry_ms]},
|
|
377
372
|
}
|
|
378
373
|
},
|
|
379
|
-
"else": "$
|
|
374
|
+
"else": "$expireAt",
|
|
380
375
|
}
|
|
381
376
|
},
|
|
382
377
|
}
|
|
@@ -396,7 +391,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
396
391
|
0,
|
|
397
392
|
{
|
|
398
393
|
"$subtract": [
|
|
399
|
-
"$
|
|
394
|
+
"$expireAt",
|
|
400
395
|
{
|
|
401
396
|
"$add": [
|
|
402
397
|
"$$NOW",
|
|
@@ -464,7 +459,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
464
459
|
"$cond": {
|
|
465
460
|
"if": {
|
|
466
461
|
"$lte": [
|
|
467
|
-
{"$subtract": ["$
|
|
462
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
468
463
|
expiry_ms,
|
|
469
464
|
]
|
|
470
465
|
},
|
|
@@ -476,7 +471,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
476
471
|
"$cond": {
|
|
477
472
|
"if": {
|
|
478
473
|
"$lte": [
|
|
479
|
-
{"$subtract": ["$
|
|
474
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
480
475
|
expiry_ms,
|
|
481
476
|
]
|
|
482
477
|
},
|
|
@@ -484,27 +479,27 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
484
479
|
"else": {"$ifNull": ["$currentCount", 0]},
|
|
485
480
|
}
|
|
486
481
|
},
|
|
487
|
-
"
|
|
482
|
+
"expireAt": {
|
|
488
483
|
"$cond": {
|
|
489
484
|
"if": {
|
|
490
485
|
"$lte": [
|
|
491
|
-
{"$subtract": ["$
|
|
486
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
492
487
|
expiry_ms,
|
|
493
488
|
]
|
|
494
489
|
},
|
|
495
|
-
"then": {"$add": ["$
|
|
496
|
-
"else": "$
|
|
490
|
+
"then": {"$add": ["$expireAt", expiry_ms]},
|
|
491
|
+
"else": "$expireAt",
|
|
497
492
|
}
|
|
498
493
|
},
|
|
499
494
|
}
|
|
500
495
|
}
|
|
501
496
|
],
|
|
502
497
|
return_document=self.proxy_dependency.module.ReturnDocument.AFTER,
|
|
503
|
-
projection=["currentCount", "previousCount", "
|
|
498
|
+
projection=["currentCount", "previousCount", "expireAt"],
|
|
504
499
|
):
|
|
505
500
|
expires_at = (
|
|
506
|
-
(result["
|
|
507
|
-
if result.get("
|
|
501
|
+
(result["expireAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
502
|
+
if result.get("expireAt")
|
|
508
503
|
else time.time()
|
|
509
504
|
)
|
|
510
505
|
current_ttl = max(0, expires_at - time.time())
|
|
@@ -139,9 +139,7 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
139
139
|
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
140
140
|
return self.bridge.base_exceptions
|
|
141
141
|
|
|
142
|
-
async def incr(
|
|
143
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
144
|
-
) -> int:
|
|
142
|
+
async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
145
143
|
"""
|
|
146
144
|
increments the counter for a given rate limit key
|
|
147
145
|
|
|
@@ -150,7 +148,7 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
150
148
|
:param amount: the number to increment by
|
|
151
149
|
"""
|
|
152
150
|
|
|
153
|
-
return await self.bridge.incr(key, expiry,
|
|
151
|
+
return await self.bridge.incr(key, expiry, amount)
|
|
154
152
|
|
|
155
153
|
async def get(self, key: str) -> int:
|
|
156
154
|
"""
|
|
@@ -112,14 +112,10 @@ class CoredisBridge(RedisBridge):
|
|
|
112
112
|
self.SCRIPT_ACQUIRE_SLIDING_WINDOW
|
|
113
113
|
)
|
|
114
114
|
|
|
115
|
-
async def incr(
|
|
116
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
117
|
-
) -> int:
|
|
115
|
+
async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
118
116
|
key = self.prefixed_key(key)
|
|
119
|
-
value
|
|
120
|
-
if elastic_expiry or value == amount:
|
|
117
|
+
if (value := await self.get_connection().incrby(key, amount)) == amount:
|
|
121
118
|
await self.get_connection().expire(key, expiry)
|
|
122
|
-
|
|
123
119
|
return value
|
|
124
120
|
|
|
125
121
|
async def get(self, key: str) -> int:
|
|
@@ -119,7 +119,6 @@ class RedispyBridge(RedisBridge):
|
|
|
119
119
|
self,
|
|
120
120
|
key: str,
|
|
121
121
|
expiry: int,
|
|
122
|
-
elastic_expiry: bool = False,
|
|
123
122
|
amount: int = 1,
|
|
124
123
|
) -> int:
|
|
125
124
|
"""
|
|
@@ -131,13 +130,7 @@ class RedispyBridge(RedisBridge):
|
|
|
131
130
|
:param amount: the number to increment by
|
|
132
131
|
"""
|
|
133
132
|
key = self.prefixed_key(key)
|
|
134
|
-
|
|
135
|
-
if elastic_expiry:
|
|
136
|
-
value = await self.get_connection().incrby(key, amount)
|
|
137
|
-
await self.get_connection().expire(key, expiry)
|
|
138
|
-
return value
|
|
139
|
-
else:
|
|
140
|
-
return cast(int, await self.lua_incr_expire([key], [expiry, amount]))
|
|
133
|
+
return cast(int, await self.lua_incr_expire([key], [expiry, amount]))
|
|
141
134
|
|
|
142
135
|
async def get(self, key: str) -> int:
|
|
143
136
|
"""
|
limits/aio/strategies.py
CHANGED
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from math import floor, inf
|
|
10
10
|
|
|
11
|
-
from deprecated.sphinx import
|
|
11
|
+
from deprecated.sphinx import versionadded
|
|
12
12
|
|
|
13
13
|
from ..limits import RateLimitItem
|
|
14
14
|
from ..storage import StorageTypes
|
|
@@ -150,7 +150,6 @@ class FixedWindowRateLimiter(RateLimiter):
|
|
|
150
150
|
await self.storage.incr(
|
|
151
151
|
item.key_for(*identifiers),
|
|
152
152
|
item.get_expiry(),
|
|
153
|
-
elastic_expiry=False,
|
|
154
153
|
amount=cost,
|
|
155
154
|
)
|
|
156
155
|
<= item.amount
|
|
@@ -304,34 +303,8 @@ class SlidingWindowCounterRateLimiter(RateLimiter):
|
|
|
304
303
|
return WindowStats(now + min(previous_reset_in, current_reset_in), remaining)
|
|
305
304
|
|
|
306
305
|
|
|
307
|
-
@deprecated(version="4.1")
|
|
308
|
-
class FixedWindowElasticExpiryRateLimiter(FixedWindowRateLimiter):
|
|
309
|
-
"""
|
|
310
|
-
Reference: :ref:`strategies:fixed window with elastic expiry`
|
|
311
|
-
"""
|
|
312
|
-
|
|
313
|
-
async def hit(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
314
|
-
"""
|
|
315
|
-
Consume the rate limit
|
|
316
|
-
|
|
317
|
-
:param item: a :class:`limits.limits.RateLimitItem` instance
|
|
318
|
-
:param identifiers: variable list of strings to uniquely identify the
|
|
319
|
-
limit
|
|
320
|
-
:param cost: The cost of this hit, default 1
|
|
321
|
-
"""
|
|
322
|
-
amount = await self.storage.incr(
|
|
323
|
-
item.key_for(*identifiers),
|
|
324
|
-
item.get_expiry(),
|
|
325
|
-
elastic_expiry=True,
|
|
326
|
-
amount=cost,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
return amount <= item.amount
|
|
330
|
-
|
|
331
|
-
|
|
332
306
|
STRATEGIES = {
|
|
333
307
|
"sliding-window-counter": SlidingWindowCounterRateLimiter,
|
|
334
308
|
"fixed-window": FixedWindowRateLimiter,
|
|
335
|
-
"fixed-window-elastic-expiry": FixedWindowElasticExpiryRateLimiter,
|
|
336
309
|
"moving-window": MovingWindowRateLimiter,
|
|
337
310
|
}
|