limits 5.0.0rc1__py3-none-any.whl → 5.0.0rc2__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/{memcached.py → memcached/__init__.py} +43 -113
- limits/aio/storage/memcached/bridge.py +73 -0
- limits/aio/storage/memcached/emcache.py +112 -0
- limits/aio/storage/memcached/memcachio.py +104 -0
- limits/aio/storage/memory.py +29 -18
- limits/storage/memcached.py +2 -0
- limits/storage/memory.py +5 -5
- limits/typing.py +1 -0
- {limits-5.0.0rc1.dist-info → limits-5.0.0rc2.dist-info}/METADATA +2 -1
- {limits-5.0.0rc1.dist-info → limits-5.0.0rc2.dist-info}/RECORD +14 -11
- {limits-5.0.0rc1.dist-info → limits-5.0.0rc2.dist-info}/WHEEL +0 -0
- {limits-5.0.0rc1.dist-info → limits-5.0.0rc2.dist-info}/licenses/LICENSE.txt +0 -0
- {limits-5.0.0rc1.dist-info → limits-5.0.0rc2.dist-info}/top_level.txt +0 -0
limits/_version.py
CHANGED
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-04-
|
|
11
|
+
"date": "2025-04-15T12:47:18-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "5.0.
|
|
14
|
+
"full-revisionid": "ca0e9ca30c696af1102471218171c07ce8ee7644",
|
|
15
|
+
"version": "5.0.0rc2"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
import
|
|
5
|
-
from collections.abc import Iterable
|
|
6
|
-
from math import ceil, floor
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from math import floor
|
|
8
5
|
|
|
9
6
|
from deprecated.sphinx import versionadded, versionchanged
|
|
7
|
+
from packaging.version import Version
|
|
10
8
|
|
|
11
|
-
from limits.aio.storage
|
|
9
|
+
from limits.aio.storage import SlidingWindowCounterSupport, Storage
|
|
10
|
+
from limits.aio.storage.memcached.bridge import MemcachedBridge
|
|
11
|
+
from limits.aio.storage.memcached.emcache import EmcacheBridge
|
|
12
|
+
from limits.aio.storage.memcached.memcachio import MemcachioBridge
|
|
12
13
|
from limits.storage.base import TimestampedSlidingWindow
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
import memcachio
|
|
14
|
+
from limits.typing import Literal
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
@versionadded(version="2.1")
|
|
19
18
|
@versionchanged(
|
|
20
19
|
version="5.0",
|
|
21
|
-
reason="Switched to :pypi:`memcachio`
|
|
20
|
+
reason="Switched default implementation to :pypi:`memcachio`",
|
|
22
21
|
)
|
|
23
22
|
class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingWindow):
|
|
24
23
|
"""
|
|
@@ -30,12 +29,19 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
30
29
|
STORAGE_SCHEME = ["async+memcached"]
|
|
31
30
|
"""The storage scheme for memcached to be used in an async context"""
|
|
32
31
|
|
|
33
|
-
DEPENDENCIES =
|
|
32
|
+
DEPENDENCIES = {
|
|
33
|
+
"memcachio": Version("0.3"),
|
|
34
|
+
"emcache": Version("0.0"),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
bridge: MemcachedBridge
|
|
38
|
+
storage_exceptions: tuple[Exception, ...]
|
|
34
39
|
|
|
35
40
|
def __init__(
|
|
36
41
|
self,
|
|
37
42
|
uri: str,
|
|
38
43
|
wrap_exceptions: bool = False,
|
|
44
|
+
implementation: Literal["memcachio", "emcache"] = "memcachio",
|
|
39
45
|
**options: float | str | bool,
|
|
40
46
|
) -> None:
|
|
41
47
|
"""
|
|
@@ -43,77 +49,41 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
43
49
|
``async+memcached://host:port,host:port``
|
|
44
50
|
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
45
51
|
:exc:`limits.errors.StorageError` before raising it.
|
|
52
|
+
:param implementation: Whether to use the client implementation from
|
|
53
|
+
|
|
54
|
+
- ``memcachio``: :class:`memcachio.Client`
|
|
55
|
+
- ``emcache``: :class:`emcache.Client`
|
|
46
56
|
:param options: all remaining keyword arguments are passed
|
|
47
57
|
directly to the constructor of :class:`memcachio.Client`
|
|
48
58
|
:raise ConfigurationError: when :pypi:`memcachio` is not available
|
|
49
59
|
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self._options = options
|
|
59
|
-
self._storage = None
|
|
60
|
+
if implementation == "emcache":
|
|
61
|
+
self.bridge = EmcacheBridge(
|
|
62
|
+
uri, self.dependencies["emcache"].module, **options
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
self.bridge = MemcachioBridge(
|
|
66
|
+
uri, self.dependencies["memcachio"].module, **options
|
|
67
|
+
)
|
|
60
68
|
super().__init__(uri, wrap_exceptions=wrap_exceptions, **options)
|
|
61
|
-
self.dependency = self.dependencies["memcachio"].module
|
|
62
69
|
|
|
63
70
|
@property
|
|
64
71
|
def base_exceptions(
|
|
65
72
|
self,
|
|
66
73
|
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
67
|
-
return
|
|
68
|
-
self.dependency.errors.NoNodeAvailable,
|
|
69
|
-
self.dependency.errors.MemcachioConnectionError,
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
async def get_storage(self) -> memcachio.Client[bytes]:
|
|
73
|
-
if not self._storage:
|
|
74
|
-
self._storage = self.dependency.Client(
|
|
75
|
-
[(h, p) for h, p in self.hosts],
|
|
76
|
-
**self._options,
|
|
77
|
-
)
|
|
78
|
-
assert self._storage
|
|
79
|
-
return self._storage
|
|
74
|
+
return self.bridge.base_exceptions
|
|
80
75
|
|
|
81
76
|
async def get(self, key: str) -> int:
|
|
82
77
|
"""
|
|
83
78
|
:param key: the key to get the counter value for
|
|
84
79
|
"""
|
|
85
|
-
|
|
86
|
-
return item and int(item.value) or 0
|
|
87
|
-
|
|
88
|
-
async def get_many(
|
|
89
|
-
self, keys: Iterable[str]
|
|
90
|
-
) -> dict[bytes, memcachio.MemcachedItem[bytes]]:
|
|
91
|
-
"""
|
|
92
|
-
Return multiple counters at once
|
|
93
|
-
|
|
94
|
-
:param keys: the keys to get the counter values for
|
|
95
|
-
"""
|
|
96
|
-
return await (await self.get_storage()).get(*[k.encode("utf-8") for k in keys])
|
|
80
|
+
return await self.bridge.get(key)
|
|
97
81
|
|
|
98
82
|
async def clear(self, key: str) -> None:
|
|
99
83
|
"""
|
|
100
84
|
:param key: the key to clear rate limits for
|
|
101
85
|
"""
|
|
102
|
-
await
|
|
103
|
-
|
|
104
|
-
async def decr(self, key: str, amount: int = 1, noreply: bool = False) -> int:
|
|
105
|
-
"""
|
|
106
|
-
decrements the counter for a given rate limit key
|
|
107
|
-
|
|
108
|
-
retursn 0 if the key doesn't exist or if noreply is set to True
|
|
109
|
-
|
|
110
|
-
:param key: the key to decrement
|
|
111
|
-
:param amount: the number to decrement by
|
|
112
|
-
:param noreply: set to True to ignore the memcached response
|
|
113
|
-
"""
|
|
114
|
-
storage = await self.get_storage()
|
|
115
|
-
limit_key = key.encode("utf-8")
|
|
116
|
-
return await storage.decr(limit_key, amount, noreply=noreply) or 0
|
|
86
|
+
await self.bridge.clear(key)
|
|
117
87
|
|
|
118
88
|
async def incr(
|
|
119
89
|
self,
|
|
@@ -131,60 +101,22 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
131
101
|
:param amount: the number to increment by
|
|
132
102
|
:param set_expiration_key: if set to False, the expiration time won't be stored but the key will still expire
|
|
133
103
|
"""
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (value := (await storage.incr(limit_key, amount))) is None:
|
|
138
|
-
storage = await self.get_storage()
|
|
139
|
-
if await storage.add(limit_key, f"{amount}".encode(), expiry=ceil(expiry)):
|
|
140
|
-
if set_expiration_key:
|
|
141
|
-
await storage.set(
|
|
142
|
-
expire_key,
|
|
143
|
-
str(expiry + time.time()).encode("utf-8"),
|
|
144
|
-
expiry=ceil(expiry),
|
|
145
|
-
noreply=False,
|
|
146
|
-
)
|
|
147
|
-
return amount
|
|
148
|
-
else:
|
|
149
|
-
storage = await self.get_storage()
|
|
150
|
-
return await storage.incr(limit_key, amount) or amount
|
|
151
|
-
return value
|
|
104
|
+
return await self.bridge.incr(
|
|
105
|
+
key, expiry, amount, set_expiration_key=set_expiration_key
|
|
106
|
+
)
|
|
152
107
|
|
|
153
108
|
async def get_expiry(self, key: str) -> float:
|
|
154
109
|
"""
|
|
155
110
|
:param key: the key to get the expiry for
|
|
156
111
|
"""
|
|
157
|
-
|
|
158
|
-
expiration_key = self._expiration_key(key).encode("utf-8")
|
|
159
|
-
item = (await storage.get(expiration_key)).get(expiration_key, None)
|
|
160
|
-
|
|
161
|
-
return item and float(item.value) or time.time()
|
|
162
|
-
|
|
163
|
-
def _expiration_key(self, key: str) -> str:
|
|
164
|
-
"""
|
|
165
|
-
Return the expiration key for the given counter key.
|
|
166
|
-
|
|
167
|
-
Memcached doesn't natively return the expiration time or TTL for a given key,
|
|
168
|
-
so we implement the expiration time on a separate key.
|
|
169
|
-
"""
|
|
170
|
-
return key + "/expires"
|
|
171
|
-
|
|
172
|
-
async def check(self) -> bool:
|
|
173
|
-
"""
|
|
174
|
-
Check if storage is healthy by calling the ``get`` command
|
|
175
|
-
on the key ``limiter-check``
|
|
176
|
-
"""
|
|
177
|
-
try:
|
|
178
|
-
storage = await self.get_storage()
|
|
179
|
-
await storage.get(b"limiter-check")
|
|
180
|
-
|
|
181
|
-
return True
|
|
182
|
-
except: # noqa
|
|
183
|
-
return False
|
|
112
|
+
return await self.bridge.get_expiry(key)
|
|
184
113
|
|
|
185
114
|
async def reset(self) -> int | None:
|
|
186
115
|
raise NotImplementedError
|
|
187
116
|
|
|
117
|
+
async def check(self) -> bool:
|
|
118
|
+
return await self.bridge.check()
|
|
119
|
+
|
|
188
120
|
async def acquire_sliding_window_entry(
|
|
189
121
|
self,
|
|
190
122
|
key: str,
|
|
@@ -219,10 +151,10 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
219
151
|
previous_count * actualised_previous_ttl / expiry + current_count
|
|
220
152
|
)
|
|
221
153
|
if floor(weighted_count) > limit:
|
|
222
|
-
# Another hit won the race condition: revert the
|
|
154
|
+
# Another hit won the race condition: revert the increment and refuse this hit
|
|
223
155
|
# Limitation: during high concurrency at the end of the window,
|
|
224
156
|
# the counter is shifted and cannot be decremented, so less requests than expected are allowed.
|
|
225
|
-
await self.decr(current_key, amount, noreply=True)
|
|
157
|
+
await self.bridge.decr(current_key, amount, noreply=True)
|
|
226
158
|
return False
|
|
227
159
|
return True
|
|
228
160
|
|
|
@@ -238,13 +170,11 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
238
170
|
async def _get_sliding_window_info(
|
|
239
171
|
self, previous_key: str, current_key: str, expiry: int, now: float
|
|
240
172
|
) -> tuple[int, float, int, float]:
|
|
241
|
-
result = await self.get_many([previous_key, current_key])
|
|
173
|
+
result = await self.bridge.get_many([previous_key, current_key])
|
|
242
174
|
|
|
243
|
-
|
|
244
|
-
|
|
175
|
+
previous_count = result.get(previous_key.encode("utf-8"), 0)
|
|
176
|
+
current_count = result.get(current_key.encode("utf-8"), 0)
|
|
245
177
|
|
|
246
|
-
current_count = raw_current_count and int(raw_current_count.value) or 0
|
|
247
|
-
previous_count = raw_previous_count and int(raw_previous_count.value) or 0
|
|
248
178
|
if previous_count == 0:
|
|
249
179
|
previous_ttl = float(0)
|
|
250
180
|
else:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
from limits.typing import Iterable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MemcachedBridge(ABC):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
uri: str,
|
|
14
|
+
dependency: ModuleType,
|
|
15
|
+
**options: float | str | bool,
|
|
16
|
+
) -> None:
|
|
17
|
+
self.uri = uri
|
|
18
|
+
self.parsed_uri = urllib.parse.urlparse(self.uri)
|
|
19
|
+
self.dependency = dependency
|
|
20
|
+
self.hosts = []
|
|
21
|
+
self.options = options
|
|
22
|
+
|
|
23
|
+
sep = self.parsed_uri.netloc.strip().find("@") + 1
|
|
24
|
+
for loc in self.parsed_uri.netloc.strip()[sep:].split(","):
|
|
25
|
+
host, port = loc.split(":")
|
|
26
|
+
self.hosts.append((host, int(port)))
|
|
27
|
+
|
|
28
|
+
if self.parsed_uri.username:
|
|
29
|
+
self.options["username"] = self.parsed_uri.username
|
|
30
|
+
if self.parsed_uri.password:
|
|
31
|
+
self.options["password"] = self.parsed_uri.password
|
|
32
|
+
|
|
33
|
+
def _expiration_key(self, key: str) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Return the expiration key for the given counter key.
|
|
36
|
+
|
|
37
|
+
Memcached doesn't natively return the expiration time or TTL for a given key,
|
|
38
|
+
so we implement the expiration time on a separate key.
|
|
39
|
+
"""
|
|
40
|
+
return key + "/expires"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def base_exceptions(
|
|
45
|
+
self,
|
|
46
|
+
) -> type[Exception] | tuple[type[Exception], ...]: ...
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
async def get(self, key: str) -> int: ...
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
async def get_many(self, keys: Iterable[str]) -> dict[bytes, int]: ...
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def clear(self, key: str) -> None: ...
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def decr(self, key: str, amount: int = 1, noreply: bool = False) -> int: ...
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
async def incr(
|
|
62
|
+
self,
|
|
63
|
+
key: str,
|
|
64
|
+
expiry: float,
|
|
65
|
+
amount: int = 1,
|
|
66
|
+
set_expiration_key: bool = True,
|
|
67
|
+
) -> int: ...
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def get_expiry(self, key: str) -> float: ...
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
async def check(self) -> bool: ...
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from math import ceil
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
from limits.typing import TYPE_CHECKING, Iterable
|
|
8
|
+
|
|
9
|
+
from .bridge import MemcachedBridge
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import emcache
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EmcacheBridge(MemcachedBridge):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
uri: str,
|
|
19
|
+
dependency: ModuleType,
|
|
20
|
+
**options: float | str | bool,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(uri, dependency, **options)
|
|
23
|
+
self._storage = None
|
|
24
|
+
|
|
25
|
+
async def get_storage(self) -> emcache.Client:
|
|
26
|
+
if not self._storage:
|
|
27
|
+
self._storage = await self.dependency.create_client(
|
|
28
|
+
[self.dependency.MemcachedHostAddress(h, p) for h, p in self.hosts],
|
|
29
|
+
**self.options,
|
|
30
|
+
)
|
|
31
|
+
assert self._storage
|
|
32
|
+
return self._storage
|
|
33
|
+
|
|
34
|
+
async def get(self, key: str) -> int:
|
|
35
|
+
item = await (await self.get_storage()).get(key.encode("utf-8"))
|
|
36
|
+
return item and int(item.value) or 0
|
|
37
|
+
|
|
38
|
+
async def get_many(self, keys: Iterable[str]) -> dict[bytes, int]:
|
|
39
|
+
results = await (await self.get_storage()).get_many(
|
|
40
|
+
[k.encode("utf-8") for k in keys]
|
|
41
|
+
)
|
|
42
|
+
return {k: int(item.value) if item else 0 for k, item in results.items()}
|
|
43
|
+
|
|
44
|
+
async def clear(self, key: str) -> None:
|
|
45
|
+
try:
|
|
46
|
+
await (await self.get_storage()).delete(key.encode("utf-8"))
|
|
47
|
+
except self.dependency.NotFoundCommandError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
async def decr(self, key: str, amount: int = 1, noreply: bool = False) -> int:
|
|
51
|
+
storage = await self.get_storage()
|
|
52
|
+
limit_key = key.encode("utf-8")
|
|
53
|
+
try:
|
|
54
|
+
value = await storage.decrement(limit_key, amount, noreply=noreply) or 0
|
|
55
|
+
except self.dependency.NotFoundCommandError:
|
|
56
|
+
value = 0
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
async def incr(
|
|
60
|
+
self, key: str, expiry: float, amount: int = 1, set_expiration_key: bool = True
|
|
61
|
+
) -> int:
|
|
62
|
+
storage = await self.get_storage()
|
|
63
|
+
limit_key = key.encode("utf-8")
|
|
64
|
+
expire_key = self._expiration_key(key).encode()
|
|
65
|
+
try:
|
|
66
|
+
return await storage.increment(limit_key, amount) or amount
|
|
67
|
+
except self.dependency.NotFoundCommandError:
|
|
68
|
+
storage = await self.get_storage()
|
|
69
|
+
try:
|
|
70
|
+
await storage.add(limit_key, f"{amount}".encode(), exptime=ceil(expiry))
|
|
71
|
+
if set_expiration_key:
|
|
72
|
+
await storage.set(
|
|
73
|
+
expire_key,
|
|
74
|
+
str(expiry + time.time()).encode("utf-8"),
|
|
75
|
+
exptime=ceil(expiry),
|
|
76
|
+
noreply=False,
|
|
77
|
+
)
|
|
78
|
+
value = amount
|
|
79
|
+
except self.dependency.NotStoredStorageCommandError:
|
|
80
|
+
# Coult not add the key, probably because a concurrent call has added it
|
|
81
|
+
storage = await self.get_storage()
|
|
82
|
+
value = await storage.increment(limit_key, amount) or amount
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
async def get_expiry(self, key: str) -> float:
|
|
86
|
+
storage = await self.get_storage()
|
|
87
|
+
item = await storage.get(self._expiration_key(key).encode("utf-8"))
|
|
88
|
+
|
|
89
|
+
return item and float(item.value) or time.time()
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def base_exceptions(
|
|
94
|
+
self,
|
|
95
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
96
|
+
return (
|
|
97
|
+
self.dependency.ClusterNoAvailableNodes,
|
|
98
|
+
self.dependency.CommandError,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def check(self) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Check if storage is healthy by calling the ``get`` command
|
|
104
|
+
on the key ``limiter-check``
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
storage = await self.get_storage()
|
|
108
|
+
await storage.get(b"limiter-check")
|
|
109
|
+
|
|
110
|
+
return True
|
|
111
|
+
except: # noqa
|
|
112
|
+
return False
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from math import ceil
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable
|
|
7
|
+
|
|
8
|
+
from .bridge import MemcachedBridge
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import memcachio
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemcachioBridge(MemcachedBridge):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
uri: str,
|
|
18
|
+
dependency: ModuleType,
|
|
19
|
+
**options: float | str | bool,
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(uri, dependency, **options)
|
|
22
|
+
self._storage: memcachio.Client[bytes] | None = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def base_exceptions(
|
|
26
|
+
self,
|
|
27
|
+
) -> type[Exception] | tuple[type[Exception], ...]:
|
|
28
|
+
return (
|
|
29
|
+
self.dependency.errors.NoAvailableNodes,
|
|
30
|
+
self.dependency.errors.MemcachioConnectionError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async def get_storage(self) -> memcachio.Client[bytes]:
|
|
34
|
+
if not self._storage:
|
|
35
|
+
self._storage = self.dependency.Client(
|
|
36
|
+
[(h, p) for h, p in self.hosts],
|
|
37
|
+
**self.options,
|
|
38
|
+
)
|
|
39
|
+
assert self._storage
|
|
40
|
+
return self._storage
|
|
41
|
+
|
|
42
|
+
async def get(self, key: str) -> int:
|
|
43
|
+
return (await self.get_many([key])).get(key.encode("utf-8"), 0)
|
|
44
|
+
|
|
45
|
+
async def get_many(self, keys: Iterable[str]) -> dict[bytes, int]:
|
|
46
|
+
"""
|
|
47
|
+
Return multiple counters at once
|
|
48
|
+
|
|
49
|
+
:param keys: the keys to get the counter values for
|
|
50
|
+
"""
|
|
51
|
+
results = await (await self.get_storage()).get(
|
|
52
|
+
*[k.encode("utf-8") for k in keys]
|
|
53
|
+
)
|
|
54
|
+
return {k: int(v.value) for k, v in results.items()}
|
|
55
|
+
|
|
56
|
+
async def clear(self, key: str) -> None:
|
|
57
|
+
await (await self.get_storage()).delete(key.encode("utf-8"))
|
|
58
|
+
|
|
59
|
+
async def decr(self, key: str, amount: int = 1, noreply: bool = False) -> int:
|
|
60
|
+
storage = await self.get_storage()
|
|
61
|
+
limit_key = key.encode("utf-8")
|
|
62
|
+
return await storage.decr(limit_key, amount, noreply=noreply) or 0
|
|
63
|
+
|
|
64
|
+
async def incr(
|
|
65
|
+
self, key: str, expiry: float, amount: int = 1, set_expiration_key: bool = True
|
|
66
|
+
) -> int:
|
|
67
|
+
storage = await self.get_storage()
|
|
68
|
+
limit_key = key.encode("utf-8")
|
|
69
|
+
expire_key = self._expiration_key(key).encode()
|
|
70
|
+
if (value := (await storage.incr(limit_key, amount))) is None:
|
|
71
|
+
storage = await self.get_storage()
|
|
72
|
+
if await storage.add(limit_key, f"{amount}".encode(), expiry=ceil(expiry)):
|
|
73
|
+
if set_expiration_key:
|
|
74
|
+
await storage.set(
|
|
75
|
+
expire_key,
|
|
76
|
+
str(expiry + time.time()).encode("utf-8"),
|
|
77
|
+
expiry=ceil(expiry),
|
|
78
|
+
noreply=False,
|
|
79
|
+
)
|
|
80
|
+
return amount
|
|
81
|
+
else:
|
|
82
|
+
storage = await self.get_storage()
|
|
83
|
+
return await storage.incr(limit_key, amount) or amount
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
async def get_expiry(self, key: str) -> float:
|
|
87
|
+
storage = await self.get_storage()
|
|
88
|
+
expiration_key = self._expiration_key(key).encode("utf-8")
|
|
89
|
+
item = (await storage.get(expiration_key)).get(expiration_key, None)
|
|
90
|
+
|
|
91
|
+
return item and float(item.value) or time.time()
|
|
92
|
+
|
|
93
|
+
async def check(self) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Check if storage is healthy by calling the ``get`` command
|
|
96
|
+
on the key ``limiter-check``
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
storage = await self.get_storage()
|
|
100
|
+
await storage.get(b"limiter-check")
|
|
101
|
+
|
|
102
|
+
return True
|
|
103
|
+
except: # noqa
|
|
104
|
+
return False
|
limits/aio/storage/memory.py
CHANGED
|
@@ -62,25 +62,29 @@ class MemoryStorage(
|
|
|
62
62
|
asyncio.ensure_future(self.__schedule_expiry())
|
|
63
63
|
|
|
64
64
|
async def __expire_events(self) -> None:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
try:
|
|
66
|
+
now = time.time()
|
|
67
|
+
for key in list(self.events.keys()):
|
|
68
|
+
cutoff = await asyncio.to_thread(
|
|
69
|
+
lambda evts: bisect.bisect_left(
|
|
70
|
+
evts, -now, key=lambda event: -event.expiry
|
|
71
|
+
),
|
|
72
|
+
self.events[key],
|
|
73
|
+
)
|
|
74
|
+
async with self.locks[key]:
|
|
75
|
+
if self.events.get(key, []):
|
|
76
|
+
self.events[key] = self.events[key][:cutoff]
|
|
77
|
+
if not self.events.get(key, None):
|
|
78
|
+
self.events.pop(key, None)
|
|
79
|
+
self.locks.pop(key, None)
|
|
80
|
+
|
|
81
|
+
for key in list(self.expirations.keys()):
|
|
82
|
+
if self.expirations[key] <= time.time():
|
|
83
|
+
self.storage.pop(key, None)
|
|
84
|
+
self.expirations.pop(key, None)
|
|
77
85
|
self.locks.pop(key, None)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if self.expirations[key] <= time.time():
|
|
81
|
-
self.storage.pop(key, None)
|
|
82
|
-
self.expirations.pop(key, None)
|
|
83
|
-
self.locks.pop(key, None)
|
|
86
|
+
except asyncio.CancelledError:
|
|
87
|
+
return
|
|
84
88
|
|
|
85
89
|
async def __schedule_expiry(self) -> None:
|
|
86
90
|
if not self.timer or self.timer.done():
|
|
@@ -268,3 +272,10 @@ class MemoryStorage(
|
|
|
268
272
|
self.locks.clear()
|
|
269
273
|
|
|
270
274
|
return num_items
|
|
275
|
+
|
|
276
|
+
def __del__(self) -> None:
|
|
277
|
+
try:
|
|
278
|
+
if self.timer and not self.timer.done():
|
|
279
|
+
self.timer.cancel()
|
|
280
|
+
except RuntimeError: # noqa
|
|
281
|
+
pass
|
limits/storage/memcached.py
CHANGED
limits/storage/memory.py
CHANGED
|
@@ -57,11 +57,11 @@ class MemoryStorage(
|
|
|
57
57
|
def __expire_events(self) -> None:
|
|
58
58
|
for key in list(self.events.keys()):
|
|
59
59
|
with self.locks[key]:
|
|
60
|
-
events
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
if 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]
|
|
65
65
|
if not self.events.get(key, None):
|
|
66
66
|
self.locks.pop(key, None)
|
|
67
67
|
for key in list(self.expirations.keys()):
|
limits/typing.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: limits
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.0rc2
|
|
4
4
|
Summary: Rate limiting utilities
|
|
5
5
|
Home-page: https://limits.readthedocs.org
|
|
6
6
|
Author: Ali-Akber Saifee
|
|
@@ -274,5 +274,6 @@ Links
|
|
|
274
274
|
=====
|
|
275
275
|
|
|
276
276
|
* `Documentation <http://limits.readthedocs.org/en/latest>`_
|
|
277
|
+
* `Benchmarks <http://limits.readthedocs.org/en/latest/performance.html>`_
|
|
277
278
|
* `Changelog <http://limits.readthedocs.org/en/stable/changelog.html>`_
|
|
278
279
|
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
limits/__init__.py,sha256=gPUFrt02kHF_syLjiVRSs-S4UVGpRMcM2VMFNhF6G24,748
|
|
2
|
-
limits/_version.py,sha256=
|
|
2
|
+
limits/_version.py,sha256=NfHJ8jCFa69gQxFo8GoNxy5o_PAmF3e9sDd1Q4gjLTU,500
|
|
3
3
|
limits/errors.py,sha256=s1el9Vg0ly-z92guvnvYNgKi3_aVqpiw_sufemiLLTI,662
|
|
4
4
|
limits/limits.py,sha256=YzzZP8_ay_zlMMnnY2xhAcFTTFvFe5HEk8NQlvUTru4,4907
|
|
5
5
|
limits/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
limits/strategies.py,sha256=LeZ6lnE73EIQqQ8TfKaTzlxNvBMrZOOSXFB0l8D17fI,9946
|
|
7
|
-
limits/typing.py,sha256=
|
|
7
|
+
limits/typing.py,sha256=pVt5D23MhQSUGqi0MBG5FCSqDwta2ygu18BpKvJFxow,3283
|
|
8
8
|
limits/util.py,sha256=nk5QYvezFuXPq1OTEj04RrZFSWIH-khT0e_Dim6zGCw,6002
|
|
9
9
|
limits/version.py,sha256=YwkF3dtq1KGzvmL3iVGctA8NNtGlK_0arrzZkZGVjUs,47
|
|
10
10
|
limits/aio/__init__.py,sha256=yxvWb_ZmV245Hg2LqD365WC5IDllcGDMw6udJ1jNp1g,118
|
|
11
11
|
limits/aio/strategies.py,sha256=RzZExH2r6jnHra4SpDHqtZCC0Bo3085zUJYo2boAj6Y,9897
|
|
12
12
|
limits/aio/storage/__init__.py,sha256=vKeArUnN1ld_0mQOBBZPCjaQgM5xI1GBPM7_F2Ydz5c,646
|
|
13
13
|
limits/aio/storage/base.py,sha256=VfHpL9Z3RL76eKhoaSQKLKQsqcF5B2bnF6gfa-8ltWA,6296
|
|
14
|
-
limits/aio/storage/
|
|
15
|
-
limits/aio/storage/memory.py,sha256=HdkQPcjjHv1MhGoeYWOkBwwMj5e0tQbaO0OlECzh64A,9178
|
|
14
|
+
limits/aio/storage/memory.py,sha256=sWrDzOe-6Opy9uFmfP1S38IbN2_wNCBaIHTS4UTRy6g,9562
|
|
16
15
|
limits/aio/storage/mongodb.py,sha256=tIMfQrseONRMR2nuRmPO7ocp8dTCABfqBICS_kgp550,19141
|
|
16
|
+
limits/aio/storage/memcached/__init__.py,sha256=VMWsH4XpaPswtPV7cQmsfckhVRbOOrKvoUPYnGt5MRY,6611
|
|
17
|
+
limits/aio/storage/memcached/bridge.py,sha256=3CEruS6LvZWDQPGPLlwY4hemy6oN0WWduUE7t8vyXBI,2017
|
|
18
|
+
limits/aio/storage/memcached/emcache.py,sha256=J01jP-Udd2fLgamCh2CX9NEIvhN8eZVTzUok096Bbe4,3833
|
|
19
|
+
limits/aio/storage/memcached/memcachio.py,sha256=OoGVqOVG0pVX2McFeTGQ_AbiqQUu_FYwWItpQMtNV7g,3491
|
|
17
20
|
limits/aio/storage/redis/__init__.py,sha256=lwoKk91YLEBlZ3W6hCnQ1e7Gc6LxpvSzZZW16saCyR4,14143
|
|
18
21
|
limits/aio/storage/redis/bridge.py,sha256=eoRi9h2bSy194cVwoKgRYQV1HQ7SvwarL-4LeazrxeA,3145
|
|
19
22
|
limits/aio/storage/redis/coredis.py,sha256=IzfEyXBvQbr4QUWML9xAd87a2aHCvglOBEjAg-Vq4z0,7420
|
|
@@ -27,15 +30,15 @@ limits/resources/redis/lua_scripts/moving_window.lua,sha256=zlieQwfET0BC7sxpfiOu
|
|
|
27
30
|
limits/resources/redis/lua_scripts/sliding_window.lua,sha256=qG3Yg30Dq54QpRUcR9AOrKQ5bdJiaYpCacTm6Kxblvc,713
|
|
28
31
|
limits/storage/__init__.py,sha256=9iNxIlwzLQw2d54EcMa2LBJ47wiWCPOnHgn6ddqKkDI,2652
|
|
29
32
|
limits/storage/base.py,sha256=IdOL_iqR9KhaJO73M_h9c6OYe8Ox632pxx5uXaL9Dbo,6860
|
|
30
|
-
limits/storage/memcached.py,sha256=
|
|
31
|
-
limits/storage/memory.py,sha256=
|
|
33
|
+
limits/storage/memcached.py,sha256=5GUKGWS_BYTwUss2WmOlCwBtOieGT7AFUcpX65WYXdQ,10217
|
|
34
|
+
limits/storage/memory.py,sha256=rVlsirSp9LDhuqNFp6KMLR85fJc9xwrU58IHIVz6eq4,8719
|
|
32
35
|
limits/storage/mongodb.py,sha256=V4Ib_AwPFX6JpNI7oUUGJx_3MxD8EmYAi4Q6QcWnQ5U,18071
|
|
33
36
|
limits/storage/redis.py,sha256=i_6qh4S6JQd-lG6eRJdTPxNnZIAkm4G0cA0mfow9OOk,10389
|
|
34
37
|
limits/storage/redis_cluster.py,sha256=z6aONMl4p1AY78G3J0BbtK--uztz88krwnpiOsU61BM,4447
|
|
35
38
|
limits/storage/redis_sentinel.py,sha256=AN0WtwHN88TvXk0C2uUE8l5Jhsd1ZxU8XSqrEyQSR20,4327
|
|
36
39
|
limits/storage/registry.py,sha256=CxSaDBGR5aBJPFAIsfX9axCnbcThN3Bu-EH4wHrXtu8,650
|
|
37
|
-
limits-5.0.
|
|
38
|
-
limits-5.0.
|
|
39
|
-
limits-5.0.
|
|
40
|
-
limits-5.0.
|
|
41
|
-
limits-5.0.
|
|
40
|
+
limits-5.0.0rc2.dist-info/licenses/LICENSE.txt,sha256=T6i7kq7F5gIPfcno9FCxU5Hcwm22Bjq0uHZV3ElcjsQ,1061
|
|
41
|
+
limits-5.0.0rc2.dist-info/METADATA,sha256=0bbBoIEtEg06utKBNEGa7iCNVmbQZ6xLFaIfoxx5aDU,10903
|
|
42
|
+
limits-5.0.0rc2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
43
|
+
limits-5.0.0rc2.dist-info/top_level.txt,sha256=C7g5ahldPoU2s6iWTaJayUrbGmPK1d6e9t5Nn0vQ2jM,7
|
|
44
|
+
limits-5.0.0rc2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|