limits 3.12.0__py3-none-any.whl → 3.14.0__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/mongodb.py +66 -34
- limits/aio/strategies.py +10 -5
- limits/storage/mongodb.py +16 -3
- limits/storage/redis_cluster.py +28 -62
- limits/storage/redis_sentinel.py +7 -1
- limits/strategies.py +8 -5
- limits/typing.py +2 -2
- limits/util.py +3 -3
- {limits-3.12.0.dist-info → limits-3.14.0.dist-info}/METADATA +4 -5
- {limits-3.12.0.dist-info → limits-3.14.0.dist-info}/RECORD +14 -14
- {limits-3.12.0.dist-info → limits-3.14.0.dist-info}/WHEEL +1 -1
- {limits-3.12.0.dist-info → limits-3.14.0.dist-info}/LICENSE.txt +0 -0
- {limits-3.12.0.dist-info → limits-3.14.0.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": "2024-
|
|
11
|
+
"date": "2024-11-29T18:35:52-0800",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "3.
|
|
14
|
+
"full-revisionid": "68e30b16c1b081d837e0050929de63f38ea3ca38",
|
|
15
|
+
"version": "3.14.0"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
limits/aio/storage/mongodb.py
CHANGED
|
@@ -6,7 +6,7 @@ import datetime
|
|
|
6
6
|
import time
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
|
-
from deprecated.sphinx import versionadded
|
|
9
|
+
from deprecated.sphinx import versionadded, versionchanged
|
|
10
10
|
|
|
11
11
|
from limits.aio.storage.base import MovingWindowSupport, Storage
|
|
12
12
|
from limits.typing import Dict, Optional, ParamSpec, Tuple, Type, TypeVar, Union
|
|
@@ -17,6 +17,10 @@ R = TypeVar("R")
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@versionadded(version="2.1")
|
|
20
|
+
@versionchanged(
|
|
21
|
+
version="3.14.0",
|
|
22
|
+
reason="Added option to select custom collection names for windows & counters",
|
|
23
|
+
)
|
|
20
24
|
class MongoDBStorage(Storage, MovingWindowSupport):
|
|
21
25
|
"""
|
|
22
26
|
Rate limit storage with MongoDB as backend.
|
|
@@ -35,6 +39,8 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
35
39
|
self,
|
|
36
40
|
uri: str,
|
|
37
41
|
database_name: str = "limits",
|
|
42
|
+
counter_collection_name: str = "counters",
|
|
43
|
+
window_collection_name: str = "windows",
|
|
38
44
|
wrap_exceptions: bool = False,
|
|
39
45
|
**options: Union[float, str, bool],
|
|
40
46
|
) -> None:
|
|
@@ -43,6 +49,9 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
43
49
|
This uri is passed directly to :class:`~motor.motor_asyncio.AsyncIOMotorClient`
|
|
44
50
|
:param database_name: The database to use for storing the rate limit
|
|
45
51
|
collections.
|
|
52
|
+
:param counter_collection_name: The collection name to use for individual counters
|
|
53
|
+
used in fixed window strategies
|
|
54
|
+
:param window_collection_name: The collection name to use for moving window storage
|
|
46
55
|
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
47
56
|
:exc:`limits.errors.StorageError` before raising it.
|
|
48
57
|
:param options: all remaining keyword arguments are passed
|
|
@@ -66,6 +75,10 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
66
75
|
self.storage.get_io_loop = asyncio.get_running_loop
|
|
67
76
|
|
|
68
77
|
self.__database_name = database_name
|
|
78
|
+
self.__collection_mapping = {
|
|
79
|
+
"counters": counter_collection_name,
|
|
80
|
+
"windows": window_collection_name,
|
|
81
|
+
}
|
|
69
82
|
self.__indices_created = False
|
|
70
83
|
|
|
71
84
|
@property
|
|
@@ -81,8 +94,12 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
81
94
|
async def create_indices(self) -> None:
|
|
82
95
|
if not self.__indices_created:
|
|
83
96
|
await asyncio.gather(
|
|
84
|
-
self.database.counters.create_index(
|
|
85
|
-
|
|
97
|
+
self.database[self.__collection_mapping["counters"]].create_index(
|
|
98
|
+
"expireAt", expireAfterSeconds=0
|
|
99
|
+
),
|
|
100
|
+
self.database[self.__collection_mapping["windows"]].create_index(
|
|
101
|
+
"expireAt", expireAfterSeconds=0
|
|
102
|
+
),
|
|
86
103
|
)
|
|
87
104
|
self.__indices_created = True
|
|
88
105
|
|
|
@@ -92,12 +109,15 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
92
109
|
"""
|
|
93
110
|
num_keys = sum(
|
|
94
111
|
await asyncio.gather(
|
|
95
|
-
self.database.counters.count_documents(
|
|
96
|
-
|
|
112
|
+
self.database[self.__collection_mapping["counters"]].count_documents(
|
|
113
|
+
{}
|
|
114
|
+
),
|
|
115
|
+
self.database[self.__collection_mapping["windows"]].count_documents({}),
|
|
97
116
|
)
|
|
98
117
|
)
|
|
99
118
|
await asyncio.gather(
|
|
100
|
-
self.database.counters.drop(),
|
|
119
|
+
self.database[self.__collection_mapping["counters"]].drop(),
|
|
120
|
+
self.database[self.__collection_mapping["windows"]].drop(),
|
|
101
121
|
)
|
|
102
122
|
|
|
103
123
|
return cast(int, num_keys)
|
|
@@ -107,15 +127,21 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
107
127
|
:param key: the key to clear rate limits for
|
|
108
128
|
"""
|
|
109
129
|
await asyncio.gather(
|
|
110
|
-
self.database.counters.find_one_and_delete(
|
|
111
|
-
|
|
130
|
+
self.database[self.__collection_mapping["counters"]].find_one_and_delete(
|
|
131
|
+
{"_id": key}
|
|
132
|
+
),
|
|
133
|
+
self.database[self.__collection_mapping["windows"]].find_one_and_delete(
|
|
134
|
+
{"_id": key}
|
|
135
|
+
),
|
|
112
136
|
)
|
|
113
137
|
|
|
114
138
|
async def get_expiry(self, key: str) -> int:
|
|
115
139
|
"""
|
|
116
140
|
:param key: the key to get the expiry for
|
|
117
141
|
"""
|
|
118
|
-
counter = await self.database.counters.find_one(
|
|
142
|
+
counter = await self.database[self.__collection_mapping["counters"]].find_one(
|
|
143
|
+
{"_id": key}
|
|
144
|
+
)
|
|
119
145
|
expiry = (
|
|
120
146
|
counter["expireAt"]
|
|
121
147
|
if counter
|
|
@@ -128,7 +154,7 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
128
154
|
"""
|
|
129
155
|
:param key: the key to get the counter value for
|
|
130
156
|
"""
|
|
131
|
-
counter = await self.database.counters.find_one(
|
|
157
|
+
counter = await self.database[self.__collection_mapping["counters"]].find_one(
|
|
132
158
|
{
|
|
133
159
|
"_id": key,
|
|
134
160
|
"expireAt": {"$gte": datetime.datetime.now(datetime.timezone.utc)},
|
|
@@ -156,7 +182,9 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
156
182
|
seconds=expiry
|
|
157
183
|
)
|
|
158
184
|
|
|
159
|
-
response = await self.database
|
|
185
|
+
response = await self.database[
|
|
186
|
+
self.__collection_mapping["counters"]
|
|
187
|
+
].find_one_and_update(
|
|
160
188
|
{"_id": key},
|
|
161
189
|
[
|
|
162
190
|
{
|
|
@@ -209,30 +237,34 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
209
237
|
:return: (start of window, number of acquired entries)
|
|
210
238
|
"""
|
|
211
239
|
timestamp = time.time()
|
|
212
|
-
result =
|
|
213
|
-
[
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"$
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"
|
|
221
|
-
|
|
240
|
+
result = (
|
|
241
|
+
await self.database[self.__collection_mapping["windows"]]
|
|
242
|
+
.aggregate(
|
|
243
|
+
[
|
|
244
|
+
{"$match": {"_id": key}},
|
|
245
|
+
{
|
|
246
|
+
"$project": {
|
|
247
|
+
"entries": {
|
|
248
|
+
"$filter": {
|
|
249
|
+
"input": "$entries",
|
|
250
|
+
"as": "entry",
|
|
251
|
+
"cond": {"$gte": ["$$entry", timestamp - expiry]},
|
|
252
|
+
}
|
|
222
253
|
}
|
|
223
254
|
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
255
|
+
},
|
|
256
|
+
{"$unwind": "$entries"},
|
|
257
|
+
{
|
|
258
|
+
"$group": {
|
|
259
|
+
"_id": "$_id",
|
|
260
|
+
"min": {"$min": "$entries"},
|
|
261
|
+
"count": {"$sum": 1},
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
]
|
|
265
|
+
)
|
|
266
|
+
.to_list(length=1)
|
|
267
|
+
)
|
|
236
268
|
|
|
237
269
|
if result:
|
|
238
270
|
return (int(result[0]["min"]), result[0]["count"])
|
|
@@ -266,7 +298,7 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
266
298
|
)
|
|
267
299
|
}
|
|
268
300
|
updates["$push"]["entries"]["$each"] = [timestamp] * amount
|
|
269
|
-
await self.database.windows.update_one(
|
|
301
|
+
await self.database[self.__collection_mapping["windows"]].update_one(
|
|
270
302
|
{
|
|
271
303
|
"_id": key,
|
|
272
304
|
"entries.%d" % (limit - amount): {
|
limits/aio/strategies.py
CHANGED
|
@@ -29,13 +29,14 @@ class RateLimiter(ABC):
|
|
|
29
29
|
raise NotImplementedError
|
|
30
30
|
|
|
31
31
|
@abstractmethod
|
|
32
|
-
async def test(self, item: RateLimitItem, *identifiers: str) -> bool:
|
|
32
|
+
async def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
33
33
|
"""
|
|
34
34
|
Check if the rate limit can be consumed
|
|
35
35
|
|
|
36
36
|
:param item: the rate limit item
|
|
37
37
|
:param identifiers: variable list of strings to uniquely identify the
|
|
38
38
|
limit
|
|
39
|
+
:param cost: The expected cost to be consumed, default 1
|
|
39
40
|
"""
|
|
40
41
|
raise NotImplementedError
|
|
41
42
|
|
|
@@ -86,13 +87,14 @@ class MovingWindowRateLimiter(RateLimiter):
|
|
|
86
87
|
item.key_for(*identifiers), item.amount, item.get_expiry(), amount=cost
|
|
87
88
|
)
|
|
88
89
|
|
|
89
|
-
async def test(self, item: RateLimitItem, *identifiers: str) -> bool:
|
|
90
|
+
async def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
90
91
|
"""
|
|
91
92
|
Check if the rate limit can be consumed
|
|
92
93
|
|
|
93
94
|
:param item: the rate limit item
|
|
94
95
|
:param identifiers: variable list of strings to uniquely identify the
|
|
95
96
|
limit
|
|
97
|
+
:param cost: The expected cost to be consumed, default 1
|
|
96
98
|
"""
|
|
97
99
|
res = await cast(MovingWindowSupport, self.storage).get_moving_window(
|
|
98
100
|
item.key_for(*identifiers),
|
|
@@ -101,7 +103,7 @@ class MovingWindowRateLimiter(RateLimiter):
|
|
|
101
103
|
)
|
|
102
104
|
amount = res[1]
|
|
103
105
|
|
|
104
|
-
return amount
|
|
106
|
+
return amount <= item.amount - cost
|
|
105
107
|
|
|
106
108
|
async def get_window_stats(
|
|
107
109
|
self, item: RateLimitItem, *identifiers: str
|
|
@@ -147,16 +149,19 @@ class FixedWindowRateLimiter(RateLimiter):
|
|
|
147
149
|
<= item.amount
|
|
148
150
|
)
|
|
149
151
|
|
|
150
|
-
async def test(self, item: RateLimitItem, *identifiers: str) -> bool:
|
|
152
|
+
async def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
151
153
|
"""
|
|
152
154
|
Check if the rate limit can be consumed
|
|
153
155
|
|
|
154
156
|
:param item: the rate limit item
|
|
155
157
|
:param identifiers: variable list of strings to uniquely identify the
|
|
156
158
|
limit
|
|
159
|
+
:param cost: The expected cost to be consumed, default 1
|
|
157
160
|
"""
|
|
158
161
|
|
|
159
|
-
return
|
|
162
|
+
return (
|
|
163
|
+
await self.storage.get(item.key_for(*identifiers)) < item.amount - cost + 1
|
|
164
|
+
)
|
|
160
165
|
|
|
161
166
|
async def get_window_stats(
|
|
162
167
|
self, item: RateLimitItem, *identifiers: str
|
limits/storage/mongodb.py
CHANGED
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
|
-
from deprecated.sphinx import versionadded
|
|
9
|
+
from deprecated.sphinx import versionadded, versionchanged
|
|
10
10
|
|
|
11
11
|
from limits.typing import (
|
|
12
12
|
Dict,
|
|
@@ -36,6 +36,8 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
36
36
|
self,
|
|
37
37
|
uri: str,
|
|
38
38
|
database_name: str = "limits",
|
|
39
|
+
counter_collection_name: str = "counters",
|
|
40
|
+
window_collection_name: str = "windows",
|
|
39
41
|
wrap_exceptions: bool = False,
|
|
40
42
|
**options: Union[int, str, bool],
|
|
41
43
|
) -> None:
|
|
@@ -44,6 +46,9 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
44
46
|
This uri is passed directly to :class:`~pymongo.mongo_client.MongoClient`
|
|
45
47
|
:param database_name: The database to use for storing the rate limit
|
|
46
48
|
collections.
|
|
49
|
+
:param counter_collection_name: The collection name to use for individual counters
|
|
50
|
+
used in fixed window strategies
|
|
51
|
+
:param window_collection_name: The collection name to use for moving window storage
|
|
47
52
|
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
48
53
|
:exc:`limits.errors.StorageError` before raising it.
|
|
49
54
|
:param options: all remaining keyword arguments are passed to the
|
|
@@ -53,6 +58,10 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
53
58
|
|
|
54
59
|
super().__init__(uri, wrap_exceptions=wrap_exceptions, **options)
|
|
55
60
|
self._database_name = database_name
|
|
61
|
+
self._collection_mapping = {
|
|
62
|
+
"counters": counter_collection_name,
|
|
63
|
+
"windows": window_collection_name,
|
|
64
|
+
}
|
|
56
65
|
self.lib = self.dependencies["pymongo"].module
|
|
57
66
|
self.lib_errors, _ = get_dependency("pymongo.errors")
|
|
58
67
|
self._storage_uri = uri
|
|
@@ -74,11 +83,11 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
74
83
|
|
|
75
84
|
@property
|
|
76
85
|
def counters(self) -> MongoCollection:
|
|
77
|
-
return self._database["counters"]
|
|
86
|
+
return self._database[self._collection_mapping["counters"]]
|
|
78
87
|
|
|
79
88
|
@property
|
|
80
89
|
def windows(self) -> MongoCollection:
|
|
81
|
-
return self._database["windows"]
|
|
90
|
+
return self._database[self._collection_mapping["windows"]]
|
|
82
91
|
|
|
83
92
|
@abstractmethod
|
|
84
93
|
def _init_mongo_client(
|
|
@@ -278,6 +287,10 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
278
287
|
|
|
279
288
|
|
|
280
289
|
@versionadded(version="2.1")
|
|
290
|
+
@versionchanged(
|
|
291
|
+
version="3.14.0",
|
|
292
|
+
reason="Added option to select custom collection names for windows & counters",
|
|
293
|
+
)
|
|
281
294
|
class MongoDBStorage(MongoDBStorageBase):
|
|
282
295
|
STORAGE_SCHEME = ["mongodb", "mongodb+srv"]
|
|
283
296
|
|
limits/storage/redis_cluster.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import urllib
|
|
2
|
-
import warnings
|
|
3
|
-
from typing import cast
|
|
4
2
|
|
|
5
3
|
from deprecated.sphinx import versionchanged
|
|
6
4
|
from packaging.version import Version
|
|
7
5
|
|
|
8
|
-
from limits.errors import ConfigurationError
|
|
9
6
|
from limits.storage.redis import RedisStorage
|
|
10
|
-
from limits.typing import Dict,
|
|
7
|
+
from limits.typing import Dict, Optional, Union
|
|
11
8
|
|
|
12
9
|
|
|
10
|
+
@versionchanged(
|
|
11
|
+
version="3.14.0",
|
|
12
|
+
reason="""
|
|
13
|
+
Dropped support for the :pypi:`redis-py-cluster` library
|
|
14
|
+
which has been abandoned/deprecated.
|
|
15
|
+
""",
|
|
16
|
+
)
|
|
13
17
|
@versionchanged(
|
|
14
18
|
version="2.5.0",
|
|
15
19
|
reason="""
|
|
@@ -37,13 +41,19 @@ class RedisClusterStorage(RedisStorage):
|
|
|
37
41
|
|
|
38
42
|
DEPENDENCIES = {
|
|
39
43
|
"redis": Version("4.2.0"),
|
|
40
|
-
"rediscluster": Version("2.0.0"), # Deprecated since 2.6.0
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
def __init__(
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
uri: str,
|
|
49
|
+
wrap_exceptions: bool = False,
|
|
50
|
+
**options: Union[float, str, bool],
|
|
51
|
+
) -> None:
|
|
44
52
|
"""
|
|
45
53
|
:param uri: url of the form
|
|
46
54
|
``redis+cluster://[:password]@host:port,host:port``
|
|
55
|
+
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
56
|
+
:exc:`limits.errors.StorageError` before raising it.
|
|
47
57
|
:param options: all remaining keyword arguments are passed
|
|
48
58
|
directly to the constructor of :class:`redis.cluster.RedisCluster`
|
|
49
59
|
:raise ConfigurationError: when the :pypi:`redis` library is not
|
|
@@ -64,53 +74,15 @@ class RedisClusterStorage(RedisStorage):
|
|
|
64
74
|
cluster_hosts.append((host, int(port)))
|
|
65
75
|
|
|
66
76
|
self.storage = None
|
|
67
|
-
self.
|
|
68
|
-
self.
|
|
69
|
-
|
|
77
|
+
merged_options = {**self.DEFAULT_OPTIONS, **parsed_auth, **options}
|
|
78
|
+
self.dependency = self.dependencies["redis"].module
|
|
79
|
+
startup_nodes = [self.dependency.cluster.ClusterNode(*c) for c in cluster_hosts]
|
|
80
|
+
self.storage = self.dependency.cluster.RedisCluster(
|
|
81
|
+
startup_nodes=startup_nodes, **merged_options
|
|
70
82
|
)
|
|
71
83
|
assert self.storage
|
|
72
84
|
self.initialize_storage(uri)
|
|
73
|
-
super(RedisStorage, self).__init__(uri, **options)
|
|
74
|
-
|
|
75
|
-
def __pick_storage(
|
|
76
|
-
self, cluster_hosts: List[Tuple[str, int]], **options: Union[float, str, bool]
|
|
77
|
-
) -> None:
|
|
78
|
-
try:
|
|
79
|
-
redis_py = self.dependencies["redis"].module
|
|
80
|
-
startup_nodes = [redis_py.cluster.ClusterNode(*c) for c in cluster_hosts]
|
|
81
|
-
self.storage = redis_py.cluster.RedisCluster(
|
|
82
|
-
startup_nodes=startup_nodes, **options
|
|
83
|
-
)
|
|
84
|
-
self.using_redis_py = True
|
|
85
|
-
return
|
|
86
|
-
except ConfigurationError: # pragma: no cover
|
|
87
|
-
self.__use_legacy_cluster_implementation(cluster_hosts, **options)
|
|
88
|
-
if not self.storage:
|
|
89
|
-
raise ConfigurationError(
|
|
90
|
-
(
|
|
91
|
-
"Unable to find an implementation for redis cluster"
|
|
92
|
-
" Cluster support requires either redis-py>=4.2 or"
|
|
93
|
-
" redis-py-cluster"
|
|
94
|
-
)
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
def __use_legacy_cluster_implementation(
|
|
98
|
-
self, cluster_hosts: List[Tuple[str, int]], **options: Union[float, str, bool]
|
|
99
|
-
) -> None: # pragma: no cover
|
|
100
|
-
redis_cluster = self.dependencies["rediscluster"].module
|
|
101
|
-
warnings.warn(
|
|
102
|
-
(
|
|
103
|
-
"Using redis-py-cluster is deprecated as the library has been"
|
|
104
|
-
" absorbed by redis-py (>=4.2). The support will be eventually "
|
|
105
|
-
" removed from the limits library and is no longer tested "
|
|
106
|
-
" against since version: 2.6. To get rid of this warning, "
|
|
107
|
-
" uninstall redis-py-cluster and ensure redis-py>=4.2.0 is installed"
|
|
108
|
-
)
|
|
109
|
-
)
|
|
110
|
-
self.storage = redis_cluster.RedisCluster(
|
|
111
|
-
startup_nodes=[{"host": c[0], "port": c[1]} for c in cluster_hosts],
|
|
112
|
-
**options,
|
|
113
|
-
)
|
|
85
|
+
super(RedisStorage, self).__init__(uri, wrap_exceptions, **options)
|
|
114
86
|
|
|
115
87
|
def reset(self) -> Optional[int]:
|
|
116
88
|
"""
|
|
@@ -125,15 +97,9 @@ class RedisClusterStorage(RedisStorage):
|
|
|
125
97
|
usage as it could be slow on very large data sets"""
|
|
126
98
|
|
|
127
99
|
prefix = self.prefixed_key("*")
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return count
|
|
135
|
-
else: # pragma: no cover
|
|
136
|
-
keys = self.storage.keys(prefix)
|
|
137
|
-
return cast(
|
|
138
|
-
int, sum([self.storage.delete(k.decode("utf-8")) for k in keys])
|
|
139
|
-
)
|
|
100
|
+
count = 0
|
|
101
|
+
for primary in self.storage.get_primaries():
|
|
102
|
+
node = self.storage.get_redis_connection(primary)
|
|
103
|
+
keys = node.keys(prefix)
|
|
104
|
+
count += sum([node.delete(k.decode("utf-8")) for k in keys])
|
|
105
|
+
return count
|
limits/storage/redis_sentinel.py
CHANGED
|
@@ -5,7 +5,7 @@ from packaging.version import Version
|
|
|
5
5
|
|
|
6
6
|
from limits.errors import ConfigurationError
|
|
7
7
|
from limits.storage.redis import RedisStorage
|
|
8
|
-
from limits.typing import Dict, Optional, Union
|
|
8
|
+
from limits.typing import Dict, Optional, Tuple, Type, Union
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
import redis.sentinel
|
|
@@ -86,6 +86,12 @@ class RedisSentinelStorage(RedisStorage):
|
|
|
86
86
|
self.use_replicas = use_replicas
|
|
87
87
|
self.initialize_storage(uri)
|
|
88
88
|
|
|
89
|
+
@property
|
|
90
|
+
def base_exceptions(
|
|
91
|
+
self,
|
|
92
|
+
) -> Union[Type[Exception], Tuple[Type[Exception], ...]]: # pragma: no cover
|
|
93
|
+
return self.dependencies["redis"].RedisError # type: ignore[no-any-return, attr-defined]
|
|
94
|
+
|
|
89
95
|
def get(self, key: str) -> int:
|
|
90
96
|
"""
|
|
91
97
|
:param key: the key to get the counter value for
|
limits/strategies.py
CHANGED
|
@@ -28,13 +28,14 @@ class RateLimiter(metaclass=ABCMeta):
|
|
|
28
28
|
raise NotImplementedError
|
|
29
29
|
|
|
30
30
|
@abstractmethod
|
|
31
|
-
def test(self, item: RateLimitItem, *identifiers: str) -> bool:
|
|
31
|
+
def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
32
32
|
"""
|
|
33
33
|
Check the rate limit without consuming from it.
|
|
34
34
|
|
|
35
35
|
:param item: The rate limit item
|
|
36
36
|
:param identifiers: variable list of strings to uniquely identify this
|
|
37
37
|
instance of the limit
|
|
38
|
+
:param cost: The expected cost to be consumed, default 1
|
|
38
39
|
"""
|
|
39
40
|
raise NotImplementedError
|
|
40
41
|
|
|
@@ -84,13 +85,14 @@ class MovingWindowRateLimiter(RateLimiter):
|
|
|
84
85
|
item.key_for(*identifiers), item.amount, item.get_expiry(), amount=cost
|
|
85
86
|
)
|
|
86
87
|
|
|
87
|
-
def test(self, item: RateLimitItem, *identifiers: str) -> bool:
|
|
88
|
+
def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
88
89
|
"""
|
|
89
90
|
Check if the rate limit can be consumed
|
|
90
91
|
|
|
91
92
|
:param item: The rate limit item
|
|
92
93
|
:param identifiers: variable list of strings to uniquely identify this
|
|
93
94
|
instance of the limit
|
|
95
|
+
:param cost: The expected cost to be consumed, default 1
|
|
94
96
|
"""
|
|
95
97
|
|
|
96
98
|
return (
|
|
@@ -99,7 +101,7 @@ class MovingWindowRateLimiter(RateLimiter):
|
|
|
99
101
|
item.amount,
|
|
100
102
|
item.get_expiry(),
|
|
101
103
|
)[1]
|
|
102
|
-
|
|
104
|
+
<= item.amount - cost
|
|
103
105
|
)
|
|
104
106
|
|
|
105
107
|
def get_window_stats(self, item: RateLimitItem, *identifiers: str) -> WindowStats:
|
|
@@ -144,16 +146,17 @@ class FixedWindowRateLimiter(RateLimiter):
|
|
|
144
146
|
<= item.amount
|
|
145
147
|
)
|
|
146
148
|
|
|
147
|
-
def test(self, item: RateLimitItem, *identifiers: str) -> bool:
|
|
149
|
+
def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
148
150
|
"""
|
|
149
151
|
Check if the rate limit can be consumed
|
|
150
152
|
|
|
151
153
|
:param item: The rate limit item
|
|
152
154
|
:param identifiers: variable list of strings to uniquely identify this
|
|
153
155
|
instance of the limit
|
|
156
|
+
:param cost: The expected cost to be consumed, default 1
|
|
154
157
|
"""
|
|
155
158
|
|
|
156
|
-
return self.storage.get(item.key_for(*identifiers)) < item.amount
|
|
159
|
+
return self.storage.get(item.key_for(*identifiers)) < item.amount - cost + 1
|
|
157
160
|
|
|
158
161
|
def get_window_stats(self, item: RateLimitItem, *identifiers: str) -> WindowStats:
|
|
159
162
|
"""
|
limits/typing.py
CHANGED
|
@@ -110,8 +110,8 @@ class ScriptP(Protocol[R_co]):
|
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
MongoClient: TypeAlias = "pymongo.MongoClient[Dict[str, Any]]" # type:ignore[misc]
|
|
113
|
-
MongoDatabase: TypeAlias = "pymongo.database.Database[Dict[str, Any]]" # type:ignore
|
|
114
|
-
MongoCollection: TypeAlias = "pymongo.collection.Collection[Dict[str, Any]]" # type:ignore
|
|
113
|
+
MongoDatabase: TypeAlias = "pymongo.database.Database[Dict[str, Any]]" # type:ignore
|
|
114
|
+
MongoCollection: TypeAlias = "pymongo.collection.Collection[Dict[str, Any]]" # type:ignore
|
|
115
115
|
|
|
116
116
|
__all__ = [
|
|
117
117
|
"AsyncRedisClient",
|
limits/util.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
""" """
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
+
import importlib.resources
|
|
4
5
|
import re
|
|
5
6
|
import sys
|
|
6
7
|
from collections import UserDict
|
|
7
8
|
from types import ModuleType
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
10
|
-
import importlib_resources
|
|
11
11
|
from packaging.version import Version
|
|
12
12
|
|
|
13
13
|
from limits.typing import Dict, List, NamedTuple, Optional, Tuple, Type, Union
|
|
@@ -142,7 +142,7 @@ def get_dependency(module_path: str) -> Tuple[Optional[ModuleType], Optional[Ver
|
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
def get_package_data(path: str) -> bytes:
|
|
145
|
-
return
|
|
145
|
+
return importlib.resources.files("limits").joinpath(path).read_bytes()
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
def parse_many(limit_string: str) -> List[RateLimitItem]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: limits
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.14.0
|
|
4
4
|
Summary: Rate limiting utilities
|
|
5
5
|
Home-page: https://limits.readthedocs.org
|
|
6
6
|
Author: Ali-Akber Saifee
|
|
@@ -20,10 +20,9 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
24
|
License-File: LICENSE.txt
|
|
25
25
|
Requires-Dist: deprecated >=1.2
|
|
26
|
-
Requires-Dist: importlib-resources >=1.3
|
|
27
26
|
Requires-Dist: packaging <25,>=21
|
|
28
27
|
Requires-Dist: typing-extensions
|
|
29
28
|
Provides-Extra: all
|
|
@@ -36,12 +35,12 @@ Requires-Dist: coredis <5,>=3.4.0 ; extra == 'all'
|
|
|
36
35
|
Requires-Dist: motor <4,>=3 ; extra == 'all'
|
|
37
36
|
Requires-Dist: aetcd ; extra == 'all'
|
|
38
37
|
Requires-Dist: emcache >=0.6.1 ; (python_version < "3.11") and extra == 'all'
|
|
39
|
-
Requires-Dist: emcache >=1 ; (python_version >= "3.11") and extra == 'all'
|
|
38
|
+
Requires-Dist: emcache >=1 ; (python_version >= "3.11" and python_version < "3.13.0") and extra == 'all'
|
|
40
39
|
Provides-Extra: async-etcd
|
|
41
40
|
Requires-Dist: aetcd ; extra == 'async-etcd'
|
|
42
41
|
Provides-Extra: async-memcached
|
|
43
42
|
Requires-Dist: emcache >=0.6.1 ; (python_version < "3.11") and extra == 'async-memcached'
|
|
44
|
-
Requires-Dist: emcache >=1 ; (python_version >= "3.11") and extra == 'async-memcached'
|
|
43
|
+
Requires-Dist: emcache >=1 ; (python_version >= "3.11" and python_version < "3.13.0") and extra == 'async-memcached'
|
|
45
44
|
Provides-Extra: async-mongodb
|
|
46
45
|
Requires-Dist: motor <4,>=3 ; extra == 'async-mongodb'
|
|
47
46
|
Provides-Extra: async-redis
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
limits/__init__.py,sha256=j_yVhgN9pdz8o5rQjVwdJTBSq8F-CTzof9kkiYgjRbw,728
|
|
2
|
-
limits/_version.py,sha256=
|
|
2
|
+
limits/_version.py,sha256=xQZScpdLAtlbMB-VTWYGmvTmdMnGvXsZY1qG4iWHZKo,498
|
|
3
3
|
limits/errors.py,sha256=xCKGOVJiD-g8FlsQQb17AW2pTUvalYSuizPpvEVoYJE,626
|
|
4
4
|
limits/limits.py,sha256=ZsXESq2e1ji7c2ZKjSkIAasCjiLdjVLPUa9oah_I8U4,4943
|
|
5
5
|
limits/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
limits/strategies.py,sha256=
|
|
7
|
-
limits/typing.py,sha256=
|
|
8
|
-
limits/util.py,sha256=
|
|
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
9
|
limits/version.py,sha256=YwkF3dtq1KGzvmL3iVGctA8NNtGlK_0arrzZkZGVjUs,47
|
|
10
10
|
limits/aio/__init__.py,sha256=IOetunwQy1c5GefzitK8lewbTzHGiE-kmE9NlqSdr3U,82
|
|
11
|
-
limits/aio/strategies.py,sha256=
|
|
11
|
+
limits/aio/strategies.py,sha256=SHjmJnmy7Nh4tBydkA-0qPaULYcLOAM91T4RPybq0Sg,6768
|
|
12
12
|
limits/aio/storage/__init__.py,sha256=CbtuSlVl1jPyN_vsEI_ApWblDblVaL46xcZ2M_oM0V8,595
|
|
13
13
|
limits/aio/storage/base.py,sha256=xdYpBBonyMjxE9iT-2oZjm6x29aDU6Xd09MeBYbZcMo,4817
|
|
14
14
|
limits/aio/storage/etcd.py,sha256=Rjb_EYKFRr4F2Z6zvAPP9vQOyXJQHaju3VjxxUs75_c,4791
|
|
15
15
|
limits/aio/storage/memcached.py,sha256=6aTlACfCtchdcZqoisnei0MOlCH7yLV9A1yCjOE5f9g,4802
|
|
16
16
|
limits/aio/storage/memory.py,sha256=DlmWluqUwBUWQIQ6XZi-mPrb15vfzBhA4iAKhBELDnE,5856
|
|
17
|
-
limits/aio/storage/mongodb.py,sha256=
|
|
17
|
+
limits/aio/storage/mongodb.py,sha256=gZq9Ky3J7j0xfqIqB3ULBbQ5VjHzIyT1c7aNFp01VKk,10764
|
|
18
18
|
limits/aio/storage/redis.py,sha256=jkqtdIwTpfXTXwgTWTA1Jlc3Lpc-vnu4XRy6CIptiZA,15651
|
|
19
19
|
limits/resources/redis/lua_scripts/acquire_moving_window.lua,sha256=5CFJX7D6T6RG5SFr6eVZ6zepmI1EkGWmKeVEO4QNrWo,483
|
|
20
20
|
limits/resources/redis/lua_scripts/clear_keys.lua,sha256=zU0cVfLGmapRQF9x9u0GclapM_IB2pJLszNzVQ1QRK4,184
|
|
@@ -25,13 +25,13 @@ limits/storage/base.py,sha256=fDdYLa-RrnjhBTO1hE5aTTM8q8n3M5HD-65KyWWXBtg,4627
|
|
|
25
25
|
limits/storage/etcd.py,sha256=wkC_mj4Tsf2nwUKByMiHiGzA40N3mDepEwdLmvH8wmw,4484
|
|
26
26
|
limits/storage/memcached.py,sha256=bMzfZgYa_EWcZAjSZLcygpk3hpeOAErBpRE8dVwyXQs,6640
|
|
27
27
|
limits/storage/memory.py,sha256=R16E-Ccnmn1-LlolkFf-kB1-QHh8eiwFFLYVv0PuFD0,5561
|
|
28
|
-
limits/storage/mongodb.py,sha256=
|
|
28
|
+
limits/storage/mongodb.py,sha256=t8ey5-gYrJcmyvwJkqje0-TR-UMYvBF900a_zEXAYPI,9873
|
|
29
29
|
limits/storage/redis.py,sha256=3zJ1gDMDepT_pGN9d2aAN7Pea7tMBI49VK60IHv-Ooc,8452
|
|
30
|
-
limits/storage/redis_cluster.py,sha256=
|
|
31
|
-
limits/storage/redis_sentinel.py,sha256=
|
|
30
|
+
limits/storage/redis_cluster.py,sha256=MsiEpwHphQd0P88AwGw1NVSi3UwVrhsg-pvzkHxU2kw,3739
|
|
31
|
+
limits/storage/redis_sentinel.py,sha256=lI7y7x9VGDCGq_-WRb6jR6cDgnzsOC1KGaRbwE69DNk,4122
|
|
32
32
|
limits/storage/registry.py,sha256=xcBcxuu6srqmoS4WqDpkCXnRLB19ctH98v21P8S9kS8,708
|
|
33
|
-
limits-3.
|
|
34
|
-
limits-3.
|
|
35
|
-
limits-3.
|
|
36
|
-
limits-3.
|
|
37
|
-
limits-3.
|
|
33
|
+
limits-3.14.0.dist-info/LICENSE.txt,sha256=T6i7kq7F5gIPfcno9FCxU5Hcwm22Bjq0uHZV3ElcjsQ,1061
|
|
34
|
+
limits-3.14.0.dist-info/METADATA,sha256=52wQlpKMn5JwhkgyJN-FDcGgUnImn7Ce2j5yz2BqGKg,7189
|
|
35
|
+
limits-3.14.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
36
|
+
limits-3.14.0.dist-info/top_level.txt,sha256=C7g5ahldPoU2s6iWTaJayUrbGmPK1d6e9t5Nn0vQ2jM,7
|
|
37
|
+
limits-3.14.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|