limits 3.14.1__py3-none-any.whl → 4.0.1__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/base.py +2 -2
- limits/aio/storage/etcd.py +3 -3
- limits/aio/storage/memcached.py +2 -2
- limits/aio/storage/memory.py +5 -5
- limits/aio/storage/mongodb.py +30 -30
- limits/aio/storage/redis.py +8 -8
- limits/resources/redis/lua_scripts/moving_window.lua +3 -1
- limits/storage/__init__.py +3 -3
- limits/storage/base.py +2 -2
- limits/storage/etcd.py +3 -4
- limits/storage/memcached.py +3 -4
- limits/storage/memory.py +5 -5
- limits/storage/mongodb.py +28 -24
- limits/storage/redis.py +7 -6
- limits/storage/redis_sentinel.py +1 -1
- limits/util.py +1 -1
- {limits-3.14.1.dist-info → limits-4.0.1.dist-info}/METADATA +13 -1
- limits-4.0.1.dist-info/RECORD +37 -0
- limits-3.14.1.dist-info/RECORD +0 -37
- {limits-3.14.1.dist-info → limits-4.0.1.dist-info}/LICENSE.txt +0 -0
- {limits-3.14.1.dist-info → limits-4.0.1.dist-info}/WHEEL +0 -0
- {limits-3.14.1.dist-info → limits-4.0.1.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": "
|
|
11
|
+
"date": "2025-01-16T11:38:03-0800",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "
|
|
14
|
+
"full-revisionid": "074be17ab3008f50de700e996d243ba85580b058",
|
|
15
|
+
"version": "4.0.1"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
limits/aio/storage/base.py
CHANGED
|
@@ -103,7 +103,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
103
103
|
raise NotImplementedError
|
|
104
104
|
|
|
105
105
|
@abstractmethod
|
|
106
|
-
async def get_expiry(self, key: str) ->
|
|
106
|
+
async def get_expiry(self, key: str) -> float:
|
|
107
107
|
"""
|
|
108
108
|
:param key: the key to get the expiry for
|
|
109
109
|
"""
|
|
@@ -169,7 +169,7 @@ class MovingWindowSupport(ABC):
|
|
|
169
169
|
@abstractmethod
|
|
170
170
|
async def get_moving_window(
|
|
171
171
|
self, key: str, limit: int, expiry: int
|
|
172
|
-
) -> Tuple[
|
|
172
|
+
) -> Tuple[float, int]:
|
|
173
173
|
"""
|
|
174
174
|
returns the starting point and the number of entries in the moving
|
|
175
175
|
window
|
limits/aio/storage/etcd.py
CHANGED
|
@@ -116,12 +116,12 @@ class EtcdStorage(Storage):
|
|
|
116
116
|
return int(amount)
|
|
117
117
|
return 0
|
|
118
118
|
|
|
119
|
-
async def get_expiry(self, key: str) ->
|
|
119
|
+
async def get_expiry(self, key: str) -> float:
|
|
120
120
|
cur = await self.storage.get(self.prefixed_key(key))
|
|
121
121
|
if cur:
|
|
122
122
|
window_end = float(cur.value.split(b":")[1])
|
|
123
|
-
return
|
|
124
|
-
return
|
|
123
|
+
return window_end
|
|
124
|
+
return time.time()
|
|
125
125
|
|
|
126
126
|
async def check(self) -> bool:
|
|
127
127
|
try:
|
limits/aio/storage/memcached.py
CHANGED
|
@@ -126,14 +126,14 @@ class MemcachedStorage(Storage):
|
|
|
126
126
|
|
|
127
127
|
return amount
|
|
128
128
|
|
|
129
|
-
async def get_expiry(self, key: str) ->
|
|
129
|
+
async def get_expiry(self, key: str) -> float:
|
|
130
130
|
"""
|
|
131
131
|
:param key: the key to get the expiry for
|
|
132
132
|
"""
|
|
133
133
|
storage = await self.get_storage()
|
|
134
134
|
item = await storage.get(f"{key}/expires".encode())
|
|
135
135
|
|
|
136
|
-
return
|
|
136
|
+
return item and float(item.value) or time.time()
|
|
137
137
|
|
|
138
138
|
async def check(self) -> bool:
|
|
139
139
|
"""
|
limits/aio/storage/memory.py
CHANGED
|
@@ -128,12 +128,12 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
128
128
|
|
|
129
129
|
return True
|
|
130
130
|
|
|
131
|
-
async def get_expiry(self, key: str) ->
|
|
131
|
+
async def get_expiry(self, key: str) -> float:
|
|
132
132
|
"""
|
|
133
133
|
:param key: the key to get the expiry for
|
|
134
134
|
"""
|
|
135
135
|
|
|
136
|
-
return
|
|
136
|
+
return self.expirations.get(key, time.time())
|
|
137
137
|
|
|
138
138
|
async def get_num_acquired(self, key: str, expiry: int) -> int:
|
|
139
139
|
"""
|
|
@@ -153,7 +153,7 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
153
153
|
# FIXME: arg limit is not used
|
|
154
154
|
async def get_moving_window(
|
|
155
155
|
self, key: str, limit: int, expiry: int
|
|
156
|
-
) -> Tuple[
|
|
156
|
+
) -> Tuple[float, int]:
|
|
157
157
|
"""
|
|
158
158
|
returns the starting point and the number of entries in the moving
|
|
159
159
|
window
|
|
@@ -167,9 +167,9 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
167
167
|
|
|
168
168
|
for item in self.events.get(key, [])[::-1]:
|
|
169
169
|
if item.atime >= timestamp - expiry:
|
|
170
|
-
return
|
|
170
|
+
return item.atime, acquired
|
|
171
171
|
|
|
172
|
-
return
|
|
172
|
+
return timestamp, acquired
|
|
173
173
|
|
|
174
174
|
async def check(self) -> bool:
|
|
175
175
|
"""
|
limits/aio/storage/mongodb.py
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import calendar
|
|
5
4
|
import datetime
|
|
6
5
|
import time
|
|
7
|
-
from typing import
|
|
6
|
+
from typing import cast
|
|
8
7
|
|
|
9
8
|
from deprecated.sphinx import versionadded, versionchanged
|
|
10
9
|
|
|
11
10
|
from limits.aio.storage.base import MovingWindowSupport, Storage
|
|
12
|
-
from limits.typing import Dict, Optional, ParamSpec, Tuple, Type, TypeVar, Union
|
|
11
|
+
from limits.typing import Dict, List, Optional, ParamSpec, Tuple, Type, TypeVar, Union
|
|
13
12
|
from limits.util import get_dependency
|
|
14
13
|
|
|
15
14
|
P = ParamSpec("P")
|
|
@@ -135,21 +134,19 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
135
134
|
),
|
|
136
135
|
)
|
|
137
136
|
|
|
138
|
-
async def get_expiry(self, key: str) ->
|
|
137
|
+
async def get_expiry(self, key: str) -> float:
|
|
139
138
|
"""
|
|
140
139
|
:param key: the key to get the expiry for
|
|
141
140
|
"""
|
|
142
141
|
counter = await self.database[self.__collection_mapping["counters"]].find_one(
|
|
143
142
|
{"_id": key}
|
|
144
143
|
)
|
|
145
|
-
|
|
146
|
-
counter["expireAt"]
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
return (
|
|
145
|
+
(counter["expireAt"] if counter else datetime.datetime.now())
|
|
146
|
+
.replace(tzinfo=datetime.timezone.utc)
|
|
147
|
+
.timestamp()
|
|
149
148
|
)
|
|
150
149
|
|
|
151
|
-
return calendar.timegm(expiry.timetuple())
|
|
152
|
-
|
|
153
150
|
async def get(self, key: str) -> int:
|
|
154
151
|
"""
|
|
155
152
|
:param key: the key to get the counter value for
|
|
@@ -227,7 +224,7 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
227
224
|
|
|
228
225
|
async def get_moving_window(
|
|
229
226
|
self, key: str, limit: int, expiry: int
|
|
230
|
-
) -> Tuple[
|
|
227
|
+
) -> Tuple[float, int]:
|
|
231
228
|
"""
|
|
232
229
|
returns the starting point and the number of entries in the moving
|
|
233
230
|
window
|
|
@@ -237,7 +234,7 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
237
234
|
:return: (start of window, number of acquired entries)
|
|
238
235
|
"""
|
|
239
236
|
timestamp = time.time()
|
|
240
|
-
result
|
|
237
|
+
if result := (
|
|
241
238
|
await self.database[self.__collection_mapping["windows"]]
|
|
242
239
|
.aggregate(
|
|
243
240
|
[
|
|
@@ -264,12 +261,9 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
264
261
|
]
|
|
265
262
|
)
|
|
266
263
|
.to_list(length=1)
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return (int(result[0]["min"]), result[0]["count"])
|
|
271
|
-
|
|
272
|
-
return (int(timestamp), 0)
|
|
264
|
+
):
|
|
265
|
+
return result[0]["min"], result[0]["count"]
|
|
266
|
+
return timestamp, 0
|
|
273
267
|
|
|
274
268
|
async def acquire_entry(
|
|
275
269
|
self, key: str, limit: int, expiry: int, amount: int = 1
|
|
@@ -287,23 +281,29 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
287
281
|
|
|
288
282
|
timestamp = time.time()
|
|
289
283
|
try:
|
|
290
|
-
updates: Dict[
|
|
291
|
-
|
|
284
|
+
updates: Dict[
|
|
285
|
+
str,
|
|
286
|
+
Dict[str, Union[datetime.datetime, Dict[str, Union[List[float], int]]]],
|
|
287
|
+
] = {
|
|
288
|
+
"$push": {
|
|
289
|
+
"entries": {
|
|
290
|
+
"$each": [timestamp] * amount,
|
|
291
|
+
"$position": 0,
|
|
292
|
+
"$slice": limit,
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
"$set": {
|
|
296
|
+
"expireAt": (
|
|
297
|
+
datetime.datetime.now(datetime.timezone.utc)
|
|
298
|
+
+ datetime.timedelta(seconds=expiry)
|
|
299
|
+
)
|
|
300
|
+
},
|
|
292
301
|
}
|
|
293
302
|
|
|
294
|
-
updates["$set"] = {
|
|
295
|
-
"expireAt": (
|
|
296
|
-
datetime.datetime.now(datetime.timezone.utc)
|
|
297
|
-
+ datetime.timedelta(seconds=expiry)
|
|
298
|
-
)
|
|
299
|
-
}
|
|
300
|
-
updates["$push"]["entries"]["$each"] = [timestamp] * amount
|
|
301
303
|
await self.database[self.__collection_mapping["windows"]].update_one(
|
|
302
304
|
{
|
|
303
305
|
"_id": key,
|
|
304
|
-
"entries
|
|
305
|
-
"$not": {"$gte": timestamp - expiry}
|
|
306
|
-
},
|
|
306
|
+
f"entries.{limit - amount}": {"$not": {"$gte": timestamp - expiry}},
|
|
307
307
|
},
|
|
308
308
|
updates,
|
|
309
309
|
upsert=True,
|
limits/aio/storage/redis.py
CHANGED
|
@@ -78,7 +78,7 @@ class RedisInteractor:
|
|
|
78
78
|
|
|
79
79
|
async def get_moving_window(
|
|
80
80
|
self, key: str, limit: int, expiry: int
|
|
81
|
-
) -> Tuple[
|
|
81
|
+
) -> Tuple[float, int]:
|
|
82
82
|
"""
|
|
83
83
|
returns the starting point and the number of entries in the moving
|
|
84
84
|
window
|
|
@@ -88,12 +88,12 @@ class RedisInteractor:
|
|
|
88
88
|
:return: (start of window, number of acquired entries)
|
|
89
89
|
"""
|
|
90
90
|
key = self.prefixed_key(key)
|
|
91
|
-
timestamp =
|
|
91
|
+
timestamp = time.time()
|
|
92
92
|
window = await self.lua_moving_window.execute(
|
|
93
|
-
[key], [
|
|
93
|
+
[key], [timestamp - expiry, limit]
|
|
94
94
|
)
|
|
95
95
|
if window:
|
|
96
|
-
return
|
|
96
|
+
return float(window[0]), window[1] # type: ignore
|
|
97
97
|
return timestamp, 0
|
|
98
98
|
|
|
99
99
|
async def _acquire_entry(
|
|
@@ -118,14 +118,14 @@ class RedisInteractor:
|
|
|
118
118
|
|
|
119
119
|
return bool(acquired)
|
|
120
120
|
|
|
121
|
-
async def _get_expiry(self, key: str, connection: AsyncRedisClient) ->
|
|
121
|
+
async def _get_expiry(self, key: str, connection: AsyncRedisClient) -> float:
|
|
122
122
|
"""
|
|
123
123
|
:param key: the key to get the expiry for
|
|
124
124
|
:param connection: Redis connection
|
|
125
125
|
"""
|
|
126
126
|
|
|
127
127
|
key = self.prefixed_key(key)
|
|
128
|
-
return
|
|
128
|
+
return max(await connection.ttl(key), 0) + time.time()
|
|
129
129
|
|
|
130
130
|
async def _check(self, connection: AsyncRedisClient) -> bool:
|
|
131
131
|
"""
|
|
@@ -261,7 +261,7 @@ class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
|
|
|
261
261
|
|
|
262
262
|
return await super()._acquire_entry(key, limit, expiry, self.storage, amount)
|
|
263
263
|
|
|
264
|
-
async def get_expiry(self, key: str) ->
|
|
264
|
+
async def get_expiry(self, key: str) -> float:
|
|
265
265
|
"""
|
|
266
266
|
:param key: the key to get the expiry for
|
|
267
267
|
"""
|
|
@@ -450,7 +450,7 @@ class RedisSentinelStorage(RedisStorage):
|
|
|
450
450
|
key, self.storage_replica if self.use_replicas else self.storage
|
|
451
451
|
)
|
|
452
452
|
|
|
453
|
-
async def get_expiry(self, key: str) ->
|
|
453
|
+
async def get_expiry(self, key: str) -> float:
|
|
454
454
|
"""
|
|
455
455
|
:param key: the key to get the expiry for
|
|
456
456
|
"""
|
limits/storage/__init__.py
CHANGED
|
@@ -32,9 +32,9 @@ def storage_from_string(
|
|
|
32
32
|
|
|
33
33
|
from limits.storage import storage_from_string
|
|
34
34
|
|
|
35
|
-
memory =
|
|
36
|
-
memcached =
|
|
37
|
-
redis =
|
|
35
|
+
memory = storage_from_string("memory://")
|
|
36
|
+
memcached = storage_from_string("memcached://localhost:11211")
|
|
37
|
+
redis = storage_from_string("redis://localhost:6379")
|
|
38
38
|
|
|
39
39
|
The same function can be used to construct the :ref:`storage:async storage`
|
|
40
40
|
variants, for example::
|
limits/storage/base.py
CHANGED
|
@@ -99,7 +99,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
99
99
|
raise NotImplementedError
|
|
100
100
|
|
|
101
101
|
@abstractmethod
|
|
102
|
-
def get_expiry(self, key: str) ->
|
|
102
|
+
def get_expiry(self, key: str) -> float:
|
|
103
103
|
"""
|
|
104
104
|
:param key: the key to get the expiry for
|
|
105
105
|
"""
|
|
@@ -161,7 +161,7 @@ class MovingWindowSupport(ABC):
|
|
|
161
161
|
raise NotImplementedError
|
|
162
162
|
|
|
163
163
|
@abstractmethod
|
|
164
|
-
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[
|
|
164
|
+
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[float, int]:
|
|
165
165
|
"""
|
|
166
166
|
returns the starting point and the number of entries in the moving
|
|
167
167
|
window
|
limits/storage/etcd.py
CHANGED
|
@@ -110,12 +110,11 @@ class EtcdStorage(Storage):
|
|
|
110
110
|
return int(amount)
|
|
111
111
|
return 0
|
|
112
112
|
|
|
113
|
-
def get_expiry(self, key: str) ->
|
|
113
|
+
def get_expiry(self, key: str) -> float:
|
|
114
114
|
value, _ = self.storage.get(self.prefixed_key(key))
|
|
115
115
|
if value:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return int(time.time())
|
|
116
|
+
return float(value.split(b":")[1])
|
|
117
|
+
return time.time()
|
|
119
118
|
|
|
120
119
|
def check(self) -> bool:
|
|
121
120
|
try:
|
limits/storage/memcached.py
CHANGED
|
@@ -77,8 +77,7 @@ class MemcachedStorage(Storage):
|
|
|
77
77
|
|
|
78
78
|
if not get_dependency(self.library):
|
|
79
79
|
raise ConfigurationError(
|
|
80
|
-
"memcached prerequisite not available."
|
|
81
|
-
" please install %s" % self.library
|
|
80
|
+
"memcached prerequisite not available. please install %s" % self.library
|
|
82
81
|
) # pragma: no cover
|
|
83
82
|
self.local_storage = threading.local()
|
|
84
83
|
self.local_storage.storage = None
|
|
@@ -192,12 +191,12 @@ class MemcachedStorage(Storage):
|
|
|
192
191
|
|
|
193
192
|
return amount
|
|
194
193
|
|
|
195
|
-
def get_expiry(self, key: str) ->
|
|
194
|
+
def get_expiry(self, key: str) -> float:
|
|
196
195
|
"""
|
|
197
196
|
:param key: the key to get the expiry for
|
|
198
197
|
"""
|
|
199
198
|
|
|
200
|
-
return
|
|
199
|
+
return float(self.storage.get(key + "/expires") or time.time())
|
|
201
200
|
|
|
202
201
|
def check(self) -> bool:
|
|
203
202
|
"""
|
limits/storage/memory.py
CHANGED
|
@@ -121,12 +121,12 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
121
121
|
self.events[key][:0] = [LockableEntry(expiry) for _ in range(amount)]
|
|
122
122
|
return True
|
|
123
123
|
|
|
124
|
-
def get_expiry(self, key: str) ->
|
|
124
|
+
def get_expiry(self, key: str) -> float:
|
|
125
125
|
"""
|
|
126
126
|
:param key: the key to get the expiry for
|
|
127
127
|
"""
|
|
128
128
|
|
|
129
|
-
return
|
|
129
|
+
return self.expirations.get(key, time.time())
|
|
130
130
|
|
|
131
131
|
def get_num_acquired(self, key: str, expiry: int) -> int:
|
|
132
132
|
"""
|
|
@@ -143,7 +143,7 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
143
143
|
else 0
|
|
144
144
|
)
|
|
145
145
|
|
|
146
|
-
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[
|
|
146
|
+
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[float, int]:
|
|
147
147
|
"""
|
|
148
148
|
returns the starting point and the number of entries in the moving
|
|
149
149
|
window
|
|
@@ -157,9 +157,9 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
157
157
|
|
|
158
158
|
for item in self.events.get(key, [])[::-1]:
|
|
159
159
|
if item.atime >= timestamp - expiry:
|
|
160
|
-
return
|
|
160
|
+
return item.atime, acquired
|
|
161
161
|
|
|
162
|
-
return
|
|
162
|
+
return timestamp, acquired
|
|
163
163
|
|
|
164
164
|
def check(self) -> bool:
|
|
165
165
|
"""
|
limits/storage/mongodb.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import calendar
|
|
4
3
|
import datetime
|
|
5
4
|
import time
|
|
6
5
|
from abc import ABC, abstractmethod
|
|
7
|
-
from typing import
|
|
6
|
+
from typing import cast
|
|
8
7
|
|
|
9
8
|
from deprecated.sphinx import versionadded, versionchanged
|
|
10
9
|
|
|
11
10
|
from limits.typing import (
|
|
12
11
|
Dict,
|
|
12
|
+
List,
|
|
13
13
|
MongoClient,
|
|
14
14
|
MongoCollection,
|
|
15
15
|
MongoDatabase,
|
|
@@ -122,19 +122,17 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
122
122
|
self.counters.find_one_and_delete({"_id": key})
|
|
123
123
|
self.windows.find_one_and_delete({"_id": key})
|
|
124
124
|
|
|
125
|
-
def get_expiry(self, key: str) ->
|
|
125
|
+
def get_expiry(self, key: str) -> float:
|
|
126
126
|
"""
|
|
127
127
|
:param key: the key to get the expiry for
|
|
128
128
|
"""
|
|
129
129
|
counter = self.counters.find_one({"_id": key})
|
|
130
|
-
|
|
131
|
-
counter["expireAt"]
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
return (
|
|
131
|
+
(counter["expireAt"] if counter else datetime.datetime.now())
|
|
132
|
+
.replace(tzinfo=datetime.timezone.utc)
|
|
133
|
+
.timestamp()
|
|
134
134
|
)
|
|
135
135
|
|
|
136
|
-
return calendar.timegm(expiry.timetuple())
|
|
137
|
-
|
|
138
136
|
def get(self, key: str) -> int:
|
|
139
137
|
"""
|
|
140
138
|
:param key: the key to get the counter value for
|
|
@@ -205,7 +203,7 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
205
203
|
except: # noqa: E722
|
|
206
204
|
return False
|
|
207
205
|
|
|
208
|
-
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[
|
|
206
|
+
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[float, int]:
|
|
209
207
|
"""
|
|
210
208
|
returns the starting point and the number of entries in the moving
|
|
211
209
|
window
|
|
@@ -243,9 +241,9 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
243
241
|
)
|
|
244
242
|
|
|
245
243
|
if result:
|
|
246
|
-
return
|
|
244
|
+
return result[0]["min"], result[0]["count"]
|
|
247
245
|
|
|
248
|
-
return
|
|
246
|
+
return timestamp, 0
|
|
249
247
|
|
|
250
248
|
def acquire_entry(self, key: str, limit: int, expiry: int, amount: int = 1) -> bool:
|
|
251
249
|
"""
|
|
@@ -259,23 +257,29 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
259
257
|
|
|
260
258
|
timestamp = time.time()
|
|
261
259
|
try:
|
|
262
|
-
updates: Dict[
|
|
263
|
-
|
|
260
|
+
updates: Dict[
|
|
261
|
+
str,
|
|
262
|
+
Dict[str, Union[datetime.datetime, Dict[str, Union[List[float], int]]]],
|
|
263
|
+
] = {
|
|
264
|
+
"$push": {
|
|
265
|
+
"entries": {
|
|
266
|
+
"$each": [timestamp] * amount,
|
|
267
|
+
"$position": 0,
|
|
268
|
+
"$slice": limit,
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
"$set": {
|
|
272
|
+
"expireAt": (
|
|
273
|
+
datetime.datetime.now(datetime.timezone.utc)
|
|
274
|
+
+ datetime.timedelta(seconds=expiry)
|
|
275
|
+
)
|
|
276
|
+
},
|
|
264
277
|
}
|
|
265
278
|
|
|
266
|
-
updates["$set"] = {
|
|
267
|
-
"expireAt": (
|
|
268
|
-
datetime.datetime.now(datetime.timezone.utc)
|
|
269
|
-
+ datetime.timedelta(seconds=expiry)
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
updates["$push"]["entries"]["$each"] = [timestamp] * amount
|
|
273
279
|
self.windows.update_one(
|
|
274
280
|
{
|
|
275
281
|
"_id": key,
|
|
276
|
-
"entries
|
|
277
|
-
"$not": {"$gte": timestamp - expiry}
|
|
278
|
-
},
|
|
282
|
+
f"entries.{limit - amount}": {"$not": {"$gte": timestamp - expiry}},
|
|
279
283
|
},
|
|
280
284
|
updates,
|
|
281
285
|
upsert=True,
|
limits/storage/redis.py
CHANGED
|
@@ -32,7 +32,7 @@ class RedisInteractor:
|
|
|
32
32
|
def prefixed_key(self, key: str) -> str:
|
|
33
33
|
return f"{self.PREFIX}:{key}"
|
|
34
34
|
|
|
35
|
-
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[
|
|
35
|
+
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[float, int]:
|
|
36
36
|
"""
|
|
37
37
|
returns the starting point and the number of entries in the moving
|
|
38
38
|
window
|
|
@@ -43,9 +43,10 @@ class RedisInteractor:
|
|
|
43
43
|
"""
|
|
44
44
|
key = self.prefixed_key(key)
|
|
45
45
|
timestamp = time.time()
|
|
46
|
-
window
|
|
46
|
+
if window := self.lua_moving_window([key], [timestamp - expiry, limit]):
|
|
47
|
+
return float(window[0]), window[1]
|
|
47
48
|
|
|
48
|
-
return
|
|
49
|
+
return timestamp, 0
|
|
49
50
|
|
|
50
51
|
def _incr(
|
|
51
52
|
self,
|
|
@@ -109,14 +110,14 @@ class RedisInteractor:
|
|
|
109
110
|
|
|
110
111
|
return bool(acquired)
|
|
111
112
|
|
|
112
|
-
def _get_expiry(self, key: str, connection: RedisClient) ->
|
|
113
|
+
def _get_expiry(self, key: str, connection: RedisClient) -> float:
|
|
113
114
|
"""
|
|
114
115
|
:param key: the key to get the expiry for
|
|
115
116
|
:param connection: Redis connection
|
|
116
117
|
"""
|
|
117
118
|
|
|
118
119
|
key = self.prefixed_key(key)
|
|
119
|
-
return
|
|
120
|
+
return max(connection.ttl(key), 0) + time.time()
|
|
120
121
|
|
|
121
122
|
def _check(self, connection: RedisClient) -> bool:
|
|
122
123
|
"""
|
|
@@ -232,7 +233,7 @@ class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
|
|
|
232
233
|
|
|
233
234
|
return super()._acquire_entry(key, limit, expiry, self.storage, amount)
|
|
234
235
|
|
|
235
|
-
def get_expiry(self, key: str) ->
|
|
236
|
+
def get_expiry(self, key: str) -> float:
|
|
236
237
|
"""
|
|
237
238
|
:param key: the key to get the expiry for
|
|
238
239
|
"""
|
limits/storage/redis_sentinel.py
CHANGED
|
@@ -101,7 +101,7 @@ class RedisSentinelStorage(RedisStorage):
|
|
|
101
101
|
key, self.storage_slave if self.use_replicas else self.storage
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
-
def get_expiry(self, key: str) ->
|
|
104
|
+
def get_expiry(self, key: str) -> float:
|
|
105
105
|
"""
|
|
106
106
|
:param key: the key to get the expiry for
|
|
107
107
|
"""
|
limits/util.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: limits
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.1
|
|
4
4
|
Summary: Rate limiting utilities
|
|
5
5
|
Home-page: https://limits.readthedocs.org
|
|
6
6
|
Author: Ali-Akber Saifee
|
|
@@ -172,6 +172,18 @@ Check specific limits without hitting them
|
|
|
172
172
|
time.sleep(0.01)
|
|
173
173
|
assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
|
|
174
174
|
|
|
175
|
+
Query available capacity and reset time for a limit
|
|
176
|
+
|
|
177
|
+
.. code-block:: python
|
|
178
|
+
|
|
179
|
+
assert True == moving_window.hit(one_per_minute, "test_namespace", "foo")
|
|
180
|
+
window = moving_window.get_window_stats(one_per_minute, "test_namespace", "foo")
|
|
181
|
+
assert window.remaining == 0
|
|
182
|
+
assert False == moving_window.hit(one_per_minute, "test_namespace", "foo")
|
|
183
|
+
time.sleep(window.reset_time - time.time())
|
|
184
|
+
assert True == moving_window.hit(one_per_minute, "test_namespace", "foo")
|
|
185
|
+
|
|
186
|
+
|
|
175
187
|
Links
|
|
176
188
|
=====
|
|
177
189
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
limits/__init__.py,sha256=j_yVhgN9pdz8o5rQjVwdJTBSq8F-CTzof9kkiYgjRbw,728
|
|
2
|
+
limits/_version.py,sha256=pB8_a1HHuX7Bo8LVuoQ_8GjLU5i4z56J1tr986TiLUA,497
|
|
3
|
+
limits/errors.py,sha256=xCKGOVJiD-g8FlsQQb17AW2pTUvalYSuizPpvEVoYJE,626
|
|
4
|
+
limits/limits.py,sha256=ZsXESq2e1ji7c2ZKjSkIAasCjiLdjVLPUa9oah_I8U4,4943
|
|
5
|
+
limits/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
limits/strategies.py,sha256=Zy6PIhkysPbxnMzFjyXEsxMM6jhRoQ5XT5WskTNruK0,6949
|
|
7
|
+
limits/typing.py,sha256=4yitf6iwDK-QEfSxv3EbTdGLOrqowLFffHAqYRUqiYY,3275
|
|
8
|
+
limits/util.py,sha256=fTx0JQBT6ZY3fxGefjT07CLimUVCFjS1jLqskXmb7Eo,5726
|
|
9
|
+
limits/version.py,sha256=YwkF3dtq1KGzvmL3iVGctA8NNtGlK_0arrzZkZGVjUs,47
|
|
10
|
+
limits/aio/__init__.py,sha256=IOetunwQy1c5GefzitK8lewbTzHGiE-kmE9NlqSdr3U,82
|
|
11
|
+
limits/aio/strategies.py,sha256=SHjmJnmy7Nh4tBydkA-0qPaULYcLOAM91T4RPybq0Sg,6768
|
|
12
|
+
limits/aio/storage/__init__.py,sha256=CbtuSlVl1jPyN_vsEI_ApWblDblVaL46xcZ2M_oM0V8,595
|
|
13
|
+
limits/aio/storage/base.py,sha256=V1Ur9Cu29_vP5IYBIsWHTgrc4riW8FEyz5Dcvv6fPoc,4821
|
|
14
|
+
limits/aio/storage/etcd.py,sha256=krqjWujvybuaFa2g_FkPr2ZtX9Ac1-oJzErfGW3h27o,4783
|
|
15
|
+
limits/aio/storage/memcached.py,sha256=n8b9GVtXMWdc-w4-xP1_MPJ9dgVcgoJ5j53mTdU6E3E,4799
|
|
16
|
+
limits/aio/storage/memory.py,sha256=4ah9RpE5r7Q2yOLT-OndhP4ZHmvcwV3rKvCicnK2CJc,5845
|
|
17
|
+
limits/aio/storage/mongodb.py,sha256=pC5Ng-5PlV5u1EQlgITfetLvpL4mitcxaaZ_uokwUs0,10846
|
|
18
|
+
limits/aio/storage/redis.py,sha256=JQm4pkwynSo1k6wFVB7SyRsh7yav0_61Px1FqUwuGl4,15658
|
|
19
|
+
limits/resources/redis/lua_scripts/acquire_moving_window.lua,sha256=5CFJX7D6T6RG5SFr6eVZ6zepmI1EkGWmKeVEO4QNrWo,483
|
|
20
|
+
limits/resources/redis/lua_scripts/clear_keys.lua,sha256=zU0cVfLGmapRQF9x9u0GclapM_IB2pJLszNzVQ1QRK4,184
|
|
21
|
+
limits/resources/redis/lua_scripts/incr_expire.lua,sha256=Uq9NcrrcDI-F87TDAJexoSJn2SDgeXIUEYozCp9S3oA,195
|
|
22
|
+
limits/resources/redis/lua_scripts/moving_window.lua,sha256=5hUZghISDh8Cbg8HJediM_OKjjNMF-0CBywWmsc93vA,430
|
|
23
|
+
limits/storage/__init__.py,sha256=_ozbLZtZDVRamfNe4clxcmYbM9Gbu8FzuuAeWQAi_ZA,2588
|
|
24
|
+
limits/storage/base.py,sha256=E7ZInoGZqoM1QIpd1f8lvytlic4sMOQdl_eTzD-mlWk,4631
|
|
25
|
+
limits/storage/etcd.py,sha256=Q1tndCAAJp0jnir-b-ZBN3-7Kf3v_uwNAqQJLmqB96Q,4440
|
|
26
|
+
limits/storage/memcached.py,sha256=4xgcSI7l02KbHP0P_ChFgx5ytDqiby6-kwfJ6_5lua4,6618
|
|
27
|
+
limits/storage/memory.py,sha256=ButyS6v7o7DB55bwM3CltFK4Fc5mwKuFEBPgat51CXU,5550
|
|
28
|
+
limits/storage/mongodb.py,sha256=LmrkLOlMMveI4hnv69yMOQhrXUqp6RMvdjXkvTiE4DY,9979
|
|
29
|
+
limits/storage/redis.py,sha256=R7UbE5ng1NjIHEO17gu-vIZ4qgy91JctbPYGEkZ2iM0,8483
|
|
30
|
+
limits/storage/redis_cluster.py,sha256=MsiEpwHphQd0P88AwGw1NVSi3UwVrhsg-pvzkHxU2kw,3739
|
|
31
|
+
limits/storage/redis_sentinel.py,sha256=665CvL3UZYB2sB_vVkZ4CCaPKcbIXvQUWuDWnBoSOLU,4124
|
|
32
|
+
limits/storage/registry.py,sha256=xcBcxuu6srqmoS4WqDpkCXnRLB19ctH98v21P8S9kS8,708
|
|
33
|
+
limits-4.0.1.dist-info/LICENSE.txt,sha256=T6i7kq7F5gIPfcno9FCxU5Hcwm22Bjq0uHZV3ElcjsQ,1061
|
|
34
|
+
limits-4.0.1.dist-info/METADATA,sha256=EzpDuoZfOrB3xjvhv-IHK5Q6-R_6-vH5FnUEoJ-ohdI,7662
|
|
35
|
+
limits-4.0.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
36
|
+
limits-4.0.1.dist-info/top_level.txt,sha256=C7g5ahldPoU2s6iWTaJayUrbGmPK1d6e9t5Nn0vQ2jM,7
|
|
37
|
+
limits-4.0.1.dist-info/RECORD,,
|
limits-3.14.1.dist-info/RECORD
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
limits/__init__.py,sha256=j_yVhgN9pdz8o5rQjVwdJTBSq8F-CTzof9kkiYgjRbw,728
|
|
2
|
-
limits/_version.py,sha256=WbNixYn5JfqVMNZ6ag1B3zh5evGgXTIqinJPk2vcWH0,498
|
|
3
|
-
limits/errors.py,sha256=xCKGOVJiD-g8FlsQQb17AW2pTUvalYSuizPpvEVoYJE,626
|
|
4
|
-
limits/limits.py,sha256=ZsXESq2e1ji7c2ZKjSkIAasCjiLdjVLPUa9oah_I8U4,4943
|
|
5
|
-
limits/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
limits/strategies.py,sha256=Zy6PIhkysPbxnMzFjyXEsxMM6jhRoQ5XT5WskTNruK0,6949
|
|
7
|
-
limits/typing.py,sha256=4yitf6iwDK-QEfSxv3EbTdGLOrqowLFffHAqYRUqiYY,3275
|
|
8
|
-
limits/util.py,sha256=fi2XoUBoEk0T-le41VQn3x-BJ3QqrLeIcP52Bj-qwpg,5724
|
|
9
|
-
limits/version.py,sha256=YwkF3dtq1KGzvmL3iVGctA8NNtGlK_0arrzZkZGVjUs,47
|
|
10
|
-
limits/aio/__init__.py,sha256=IOetunwQy1c5GefzitK8lewbTzHGiE-kmE9NlqSdr3U,82
|
|
11
|
-
limits/aio/strategies.py,sha256=SHjmJnmy7Nh4tBydkA-0qPaULYcLOAM91T4RPybq0Sg,6768
|
|
12
|
-
limits/aio/storage/__init__.py,sha256=CbtuSlVl1jPyN_vsEI_ApWblDblVaL46xcZ2M_oM0V8,595
|
|
13
|
-
limits/aio/storage/base.py,sha256=xdYpBBonyMjxE9iT-2oZjm6x29aDU6Xd09MeBYbZcMo,4817
|
|
14
|
-
limits/aio/storage/etcd.py,sha256=Rjb_EYKFRr4F2Z6zvAPP9vQOyXJQHaju3VjxxUs75_c,4791
|
|
15
|
-
limits/aio/storage/memcached.py,sha256=6aTlACfCtchdcZqoisnei0MOlCH7yLV9A1yCjOE5f9g,4802
|
|
16
|
-
limits/aio/storage/memory.py,sha256=DlmWluqUwBUWQIQ6XZi-mPrb15vfzBhA4iAKhBELDnE,5856
|
|
17
|
-
limits/aio/storage/mongodb.py,sha256=gZq9Ky3J7j0xfqIqB3ULBbQ5VjHzIyT1c7aNFp01VKk,10764
|
|
18
|
-
limits/aio/storage/redis.py,sha256=jkqtdIwTpfXTXwgTWTA1Jlc3Lpc-vnu4XRy6CIptiZA,15651
|
|
19
|
-
limits/resources/redis/lua_scripts/acquire_moving_window.lua,sha256=5CFJX7D6T6RG5SFr6eVZ6zepmI1EkGWmKeVEO4QNrWo,483
|
|
20
|
-
limits/resources/redis/lua_scripts/clear_keys.lua,sha256=zU0cVfLGmapRQF9x9u0GclapM_IB2pJLszNzVQ1QRK4,184
|
|
21
|
-
limits/resources/redis/lua_scripts/incr_expire.lua,sha256=Uq9NcrrcDI-F87TDAJexoSJn2SDgeXIUEYozCp9S3oA,195
|
|
22
|
-
limits/resources/redis/lua_scripts/moving_window.lua,sha256=iAInenlVd_fFDi15APpRWbOuPUz_G3nFnVAqb7wOedA,398
|
|
23
|
-
limits/storage/__init__.py,sha256=XAW1jVDMLFkPr_Tl1SXpg_p4Y3nhEatTSYq1MlnYJcA,2564
|
|
24
|
-
limits/storage/base.py,sha256=fDdYLa-RrnjhBTO1hE5aTTM8q8n3M5HD-65KyWWXBtg,4627
|
|
25
|
-
limits/storage/etcd.py,sha256=wkC_mj4Tsf2nwUKByMiHiGzA40N3mDepEwdLmvH8wmw,4484
|
|
26
|
-
limits/storage/memcached.py,sha256=bMzfZgYa_EWcZAjSZLcygpk3hpeOAErBpRE8dVwyXQs,6640
|
|
27
|
-
limits/storage/memory.py,sha256=R16E-Ccnmn1-LlolkFf-kB1-QHh8eiwFFLYVv0PuFD0,5561
|
|
28
|
-
limits/storage/mongodb.py,sha256=t8ey5-gYrJcmyvwJkqje0-TR-UMYvBF900a_zEXAYPI,9873
|
|
29
|
-
limits/storage/redis.py,sha256=3zJ1gDMDepT_pGN9d2aAN7Pea7tMBI49VK60IHv-Ooc,8452
|
|
30
|
-
limits/storage/redis_cluster.py,sha256=MsiEpwHphQd0P88AwGw1NVSi3UwVrhsg-pvzkHxU2kw,3739
|
|
31
|
-
limits/storage/redis_sentinel.py,sha256=lI7y7x9VGDCGq_-WRb6jR6cDgnzsOC1KGaRbwE69DNk,4122
|
|
32
|
-
limits/storage/registry.py,sha256=xcBcxuu6srqmoS4WqDpkCXnRLB19ctH98v21P8S9kS8,708
|
|
33
|
-
limits-3.14.1.dist-info/LICENSE.txt,sha256=T6i7kq7F5gIPfcno9FCxU5Hcwm22Bjq0uHZV3ElcjsQ,1061
|
|
34
|
-
limits-3.14.1.dist-info/METADATA,sha256=iaF5tB4JgLvdzKk1-BNDZ1jkNo_3j-QidH4Ar7h218o,7189
|
|
35
|
-
limits-3.14.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
36
|
-
limits-3.14.1.dist-info/top_level.txt,sha256=C7g5ahldPoU2s6iWTaJayUrbGmPK1d6e9t5Nn0vQ2jM,7
|
|
37
|
-
limits-3.14.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|