limits 3.11.0__py3-none-any.whl → 3.13.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/base.py +1 -1
- limits/aio/strategies.py +10 -5
- limits/storage/__init__.py +2 -1
- limits/storage/mongodb.py +53 -16
- limits/strategies.py +8 -5
- limits/typing.py +10 -1
- {limits-3.11.0.dist-info → limits-3.13.0.dist-info}/METADATA +1 -1
- {limits-3.11.0.dist-info → limits-3.13.0.dist-info}/RECORD +12 -12
- {limits-3.11.0.dist-info → limits-3.13.0.dist-info}/WHEEL +1 -1
- {limits-3.11.0.dist-info → limits-3.13.0.dist-info}/LICENSE.txt +0 -0
- {limits-3.11.0.dist-info → limits-3.13.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-06-22T18:39:54-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "3.
|
|
14
|
+
"full-revisionid": "7b87c4d37659ae5fe0a8bf7216bfff789facd5f3",
|
|
15
|
+
"version": "3.13.0"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
limits/aio/storage/base.py
CHANGED
|
@@ -27,7 +27,7 @@ def _wrap_errors(
|
|
|
27
27
|
fn: Callable[P, Awaitable[R]],
|
|
28
28
|
) -> Callable[P, Awaitable[R]]:
|
|
29
29
|
@functools.wraps(fn)
|
|
30
|
-
async def inner(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
30
|
+
async def inner(*args: P.args, **kwargs: P.kwargs) -> R: # type: ignore[misc]
|
|
31
31
|
try:
|
|
32
32
|
return await fn(*args, **kwargs)
|
|
33
33
|
except storage.base_exceptions as exc:
|
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/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ from .base import MovingWindowSupport, Storage
|
|
|
13
13
|
from .etcd import EtcdStorage
|
|
14
14
|
from .memcached import MemcachedStorage
|
|
15
15
|
from .memory import MemoryStorage
|
|
16
|
-
from .mongodb import MongoDBStorage
|
|
16
|
+
from .mongodb import MongoDBStorage, MongoDBStorageBase
|
|
17
17
|
from .redis import RedisStorage
|
|
18
18
|
from .redis_cluster import RedisClusterStorage
|
|
19
19
|
from .redis_sentinel import RedisSentinelStorage
|
|
@@ -68,6 +68,7 @@ __all__ = [
|
|
|
68
68
|
"Storage",
|
|
69
69
|
"MovingWindowSupport",
|
|
70
70
|
"EtcdStorage",
|
|
71
|
+
"MongoDBStorageBase",
|
|
71
72
|
"MemoryStorage",
|
|
72
73
|
"MongoDBStorage",
|
|
73
74
|
"RedisStorage",
|
limits/storage/mongodb.py
CHANGED
|
@@ -3,29 +3,33 @@ from __future__ import annotations
|
|
|
3
3
|
import calendar
|
|
4
4
|
import datetime
|
|
5
5
|
import time
|
|
6
|
-
from
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any, cast
|
|
7
8
|
|
|
8
9
|
from deprecated.sphinx import versionadded
|
|
9
10
|
|
|
10
|
-
from limits.typing import
|
|
11
|
+
from limits.typing import (
|
|
12
|
+
Dict,
|
|
13
|
+
MongoClient,
|
|
14
|
+
MongoCollection,
|
|
15
|
+
MongoDatabase,
|
|
16
|
+
Optional,
|
|
17
|
+
Tuple,
|
|
18
|
+
Type,
|
|
19
|
+
Union,
|
|
20
|
+
)
|
|
11
21
|
|
|
12
22
|
from ..util import get_dependency
|
|
13
23
|
from .base import MovingWindowSupport, Storage
|
|
14
24
|
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
import pymongo
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
@versionadded(version="2.1")
|
|
20
|
-
class MongoDBStorage(Storage, MovingWindowSupport):
|
|
26
|
+
class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
21
27
|
"""
|
|
22
28
|
Rate limit storage with MongoDB as backend.
|
|
23
29
|
|
|
24
30
|
Depends on :pypi:`pymongo`.
|
|
25
31
|
"""
|
|
26
32
|
|
|
27
|
-
STORAGE_SCHEME = ["mongodb", "mongodb+srv"]
|
|
28
|
-
|
|
29
33
|
DEPENDENCIES = ["pymongo"]
|
|
30
34
|
|
|
31
35
|
def __init__(
|
|
@@ -48,16 +52,39 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
48
52
|
"""
|
|
49
53
|
|
|
50
54
|
super().__init__(uri, wrap_exceptions=wrap_exceptions, **options)
|
|
51
|
-
|
|
55
|
+
self._database_name = database_name
|
|
52
56
|
self.lib = self.dependencies["pymongo"].module
|
|
53
57
|
self.lib_errors, _ = get_dependency("pymongo.errors")
|
|
58
|
+
self._storage_uri = uri
|
|
59
|
+
self._storage_options = options
|
|
60
|
+
self._storage: Optional[MongoClient] = None
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
@property
|
|
63
|
+
def storage(self) -> MongoClient:
|
|
64
|
+
if self._storage is None:
|
|
65
|
+
self._storage = self._init_mongo_client(
|
|
66
|
+
self._storage_uri, **self._storage_options
|
|
67
|
+
)
|
|
68
|
+
self.__initialize_database()
|
|
69
|
+
return self._storage
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def _database(self) -> MongoDatabase:
|
|
73
|
+
return self.storage[self._database_name]
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def counters(self) -> MongoCollection:
|
|
77
|
+
return self._database["counters"]
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def windows(self) -> MongoCollection:
|
|
81
|
+
return self._database["windows"]
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def _init_mongo_client(
|
|
85
|
+
self, uri: Optional[str], **options: Union[int, str, bool]
|
|
86
|
+
) -> MongoClient:
|
|
87
|
+
raise NotImplementedError()
|
|
61
88
|
|
|
62
89
|
@property
|
|
63
90
|
def base_exceptions(
|
|
@@ -248,3 +275,13 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
248
275
|
return True
|
|
249
276
|
except self.lib.errors.DuplicateKeyError:
|
|
250
277
|
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@versionadded(version="2.1")
|
|
281
|
+
class MongoDBStorage(MongoDBStorageBase):
|
|
282
|
+
STORAGE_SCHEME = ["mongodb", "mongodb+srv"]
|
|
283
|
+
|
|
284
|
+
def _init_mongo_client(
|
|
285
|
+
self, uri: Optional[str], **options: Union[int, str, bool]
|
|
286
|
+
) -> MongoClient:
|
|
287
|
+
return cast(MongoClient, self.lib.MongoClient(uri, **options))
|
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import (
|
|
2
2
|
TYPE_CHECKING,
|
|
3
|
+
Any,
|
|
3
4
|
Awaitable,
|
|
4
5
|
Callable,
|
|
5
6
|
Dict,
|
|
@@ -12,7 +13,7 @@ from typing import (
|
|
|
12
13
|
Union,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
|
-
from typing_extensions import ClassVar, Counter, ParamSpec, Protocol
|
|
16
|
+
from typing_extensions import ClassVar, Counter, ParamSpec, Protocol, TypeAlias
|
|
16
17
|
|
|
17
18
|
Serializable = Union[int, str, float]
|
|
18
19
|
|
|
@@ -24,6 +25,7 @@ P = ParamSpec("P")
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
import coredis
|
|
26
27
|
import coredis.commands.script
|
|
28
|
+
import pymongo
|
|
27
29
|
import redis
|
|
28
30
|
|
|
29
31
|
|
|
@@ -107,6 +109,10 @@ class ScriptP(Protocol[R_co]):
|
|
|
107
109
|
def __call__(self, keys: List[Serializable], args: List[Serializable]) -> R_co: ...
|
|
108
110
|
|
|
109
111
|
|
|
112
|
+
MongoClient: TypeAlias = "pymongo.MongoClient[Dict[str, Any]]" # type:ignore[misc]
|
|
113
|
+
MongoDatabase: TypeAlias = "pymongo.database.Database[Dict[str, Any]]" # type:ignore[misc]
|
|
114
|
+
MongoCollection: TypeAlias = "pymongo.collection.Collection[Dict[str, Any]]" # type:ignore[misc]
|
|
115
|
+
|
|
110
116
|
__all__ = [
|
|
111
117
|
"AsyncRedisClient",
|
|
112
118
|
"Awaitable",
|
|
@@ -118,6 +124,9 @@ __all__ = [
|
|
|
118
124
|
"ItemP",
|
|
119
125
|
"List",
|
|
120
126
|
"MemcachedClientP",
|
|
127
|
+
"MongoClient",
|
|
128
|
+
"MongoCollection",
|
|
129
|
+
"MongoDatabase",
|
|
121
130
|
"NamedTuple",
|
|
122
131
|
"Optional",
|
|
123
132
|
"P",
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
limits/__init__.py,sha256=j_yVhgN9pdz8o5rQjVwdJTBSq8F-CTzof9kkiYgjRbw,728
|
|
2
|
-
limits/_version.py,sha256=
|
|
2
|
+
limits/_version.py,sha256=WegVz4YKhtYN102E8QvX81l0oVnN2UGchIOm2eVCePg,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=
|
|
6
|
+
limits/strategies.py,sha256=Zy6PIhkysPbxnMzFjyXEsxMM6jhRoQ5XT5WskTNruK0,6949
|
|
7
|
+
limits/typing.py,sha256=nwJLek44QIg3869AbOSvPwotfp6JR7vEHz_UgZBiqgg,3287
|
|
8
8
|
limits/util.py,sha256=xMRR5bKksYcnzY0H0xORDGvRFF5btiBognY2sSd38NE,5743
|
|
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
|
-
limits/aio/storage/base.py,sha256=
|
|
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
|
|
@@ -20,18 +20,18 @@ limits/resources/redis/lua_scripts/acquire_moving_window.lua,sha256=5CFJX7D6T6RG
|
|
|
20
20
|
limits/resources/redis/lua_scripts/clear_keys.lua,sha256=zU0cVfLGmapRQF9x9u0GclapM_IB2pJLszNzVQ1QRK4,184
|
|
21
21
|
limits/resources/redis/lua_scripts/incr_expire.lua,sha256=Uq9NcrrcDI-F87TDAJexoSJn2SDgeXIUEYozCp9S3oA,195
|
|
22
22
|
limits/resources/redis/lua_scripts/moving_window.lua,sha256=iAInenlVd_fFDi15APpRWbOuPUz_G3nFnVAqb7wOedA,398
|
|
23
|
-
limits/storage/__init__.py,sha256=
|
|
23
|
+
limits/storage/__init__.py,sha256=XAW1jVDMLFkPr_Tl1SXpg_p4Y3nhEatTSYq1MlnYJcA,2564
|
|
24
24
|
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=QWd_SW--P86SMPDrDrBQS-2xKbWY3cw9x71KsI506zY,9213
|
|
29
29
|
limits/storage/redis.py,sha256=3zJ1gDMDepT_pGN9d2aAN7Pea7tMBI49VK60IHv-Ooc,8452
|
|
30
30
|
limits/storage/redis_cluster.py,sha256=KwhWV0v3_TliRyS3OU15IlpeC8gRQr29U4FkcME01fo,5380
|
|
31
31
|
limits/storage/redis_sentinel.py,sha256=7PVB0hBl0I_enhN_h9QSJTE7zGuYtjkebotTqxm2iZo,3875
|
|
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.13.0.dist-info/LICENSE.txt,sha256=T6i7kq7F5gIPfcno9FCxU5Hcwm22Bjq0uHZV3ElcjsQ,1061
|
|
34
|
+
limits-3.13.0.dist-info/METADATA,sha256=vQsvBw6YR-jK6cCIusRrIjc-UG-aSL4PoHqFfYKzvjE,7170
|
|
35
|
+
limits-3.13.0.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
|
|
36
|
+
limits-3.13.0.dist-info/top_level.txt,sha256=C7g5ahldPoU2s6iWTaJayUrbGmPK1d6e9t5Nn0vQ2jM,7
|
|
37
|
+
limits-3.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|