limits 4.7.1__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.1.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.1.dist-info/RECORD +0 -43
- {limits-4.7.1.dist-info → limits-5.0.0rc1.dist-info}/WHEEL +0 -0
- {limits-4.7.1.dist-info → limits-5.0.0rc1.dist-info}/licenses/LICENSE.txt +0 -0
- {limits-4.7.1.dist-info → limits-5.0.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -12,11 +12,14 @@ local entry = redis.call('lindex', KEYS[1], limit - amount)
|
|
|
12
12
|
if entry and tonumber(entry) >= timestamp - expiry then
|
|
13
13
|
return false
|
|
14
14
|
end
|
|
15
|
-
|
|
15
|
+
local entries = {}
|
|
16
16
|
for i = 1, amount do
|
|
17
|
-
|
|
17
|
+
entries[i] = timestamp
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
for i=1,#entries,5000 do
|
|
21
|
+
redis.call('lpush', KEYS[1], unpack(entries, i, math.min(i+4999, #entries)))
|
|
22
|
+
end
|
|
20
23
|
redis.call('ltrim', KEYS[1], 0, limit - 1)
|
|
21
24
|
redis.call('expire', KEYS[1], expiry)
|
|
22
25
|
|
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
local
|
|
1
|
+
local len = tonumber(ARGV[2])
|
|
2
2
|
local expiry = tonumber(ARGV[1])
|
|
3
|
-
local a = 0
|
|
4
|
-
local oldest = nil
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
-- Binary search to find the oldest valid entry in the window
|
|
5
|
+
local function oldest_entry(high, target)
|
|
6
|
+
local low = 0
|
|
7
|
+
local result = nil
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
while low <= high do
|
|
10
|
+
local mid = math.floor((low + high) / 2)
|
|
11
|
+
local val = tonumber(redis.call('lindex', KEYS[1], mid))
|
|
12
|
+
|
|
13
|
+
if val and val >= target then
|
|
14
|
+
result = mid
|
|
15
|
+
low = mid + 1
|
|
16
|
+
else
|
|
17
|
+
high = mid - 1
|
|
13
18
|
end
|
|
14
|
-
else
|
|
15
|
-
break
|
|
16
19
|
end
|
|
20
|
+
|
|
21
|
+
return result
|
|
17
22
|
end
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
local index = oldest_entry(len - 1, expiry)
|
|
25
|
+
|
|
26
|
+
if index then
|
|
27
|
+
local count = index + 1
|
|
28
|
+
local oldest = tonumber(redis.call('lindex', KEYS[1], index))
|
|
29
|
+
return {tostring(oldest), count}
|
|
30
|
+
end
|
limits/storage/__init__.py
CHANGED
|
@@ -12,7 +12,6 @@ import limits # noqa
|
|
|
12
12
|
from ..errors import ConfigurationError
|
|
13
13
|
from ..typing import TypeAlias, cast
|
|
14
14
|
from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
|
|
15
|
-
from .etcd import EtcdStorage
|
|
16
15
|
from .memcached import MemcachedStorage
|
|
17
16
|
from .memory import MemoryStorage
|
|
18
17
|
from .mongodb import MongoDBStorage, MongoDBStorageBase
|
|
@@ -67,7 +66,6 @@ def storage_from_string(
|
|
|
67
66
|
|
|
68
67
|
|
|
69
68
|
__all__ = [
|
|
70
|
-
"EtcdStorage",
|
|
71
69
|
"MemcachedStorage",
|
|
72
70
|
"MemoryStorage",
|
|
73
71
|
"MongoDBStorage",
|
limits/storage/base.py
CHANGED
|
@@ -71,16 +71,12 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
71
71
|
raise NotImplementedError
|
|
72
72
|
|
|
73
73
|
@abstractmethod
|
|
74
|
-
def incr(
|
|
75
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
76
|
-
) -> int:
|
|
74
|
+
def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
77
75
|
"""
|
|
78
76
|
increments the counter for a given rate limit key
|
|
79
77
|
|
|
80
78
|
:param key: the key to increment
|
|
81
79
|
:param expiry: amount in seconds for the key to expire in
|
|
82
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
83
|
-
window every hit.
|
|
84
80
|
:param amount: the number to increment by
|
|
85
81
|
"""
|
|
86
82
|
raise NotImplementedError
|
limits/storage/memcached.py
CHANGED
|
@@ -166,7 +166,6 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
166
166
|
self,
|
|
167
167
|
key: str,
|
|
168
168
|
expiry: float,
|
|
169
|
-
elastic_expiry: bool = False,
|
|
170
169
|
amount: int = 1,
|
|
171
170
|
set_expiration_key: bool = True,
|
|
172
171
|
) -> int:
|
|
@@ -175,43 +174,21 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
175
174
|
|
|
176
175
|
:param key: the key to increment
|
|
177
176
|
:param expiry: amount in seconds for the key to expire in
|
|
178
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
179
177
|
window every hit.
|
|
180
178
|
:param amount: the number to increment by
|
|
181
179
|
:param set_expiration_key: set the expiration key with the expiration time if needed. If set to False, the key will still expire, but memcached cannot provide the expiration time.
|
|
182
180
|
"""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
self.call_memcached_func(
|
|
189
|
-
self.storage.set,
|
|
190
|
-
self._expiration_key(key),
|
|
191
|
-
expiry + time.time(),
|
|
192
|
-
expire=ceil(expiry),
|
|
193
|
-
noreply=False,
|
|
194
|
-
)
|
|
195
|
-
|
|
181
|
+
if (
|
|
182
|
+
value := self.call_memcached_func(
|
|
183
|
+
self.storage.incr, key, amount, noreply=False
|
|
184
|
+
)
|
|
185
|
+
) is not None:
|
|
196
186
|
return value
|
|
197
187
|
else:
|
|
198
188
|
if not self.call_memcached_func(
|
|
199
189
|
self.storage.add, key, amount, ceil(expiry), noreply=False
|
|
200
190
|
):
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if elastic_expiry:
|
|
204
|
-
self.call_memcached_func(self.storage.touch, key, ceil(expiry))
|
|
205
|
-
if set_expiration_key:
|
|
206
|
-
self.call_memcached_func(
|
|
207
|
-
self.storage.set,
|
|
208
|
-
self._expiration_key(key),
|
|
209
|
-
expiry + time.time(),
|
|
210
|
-
expire=ceil(expiry),
|
|
211
|
-
noreply=False,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
return value
|
|
191
|
+
return self.storage.incr(key, amount) or amount
|
|
215
192
|
else:
|
|
216
193
|
if set_expiration_key:
|
|
217
194
|
self.call_memcached_func(
|
limits/storage/memory.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import bisect
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
5
6
|
from collections import Counter, defaultdict
|
|
@@ -25,7 +26,7 @@ class MemoryStorage(
|
|
|
25
26
|
):
|
|
26
27
|
"""
|
|
27
28
|
rate limit storage using :class:`collections.Counter`
|
|
28
|
-
as an in memory storage for fixed and
|
|
29
|
+
as an in memory storage for fixed and sliding window strategies,
|
|
29
30
|
and a simple list to implement moving window strategy.
|
|
30
31
|
|
|
31
32
|
"""
|
|
@@ -56,9 +57,11 @@ class MemoryStorage(
|
|
|
56
57
|
def __expire_events(self) -> None:
|
|
57
58
|
for key in list(self.events.keys()):
|
|
58
59
|
with self.locks[key]:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
events = self.events.get(key, [])
|
|
61
|
+
oldest = bisect.bisect_left(
|
|
62
|
+
events, -time.time(), key=lambda event: -event.expiry
|
|
63
|
+
)
|
|
64
|
+
self.events[key] = self.events[key][:oldest]
|
|
62
65
|
if not self.events.get(key, None):
|
|
63
66
|
self.locks.pop(key, None)
|
|
64
67
|
for key in list(self.expirations.keys()):
|
|
@@ -78,26 +81,20 @@ class MemoryStorage(
|
|
|
78
81
|
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
79
82
|
return ValueError
|
|
80
83
|
|
|
81
|
-
def incr(
|
|
82
|
-
self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
|
|
83
|
-
) -> int:
|
|
84
|
+
def incr(self, key: str, expiry: float, amount: int = 1) -> int:
|
|
84
85
|
"""
|
|
85
86
|
increments the counter for a given rate limit key
|
|
86
87
|
|
|
87
88
|
:param key: the key to increment
|
|
88
89
|
:param expiry: amount in seconds for the key to expire in
|
|
89
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
90
|
-
window every hit.
|
|
91
90
|
:param amount: the number to increment by
|
|
92
91
|
"""
|
|
93
92
|
self.get(key)
|
|
94
93
|
self.__schedule_expiry()
|
|
95
94
|
with self.locks[key]:
|
|
96
95
|
self.storage[key] += amount
|
|
97
|
-
|
|
98
|
-
if elastic_expiry or self.storage[key] == amount:
|
|
96
|
+
if self.storage[key] == amount:
|
|
99
97
|
self.expirations[key] = time.time() + expiry
|
|
100
|
-
|
|
101
98
|
return self.storage.get(key, 0)
|
|
102
99
|
|
|
103
100
|
def decr(self, key: str, amount: int = 1) -> int:
|
|
@@ -157,7 +154,7 @@ class MemoryStorage(
|
|
|
157
154
|
if entry and entry.atime >= timestamp - expiry:
|
|
158
155
|
return False
|
|
159
156
|
else:
|
|
160
|
-
self.events[key][:0] = [Entry(expiry)
|
|
157
|
+
self.events[key][:0] = [Entry(expiry)] * amount
|
|
161
158
|
return True
|
|
162
159
|
|
|
163
160
|
def get_expiry(self, key: str) -> float:
|
|
@@ -167,21 +164,6 @@ class MemoryStorage(
|
|
|
167
164
|
|
|
168
165
|
return self.expirations.get(key, time.time())
|
|
169
166
|
|
|
170
|
-
def get_num_acquired(self, key: str, expiry: int) -> int:
|
|
171
|
-
"""
|
|
172
|
-
returns the number of entries already acquired
|
|
173
|
-
|
|
174
|
-
:param key: rate limit key to acquire an entry in
|
|
175
|
-
:param expiry: expiry of the entry
|
|
176
|
-
"""
|
|
177
|
-
timestamp = time.time()
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
len([k for k in self.events.get(key, []) if k.atime >= timestamp - expiry])
|
|
181
|
-
if self.events.get(key)
|
|
182
|
-
else 0
|
|
183
|
-
)
|
|
184
|
-
|
|
185
167
|
def get_moving_window(self, key: str, limit: int, expiry: int) -> tuple[float, int]:
|
|
186
168
|
"""
|
|
187
169
|
returns the starting point and the number of entries in the moving
|
|
@@ -192,13 +174,12 @@ class MemoryStorage(
|
|
|
192
174
|
:return: (start of window, number of acquired entries)
|
|
193
175
|
"""
|
|
194
176
|
timestamp = time.time()
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return timestamp, acquired
|
|
177
|
+
if events := self.events.get(key, []):
|
|
178
|
+
oldest = bisect.bisect_left(
|
|
179
|
+
events, -(timestamp - expiry), key=lambda entry: -entry.atime
|
|
180
|
+
)
|
|
181
|
+
return events[oldest - 1].atime, oldest
|
|
182
|
+
return timestamp, 0
|
|
202
183
|
|
|
203
184
|
def acquire_sliding_window_entry(
|
|
204
185
|
self,
|
limits/storage/mongodb.py
CHANGED
|
@@ -144,9 +144,7 @@ class MongoDBStorageBase(
|
|
|
144
144
|
|
|
145
145
|
return counter and counter["count"] or 0
|
|
146
146
|
|
|
147
|
-
def incr(
|
|
148
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
149
|
-
) -> int:
|
|
147
|
+
def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
150
148
|
"""
|
|
151
149
|
increments the counter for a given rate limit key
|
|
152
150
|
|
|
@@ -175,9 +173,7 @@ class MongoDBStorageBase(
|
|
|
175
173
|
"$cond": {
|
|
176
174
|
"if": {"$lt": ["$expireAt", "$$NOW"]},
|
|
177
175
|
"then": expiration,
|
|
178
|
-
"else":
|
|
179
|
-
expiration if elastic_expiry else "$expireAt"
|
|
180
|
-
),
|
|
176
|
+
"else": "$expireAt",
|
|
181
177
|
}
|
|
182
178
|
},
|
|
183
179
|
}
|
|
@@ -210,13 +206,13 @@ class MongoDBStorageBase(
|
|
|
210
206
|
:return: (start of window, number of acquired entries)
|
|
211
207
|
"""
|
|
212
208
|
timestamp = time.time()
|
|
213
|
-
result
|
|
209
|
+
if result := list(
|
|
214
210
|
self.windows.aggregate(
|
|
215
211
|
[
|
|
216
212
|
{"$match": {"_id": key}},
|
|
217
213
|
{
|
|
218
214
|
"$project": {
|
|
219
|
-
"
|
|
215
|
+
"filteredEntries": {
|
|
220
216
|
"$filter": {
|
|
221
217
|
"input": "$entries",
|
|
222
218
|
"as": "entry",
|
|
@@ -225,21 +221,16 @@ class MongoDBStorageBase(
|
|
|
225
221
|
}
|
|
226
222
|
}
|
|
227
223
|
},
|
|
228
|
-
{"$unwind": "$entries"},
|
|
229
224
|
{
|
|
230
|
-
"$
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
"count": {"$sum": 1},
|
|
225
|
+
"$project": {
|
|
226
|
+
"min": {"$min": "$filteredEntries"},
|
|
227
|
+
"count": {"$size": "$filteredEntries"},
|
|
234
228
|
}
|
|
235
229
|
},
|
|
236
230
|
]
|
|
237
231
|
)
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
if result:
|
|
232
|
+
):
|
|
241
233
|
return result[0]["min"], result[0]["count"]
|
|
242
|
-
|
|
243
234
|
return timestamp, 0
|
|
244
235
|
|
|
245
236
|
def acquire_entry(self, key: str, limit: int, expiry: int, amount: int = 1) -> bool:
|
|
@@ -299,7 +290,7 @@ class MongoDBStorageBase(
|
|
|
299
290
|
"$cond": {
|
|
300
291
|
"if": {
|
|
301
292
|
"$lte": [
|
|
302
|
-
{"$subtract": ["$
|
|
293
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
303
294
|
expiry_ms,
|
|
304
295
|
]
|
|
305
296
|
},
|
|
@@ -311,7 +302,7 @@ class MongoDBStorageBase(
|
|
|
311
302
|
"$cond": {
|
|
312
303
|
"if": {
|
|
313
304
|
"$lte": [
|
|
314
|
-
{"$subtract": ["$
|
|
305
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
315
306
|
expiry_ms,
|
|
316
307
|
]
|
|
317
308
|
},
|
|
@@ -319,29 +310,29 @@ class MongoDBStorageBase(
|
|
|
319
310
|
"else": {"$ifNull": ["$currentCount", 0]},
|
|
320
311
|
}
|
|
321
312
|
},
|
|
322
|
-
"
|
|
313
|
+
"expireAt": {
|
|
323
314
|
"$cond": {
|
|
324
315
|
"if": {
|
|
325
316
|
"$lte": [
|
|
326
|
-
{"$subtract": ["$
|
|
317
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
327
318
|
expiry_ms,
|
|
328
319
|
]
|
|
329
320
|
},
|
|
330
321
|
"then": {
|
|
331
|
-
"$add": ["$
|
|
322
|
+
"$add": ["$expireAt", expiry_ms],
|
|
332
323
|
},
|
|
333
|
-
"else": "$
|
|
324
|
+
"else": "$expireAt",
|
|
334
325
|
}
|
|
335
326
|
},
|
|
336
327
|
}
|
|
337
328
|
}
|
|
338
329
|
],
|
|
339
330
|
return_document=self.lib.ReturnDocument.AFTER,
|
|
340
|
-
projection=["currentCount", "previousCount", "
|
|
331
|
+
projection=["currentCount", "previousCount", "expireAt"],
|
|
341
332
|
):
|
|
342
333
|
expires_at = (
|
|
343
|
-
(result["
|
|
344
|
-
if result.get("
|
|
334
|
+
(result["expireAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
335
|
+
if result.get("expireAt")
|
|
345
336
|
else time.time()
|
|
346
337
|
)
|
|
347
338
|
current_ttl = max(0, expires_at - time.time())
|
|
@@ -368,7 +359,7 @@ class MongoDBStorageBase(
|
|
|
368
359
|
"$cond": {
|
|
369
360
|
"if": {
|
|
370
361
|
"$lte": [
|
|
371
|
-
{"$subtract": ["$
|
|
362
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
372
363
|
expiry_ms,
|
|
373
364
|
]
|
|
374
365
|
},
|
|
@@ -384,7 +375,7 @@ class MongoDBStorageBase(
|
|
|
384
375
|
"$cond": {
|
|
385
376
|
"if": {
|
|
386
377
|
"$lte": [
|
|
387
|
-
{"$subtract": ["$
|
|
378
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
388
379
|
expiry_ms,
|
|
389
380
|
]
|
|
390
381
|
},
|
|
@@ -392,22 +383,22 @@ class MongoDBStorageBase(
|
|
|
392
383
|
"else": {"$ifNull": ["$currentCount", 0]},
|
|
393
384
|
}
|
|
394
385
|
},
|
|
395
|
-
"
|
|
386
|
+
"expireAt": {
|
|
396
387
|
"$cond": {
|
|
397
388
|
"if": {
|
|
398
389
|
"$lte": [
|
|
399
|
-
{"$subtract": ["$
|
|
390
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
400
391
|
expiry_ms,
|
|
401
392
|
]
|
|
402
393
|
},
|
|
403
394
|
"then": {
|
|
404
395
|
"$cond": {
|
|
405
|
-
"if": {"$gt": ["$
|
|
406
|
-
"then": {"$add": ["$
|
|
396
|
+
"if": {"$gt": ["$expireAt", 0]},
|
|
397
|
+
"then": {"$add": ["$expireAt", expiry_ms]},
|
|
407
398
|
"else": {"$add": ["$$NOW", 2 * expiry_ms]},
|
|
408
399
|
}
|
|
409
400
|
},
|
|
410
|
-
"else": "$
|
|
401
|
+
"else": "$expireAt",
|
|
411
402
|
}
|
|
412
403
|
},
|
|
413
404
|
}
|
|
@@ -427,7 +418,7 @@ class MongoDBStorageBase(
|
|
|
427
418
|
0,
|
|
428
419
|
{
|
|
429
420
|
"$subtract": [
|
|
430
|
-
"$
|
|
421
|
+
"$expireAt",
|
|
431
422
|
{
|
|
432
423
|
"$add": [
|
|
433
424
|
"$$NOW",
|
limits/storage/redis.py
CHANGED
|
@@ -201,7 +201,6 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
201
201
|
self,
|
|
202
202
|
key: str,
|
|
203
203
|
expiry: int,
|
|
204
|
-
elastic_expiry: bool = False,
|
|
205
204
|
amount: int = 1,
|
|
206
205
|
) -> int:
|
|
207
206
|
"""
|
|
@@ -213,12 +212,7 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
213
212
|
:param amount: the number to increment by
|
|
214
213
|
"""
|
|
215
214
|
key = self.prefixed_key(key)
|
|
216
|
-
|
|
217
|
-
value = self.get_connection().incrby(key, amount)
|
|
218
|
-
self.get_connection().expire(key, expiry)
|
|
219
|
-
return value
|
|
220
|
-
else:
|
|
221
|
-
return int(self.lua_incr_expire([key], [expiry, amount]))
|
|
215
|
+
return int(self.lua_incr_expire([key], [expiry, amount]))
|
|
222
216
|
|
|
223
217
|
def get(self, key: str) -> int:
|
|
224
218
|
"""
|
limits/strategies.py
CHANGED
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
from abc import ABCMeta, 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.storage.base import SlidingWindowCounterSupport
|
|
14
14
|
|
|
@@ -148,7 +148,6 @@ class FixedWindowRateLimiter(RateLimiter):
|
|
|
148
148
|
self.storage.incr(
|
|
149
149
|
item.key_for(*identifiers),
|
|
150
150
|
item.get_expiry(),
|
|
151
|
-
elastic_expiry=False,
|
|
152
151
|
amount=cost,
|
|
153
152
|
)
|
|
154
153
|
<= item.amount
|
|
@@ -286,43 +285,14 @@ class SlidingWindowCounterRateLimiter(RateLimiter):
|
|
|
286
285
|
return WindowStats(now + min(previous_reset_in, current_reset_in), remaining)
|
|
287
286
|
|
|
288
287
|
|
|
289
|
-
@deprecated(version="4.1", action="always")
|
|
290
|
-
class FixedWindowElasticExpiryRateLimiter(FixedWindowRateLimiter):
|
|
291
|
-
"""
|
|
292
|
-
Reference: :ref:`strategies:fixed window with elastic expiry`
|
|
293
|
-
"""
|
|
294
|
-
|
|
295
|
-
def hit(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
296
|
-
"""
|
|
297
|
-
Consume the rate limit
|
|
298
|
-
|
|
299
|
-
:param item: The rate limit item
|
|
300
|
-
:param identifiers: variable list of strings to uniquely identify this
|
|
301
|
-
instance of the limit
|
|
302
|
-
:param cost: The cost of this hit, default 1
|
|
303
|
-
"""
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
self.storage.incr(
|
|
307
|
-
item.key_for(*identifiers),
|
|
308
|
-
item.get_expiry(),
|
|
309
|
-
elastic_expiry=True,
|
|
310
|
-
amount=cost,
|
|
311
|
-
)
|
|
312
|
-
<= item.amount
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
|
|
316
288
|
KnownStrategy = (
|
|
317
289
|
type[SlidingWindowCounterRateLimiter]
|
|
318
290
|
| type[FixedWindowRateLimiter]
|
|
319
|
-
| type[FixedWindowElasticExpiryRateLimiter]
|
|
320
291
|
| type[MovingWindowRateLimiter]
|
|
321
292
|
)
|
|
322
293
|
|
|
323
294
|
STRATEGIES: dict[str, KnownStrategy] = {
|
|
324
295
|
"sliding-window-counter": SlidingWindowCounterRateLimiter,
|
|
325
296
|
"fixed-window": FixedWindowRateLimiter,
|
|
326
|
-
"fixed-window-elastic-expiry": FixedWindowElasticExpiryRateLimiter,
|
|
327
297
|
"moving-window": MovingWindowRateLimiter,
|
|
328
298
|
}
|
limits/typing.py
CHANGED
|
@@ -30,54 +30,6 @@ if TYPE_CHECKING:
|
|
|
30
30
|
import redis
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class ItemP(Protocol):
|
|
34
|
-
value: bytes
|
|
35
|
-
flags: int | None
|
|
36
|
-
cas: int | None
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class EmcacheClientP(Protocol):
|
|
40
|
-
async def add(
|
|
41
|
-
self,
|
|
42
|
-
key: bytes,
|
|
43
|
-
value: bytes,
|
|
44
|
-
*,
|
|
45
|
-
flags: int = 0,
|
|
46
|
-
exptime: int = 0,
|
|
47
|
-
noreply: bool = False,
|
|
48
|
-
) -> None: ...
|
|
49
|
-
|
|
50
|
-
async def get(self, key: bytes, return_flags: bool = False) -> ItemP | None: ...
|
|
51
|
-
|
|
52
|
-
async def get_many(self, keys: Iterable[bytes]) -> dict[bytes, ItemP]: ...
|
|
53
|
-
|
|
54
|
-
async def gets(self, key: bytes, return_flags: bool = False) -> ItemP | None: ...
|
|
55
|
-
|
|
56
|
-
async def increment(
|
|
57
|
-
self, key: bytes, value: int, *, noreply: bool = False
|
|
58
|
-
) -> int | None: ...
|
|
59
|
-
|
|
60
|
-
async def decrement(
|
|
61
|
-
self, key: bytes, value: int, *, noreply: bool = False
|
|
62
|
-
) -> int | None: ...
|
|
63
|
-
|
|
64
|
-
async def delete(self, key: bytes, *, noreply: bool = False) -> None: ...
|
|
65
|
-
|
|
66
|
-
async def set(
|
|
67
|
-
self,
|
|
68
|
-
key: bytes,
|
|
69
|
-
value: bytes,
|
|
70
|
-
*,
|
|
71
|
-
flags: int = 0,
|
|
72
|
-
exptime: int = 0,
|
|
73
|
-
noreply: bool = False,
|
|
74
|
-
) -> None: ...
|
|
75
|
-
|
|
76
|
-
async def touch(
|
|
77
|
-
self, key: bytes, exptime: int, *, noreply: bool = False
|
|
78
|
-
) -> None: ...
|
|
79
|
-
|
|
80
|
-
|
|
81
33
|
class MemcachedClientP(Protocol):
|
|
82
34
|
def add(
|
|
83
35
|
self,
|
|
@@ -155,8 +107,6 @@ __all__ = [
|
|
|
155
107
|
"Callable",
|
|
156
108
|
"ClassVar",
|
|
157
109
|
"Counter",
|
|
158
|
-
"EmcacheClientP",
|
|
159
|
-
"ItemP",
|
|
160
110
|
"Literal",
|
|
161
111
|
"MemcachedClientP",
|
|
162
112
|
"MongoClient",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: limits
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0rc1
|
|
4
4
|
Summary: Rate limiting utilities
|
|
5
5
|
Home-page: https://limits.readthedocs.org
|
|
6
6
|
Author: Ali-Akber Saifee
|
|
@@ -32,19 +32,14 @@ Provides-Extra: memcached
|
|
|
32
32
|
Requires-Dist: pymemcache<5.0.0,>3; extra == "memcached"
|
|
33
33
|
Provides-Extra: mongodb
|
|
34
34
|
Requires-Dist: pymongo<5,>4.1; extra == "mongodb"
|
|
35
|
-
Provides-Extra: etcd
|
|
36
|
-
Requires-Dist: etcd3; extra == "etcd"
|
|
37
35
|
Provides-Extra: valkey
|
|
38
36
|
Requires-Dist: valkey>=6; extra == "valkey"
|
|
39
37
|
Provides-Extra: async-redis
|
|
40
38
|
Requires-Dist: coredis<5,>=3.4.0; extra == "async-redis"
|
|
41
39
|
Provides-Extra: async-memcached
|
|
42
|
-
Requires-Dist:
|
|
43
|
-
Requires-Dist: emcache>=1; (python_version >= "3.11" and python_version < "3.13.0") and extra == "async-memcached"
|
|
40
|
+
Requires-Dist: memcachio>=0.3; extra == "async-memcached"
|
|
44
41
|
Provides-Extra: async-mongodb
|
|
45
42
|
Requires-Dist: motor<4,>=3; extra == "async-mongodb"
|
|
46
|
-
Provides-Extra: async-etcd
|
|
47
|
-
Requires-Dist: aetcd; extra == "async-etcd"
|
|
48
43
|
Provides-Extra: async-valkey
|
|
49
44
|
Requires-Dist: valkey>=6; extra == "async-valkey"
|
|
50
45
|
Provides-Extra: all
|
|
@@ -52,13 +47,10 @@ Requires-Dist: redis!=4.5.2,!=4.5.3,<6.0.0,>3; extra == "all"
|
|
|
52
47
|
Requires-Dist: redis!=4.5.2,!=4.5.3,>=4.2.0; extra == "all"
|
|
53
48
|
Requires-Dist: pymemcache<5.0.0,>3; extra == "all"
|
|
54
49
|
Requires-Dist: pymongo<5,>4.1; extra == "all"
|
|
55
|
-
Requires-Dist: etcd3; extra == "all"
|
|
56
50
|
Requires-Dist: valkey>=6; extra == "all"
|
|
57
51
|
Requires-Dist: coredis<5,>=3.4.0; extra == "all"
|
|
58
|
-
Requires-Dist:
|
|
59
|
-
Requires-Dist: emcache>=1; (python_version >= "3.11" and python_version < "3.13.0") and extra == "all"
|
|
52
|
+
Requires-Dist: memcachio>=0.3; extra == "all"
|
|
60
53
|
Requires-Dist: motor<4,>=3; extra == "all"
|
|
61
|
-
Requires-Dist: aetcd; extra == "all"
|
|
62
54
|
Requires-Dist: valkey>=6; extra == "all"
|
|
63
55
|
Dynamic: author
|
|
64
56
|
Dynamic: author-email
|
|
@@ -86,13 +78,14 @@ Dynamic: summary
|
|
|
86
78
|
.. |docs| image:: https://readthedocs.org/projects/limits/badge/?version=latest
|
|
87
79
|
:target: https://limits.readthedocs.org
|
|
88
80
|
|
|
81
|
+
######
|
|
89
82
|
limits
|
|
90
|
-
|
|
83
|
+
######
|
|
91
84
|
|docs| |ci| |codecov| |pypi| |pypi-versions| |license|
|
|
92
85
|
|
|
93
86
|
|
|
94
87
|
**limits** is a python library for rate limiting via multiple strategies
|
|
95
|
-
with commonly used storage backends (Redis, Memcached
|
|
88
|
+
with commonly used storage backends (Redis, Memcached & MongoDB).
|
|
96
89
|
|
|
97
90
|
The library provides identical APIs for use in sync and
|
|
98
91
|
`async <https://limits.readthedocs.io/en/stable/async.html>`_ codebases.
|
|
@@ -188,13 +181,13 @@ Scenario 2:
|
|
|
188
181
|
- ``weighted_count = floor(8 + (4 * 0.33)) = floor(8 + 1.32) = 9``.
|
|
189
182
|
- Since the weighted count is below the limit, the request is allowed.
|
|
190
183
|
|
|
184
|
+
|
|
191
185
|
Storage backends
|
|
192
186
|
================
|
|
193
187
|
|
|
194
188
|
- `Redis <https://limits.readthedocs.io/en/latest/storage.html#redis-storage>`_
|
|
195
189
|
- `Memcached <https://limits.readthedocs.io/en/latest/storage.html#memcached-storage>`_
|
|
196
190
|
- `MongoDB <https://limits.readthedocs.io/en/latest/storage.html#mongodb-storage>`_
|
|
197
|
-
- `Etcd <https://limits.readthedocs.io/en/latest/storage.html#etcd-storage>`_
|
|
198
191
|
- `In-Memory <https://limits.readthedocs.io/en/latest/storage.html#in-memory-storage>`_
|
|
199
192
|
|
|
200
193
|
Dive right in
|