limits 4.7.3__py3-none-any.whl → 5.0.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.
@@ -1,281 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- import urllib.parse
5
- from collections.abc import Iterable
6
- from math import ceil, floor
7
-
8
- from deprecated.sphinx import versionadded
9
-
10
- from limits.aio.storage.base import SlidingWindowCounterSupport, Storage
11
- from limits.storage.base import TimestampedSlidingWindow
12
- from limits.typing import EmcacheClientP, ItemP
13
-
14
-
15
- @versionadded(version="2.1")
16
- class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingWindow):
17
- """
18
- Rate limit storage with memcached as backend.
19
-
20
- Depends on :pypi:`emcache`
21
- """
22
-
23
- STORAGE_SCHEME = ["async+memcached"]
24
- """The storage scheme for memcached to be used in an async context"""
25
-
26
- DEPENDENCIES = ["emcache"]
27
-
28
- def __init__(
29
- self,
30
- uri: str,
31
- wrap_exceptions: bool = False,
32
- **options: float | str | bool,
33
- ) -> None:
34
- """
35
- :param uri: memcached location of the form
36
- ``async+memcached://host:port,host:port``
37
- :param wrap_exceptions: Whether to wrap storage exceptions in
38
- :exc:`limits.errors.StorageError` before raising it.
39
- :param options: all remaining keyword arguments are passed
40
- directly to the constructor of :class:`emcache.Client`
41
- :raise ConfigurationError: when :pypi:`emcache` is not available
42
- """
43
- parsed = urllib.parse.urlparse(uri)
44
- self.hosts = []
45
-
46
- for host, port in (
47
- loc.split(":") for loc in parsed.netloc.strip().split(",") if loc.strip()
48
- ):
49
- self.hosts.append((host, int(port)))
50
-
51
- self._options = options
52
- self._storage = None
53
- super().__init__(uri, wrap_exceptions=wrap_exceptions, **options)
54
- self.dependency = self.dependencies["emcache"].module
55
-
56
- @property
57
- def base_exceptions(
58
- self,
59
- ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
60
- return (
61
- self.dependency.ClusterNoAvailableNodes,
62
- self.dependency.CommandError,
63
- )
64
-
65
- async def get_storage(self) -> EmcacheClientP:
66
- if not self._storage:
67
- self._storage = await self.dependency.create_client(
68
- [self.dependency.MemcachedHostAddress(h, p) for h, p in self.hosts],
69
- **self._options,
70
- )
71
- assert self._storage
72
- return self._storage
73
-
74
- async def get(self, key: str) -> int:
75
- """
76
- :param key: the key to get the counter value for
77
- """
78
- item = await (await self.get_storage()).get(key.encode("utf-8"))
79
-
80
- return item and int(item.value) or 0
81
-
82
- async def get_many(self, keys: Iterable[str]) -> dict[bytes, ItemP]:
83
- """
84
- Return multiple counters at once
85
-
86
- :param keys: the keys to get the counter values for
87
- """
88
- return await (await self.get_storage()).get_many(
89
- [k.encode("utf-8") for k in keys]
90
- )
91
-
92
- async def clear(self, key: str) -> None:
93
- """
94
- :param key: the key to clear rate limits for
95
- """
96
- try:
97
- await (await self.get_storage()).delete(key.encode("utf-8"))
98
- except self.dependency.NotFoundCommandError:
99
- pass
100
-
101
- async def decr(self, key: str, amount: int = 1, noreply: bool = False) -> int:
102
- """
103
- decrements the counter for a given rate limit key
104
-
105
- retursn 0 if the key doesn't exist or if noreply is set to True
106
-
107
- :param key: the key to decrement
108
- :param amount: the number to decrement by
109
- :param noreply: set to True to ignore the memcached response
110
- """
111
- storage = await self.get_storage()
112
- limit_key = key.encode("utf-8")
113
- try:
114
- value = await storage.decrement(limit_key, amount, noreply=noreply) or 0
115
- except self.dependency.NotFoundCommandError:
116
- value = 0
117
- return value
118
-
119
- async def incr(
120
- self,
121
- key: str,
122
- expiry: float,
123
- elastic_expiry: bool = False,
124
- amount: int = 1,
125
- set_expiration_key: bool = True,
126
- ) -> int:
127
- """
128
- increments the counter for a given rate limit key
129
-
130
- :param key: the key to increment
131
- :param expiry: amount in seconds for the key to expire in
132
- :param elastic_expiry: whether to keep extending the rate limit
133
- window every hit.
134
- :param amount: the number to increment by
135
- :param set_expiration_key: if set to False, the expiration time won't be stored but the key will still expire
136
- """
137
- storage = await self.get_storage()
138
- limit_key = key.encode("utf-8")
139
- expire_key = self._expiration_key(key).encode()
140
- value = None
141
- try:
142
- value = await storage.increment(limit_key, amount) or amount
143
- if elastic_expiry:
144
- await storage.touch(limit_key, exptime=ceil(expiry))
145
- if set_expiration_key:
146
- await storage.set(
147
- expire_key,
148
- str(expiry + time.time()).encode("utf-8"),
149
- exptime=ceil(expiry),
150
- noreply=False,
151
- )
152
- return value
153
- except self.dependency.NotFoundCommandError:
154
- # Incrementation failed because the key doesn't exist
155
- storage = await self.get_storage()
156
- try:
157
- await storage.add(limit_key, f"{amount}".encode(), exptime=ceil(expiry))
158
- if set_expiration_key:
159
- await storage.set(
160
- expire_key,
161
- str(expiry + time.time()).encode("utf-8"),
162
- exptime=ceil(expiry),
163
- noreply=False,
164
- )
165
- value = amount
166
- except self.dependency.NotStoredStorageCommandError:
167
- # Coult not add the key, probably because a concurrent call has added it
168
- storage = await self.get_storage()
169
- value = await storage.increment(limit_key, amount) or amount
170
- if elastic_expiry:
171
- await storage.touch(limit_key, exptime=ceil(expiry))
172
- if set_expiration_key:
173
- await storage.set(
174
- expire_key,
175
- str(expiry + time.time()).encode("utf-8"),
176
- exptime=ceil(expiry),
177
- noreply=False,
178
- )
179
- return value
180
-
181
- async def get_expiry(self, key: str) -> float:
182
- """
183
- :param key: the key to get the expiry for
184
- """
185
- storage = await self.get_storage()
186
- item = await storage.get(self._expiration_key(key).encode("utf-8"))
187
-
188
- return item and float(item.value) or time.time()
189
-
190
- def _expiration_key(self, key: str) -> str:
191
- """
192
- Return the expiration key for the given counter key.
193
-
194
- Memcached doesn't natively return the expiration time or TTL for a given key,
195
- so we implement the expiration time on a separate key.
196
- """
197
- return key + "/expires"
198
-
199
- async def check(self) -> bool:
200
- """
201
- Check if storage is healthy by calling the ``get`` command
202
- on the key ``limiter-check``
203
- """
204
- try:
205
- storage = await self.get_storage()
206
- await storage.get(b"limiter-check")
207
-
208
- return True
209
- except: # noqa
210
- return False
211
-
212
- async def reset(self) -> int | None:
213
- raise NotImplementedError
214
-
215
- async def acquire_sliding_window_entry(
216
- self,
217
- key: str,
218
- limit: int,
219
- expiry: int,
220
- amount: int = 1,
221
- ) -> bool:
222
- if amount > limit:
223
- return False
224
- now = time.time()
225
- previous_key, current_key = self.sliding_window_keys(key, expiry, now)
226
- (
227
- previous_count,
228
- previous_ttl,
229
- current_count,
230
- _,
231
- ) = await self._get_sliding_window_info(previous_key, current_key, expiry, now)
232
- t0 = time.time()
233
- weighted_count = previous_count * previous_ttl / expiry + current_count
234
- if floor(weighted_count) + amount > limit:
235
- return False
236
- else:
237
- # Hit, increase the current counter.
238
- # If the counter doesn't exist yet, set twice the theorical expiry.
239
- # We don't need the expiration key as it is estimated with the timestamps directly.
240
- current_count = await self.incr(
241
- current_key, 2 * expiry, amount=amount, set_expiration_key=False
242
- )
243
- t1 = time.time()
244
- actualised_previous_ttl = max(0, previous_ttl - (t1 - t0))
245
- weighted_count = (
246
- previous_count * actualised_previous_ttl / expiry + current_count
247
- )
248
- if floor(weighted_count) > limit:
249
- # Another hit won the race condition: revert the incrementation and refuse this hit
250
- # Limitation: during high concurrency at the end of the window,
251
- # the counter is shifted and cannot be decremented, so less requests than expected are allowed.
252
- await self.decr(current_key, amount, noreply=True)
253
- return False
254
- return True
255
-
256
- async def get_sliding_window(
257
- self, key: str, expiry: int
258
- ) -> tuple[int, float, int, float]:
259
- now = time.time()
260
- previous_key, current_key = self.sliding_window_keys(key, expiry, now)
261
- return await self._get_sliding_window_info(
262
- previous_key, current_key, expiry, now
263
- )
264
-
265
- async def _get_sliding_window_info(
266
- self, previous_key: str, current_key: str, expiry: int, now: float
267
- ) -> tuple[int, float, int, float]:
268
- result = await self.get_many([previous_key, current_key])
269
-
270
- raw_previous_count = result.get(previous_key.encode("utf-8"))
271
- raw_current_count = result.get(current_key.encode("utf-8"))
272
-
273
- current_count = raw_current_count and int(raw_current_count.value) or 0
274
- previous_count = raw_previous_count and int(raw_previous_count.value) or 0
275
- if previous_count == 0:
276
- previous_ttl = float(0)
277
- else:
278
- previous_ttl = (1 - (((now - expiry) / expiry) % 1)) * expiry
279
- current_ttl = (1 - ((now / expiry) % 1)) * expiry + expiry
280
-
281
- return previous_count, previous_ttl, current_count, current_ttl
limits/storage/etcd.py DELETED
@@ -1,139 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- import urllib.parse
5
-
6
- from deprecated.sphinx import deprecated
7
-
8
- from limits.errors import ConcurrentUpdateError
9
- from limits.storage.base import Storage
10
- from limits.typing import TYPE_CHECKING
11
-
12
- if TYPE_CHECKING:
13
- import etcd3
14
-
15
-
16
- @deprecated(version="4.4")
17
- class EtcdStorage(Storage):
18
- """
19
- Rate limit storage with etcd as backend.
20
-
21
- Depends on :pypi:`etcd3`.
22
- """
23
-
24
- STORAGE_SCHEME = ["etcd"]
25
- """The storage scheme for etcd"""
26
- DEPENDENCIES = ["etcd3"]
27
- PREFIX = "limits"
28
- MAX_RETRIES = 5
29
-
30
- def __init__(
31
- self,
32
- uri: str,
33
- max_retries: int = MAX_RETRIES,
34
- wrap_exceptions: bool = False,
35
- **options: str,
36
- ) -> None:
37
- """
38
- :param uri: etcd location of the form
39
- ``etcd://host:port``,
40
- :param max_retries: Maximum number of attempts to retry
41
- in the case of concurrent updates to a rate limit key
42
- :param wrap_exceptions: Whether to wrap storage exceptions in
43
- :exc:`limits.errors.StorageError` before raising it.
44
- :param options: all remaining keyword arguments are passed
45
- directly to the constructor of :class:`etcd3.Etcd3Client`
46
- :raise ConfigurationError: when :pypi:`etcd3` is not available
47
- """
48
- parsed = urllib.parse.urlparse(uri)
49
- self.lib = self.dependencies["etcd3"].module
50
- self.storage: etcd3.Etcd3Client = self.lib.client(
51
- parsed.hostname, parsed.port, **options
52
- )
53
- self.max_retries = max_retries
54
- super().__init__(uri, wrap_exceptions=wrap_exceptions)
55
-
56
- @property
57
- def base_exceptions(
58
- self,
59
- ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
60
- return self.lib.Etcd3Exception # type: ignore[no-any-return]
61
-
62
- def prefixed_key(self, key: str) -> bytes:
63
- return f"{self.PREFIX}/{key}".encode()
64
-
65
- def incr(
66
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
67
- ) -> int:
68
- retries = 0
69
- etcd_key = self.prefixed_key(key)
70
- while retries < self.max_retries:
71
- now = time.time()
72
- lease = self.storage.lease(expiry)
73
- window_end = now + expiry
74
- create_attempt = self.storage.transaction(
75
- compare=[self.storage.transactions.create(etcd_key) == "0"],
76
- success=[
77
- self.storage.transactions.put(
78
- etcd_key,
79
- f"{amount}:{window_end}".encode(),
80
- lease=lease.id,
81
- )
82
- ],
83
- failure=[self.storage.transactions.get(etcd_key)],
84
- )
85
- if create_attempt[0]:
86
- return amount
87
- else:
88
- cur, meta = create_attempt[1][0][0]
89
- cur_value, window_end = cur.split(b":")
90
- window_end = float(window_end)
91
- if window_end <= now:
92
- self.storage.revoke_lease(meta.lease_id)
93
- self.storage.delete(etcd_key)
94
- else:
95
- if elastic_expiry:
96
- self.storage.refresh_lease(meta.lease_id)
97
- window_end = now + expiry
98
- new = int(cur_value) + amount
99
- if self.storage.transaction(
100
- compare=[self.storage.transactions.value(etcd_key) == cur],
101
- success=[
102
- self.storage.transactions.put(
103
- etcd_key,
104
- f"{new}:{window_end}".encode(),
105
- lease=meta.lease_id,
106
- )
107
- ],
108
- failure=[],
109
- )[0]:
110
- return new
111
- retries += 1
112
- raise ConcurrentUpdateError(key, retries)
113
-
114
- def get(self, key: str) -> int:
115
- value, meta = self.storage.get(self.prefixed_key(key))
116
- if value:
117
- amount, expiry = value.split(b":")
118
- if float(expiry) > time.time():
119
- return int(amount)
120
- return 0
121
-
122
- def get_expiry(self, key: str) -> float:
123
- value, _ = self.storage.get(self.prefixed_key(key))
124
- if value:
125
- return float(value.split(b":")[1])
126
- return time.time()
127
-
128
- def check(self) -> bool:
129
- try:
130
- self.storage.status()
131
- return True
132
- except: # noqa
133
- return False
134
-
135
- def reset(self) -> int | None:
136
- return self.storage.delete_prefix(f"{self.PREFIX}/").deleted
137
-
138
- def clear(self, key: str) -> None:
139
- self.storage.delete(self.prefixed_key(key))
@@ -1,43 +0,0 @@
1
- limits/__init__.py,sha256=gPUFrt02kHF_syLjiVRSs-S4UVGpRMcM2VMFNhF6G24,748
2
- limits/_version.py,sha256=jZ64F2cwQ-s302Fiuxp7ygDtnHK_weL0UhvcdhfjUSA,497
3
- limits/errors.py,sha256=s1el9Vg0ly-z92guvnvYNgKi3_aVqpiw_sufemiLLTI,662
4
- limits/limits.py,sha256=YzzZP8_ay_zlMMnnY2xhAcFTTFvFe5HEk8NQlvUTru4,4907
5
- limits/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- limits/strategies.py,sha256=14k8CFu9mfoVokEVxD48MjF61gBl-hr895brpL4XZUc,10899
7
- limits/typing.py,sha256=7G4-1MIh0pLOjBMG-gjNhpAqmX-hgQRzhD5_8ROVkls,4459
8
- limits/util.py,sha256=nk5QYvezFuXPq1OTEj04RrZFSWIH-khT0e_Dim6zGCw,6002
9
- limits/version.py,sha256=YwkF3dtq1KGzvmL3iVGctA8NNtGlK_0arrzZkZGVjUs,47
10
- limits/aio/__init__.py,sha256=yxvWb_ZmV245Hg2LqD365WC5IDllcGDMw6udJ1jNp1g,118
11
- limits/aio/strategies.py,sha256=9jnVU_nynm0zL1EjQLE3fus1vFbLqA-19ZRQhhexAGo,10776
12
- limits/aio/storage/__init__.py,sha256=gL4DGTV-XDksZxofaP__sGvwehN8MuuJQuRZeuGwiOQ,695
13
- limits/aio/storage/base.py,sha256=376Bs7l285vRTRa3-DqcVRnqOPdf3BpoCqbJr3XA9u8,6439
14
- limits/aio/storage/etcd.py,sha256=QJY8B9jNCNZLpo_cEMUyQkYsk4UIfoMhz7lJUl-ROmk,5081
15
- limits/aio/storage/memcached.py,sha256=elK5XWQr_P1wnWLxSuWQMEBndx0y4VUW0M1kcWJvIYI,10448
16
- limits/aio/storage/memory.py,sha256=KzpDnr-o3U6Ar2GkhAUTDokQ8tstUFUIe44_rYlMD3k,9765
17
- limits/aio/storage/mongodb.py,sha256=AyF5CxpuL0-O-PLZPBWPSbyL6JCRpKAlbIfczLSzFFY,19388
18
- limits/aio/storage/redis/__init__.py,sha256=p6amEcujcImDUxcYCsfBaLBKp1qH9xDXDjv3FWWfGow,14203
19
- limits/aio/storage/redis/bridge.py,sha256=cKs77RoCxUPfYD6_o1AiHfqpkeq_DFqMtVQKMLhEWdY,3183
20
- limits/aio/storage/redis/coredis.py,sha256=YT8cBx25MeSy9ApSJBfOK8VKduABTRefsnd9GhWscsI,7494
21
- limits/aio/storage/redis/redispy.py,sha256=ZAxHOFGAjRHsPzjfLowq5nMlVkK_YhVGHOOV8K4gMmU,8547
22
- limits/aio/storage/redis/valkey.py,sha256=f_-HPZhzNspywGybMNIL0F5uDZk76v8_K9wuC5ZeKhc,248
23
- limits/resources/redis/lua_scripts/acquire_moving_window.lua,sha256=5CFJX7D6T6RG5SFr6eVZ6zepmI1EkGWmKeVEO4QNrWo,483
24
- limits/resources/redis/lua_scripts/acquire_sliding_window.lua,sha256=OhVI1MAN_gT92P6r-2CEmvy1yvQVjYCCZxWIxfXYceY,1329
25
- limits/resources/redis/lua_scripts/clear_keys.lua,sha256=zU0cVfLGmapRQF9x9u0GclapM_IB2pJLszNzVQ1QRK4,184
26
- limits/resources/redis/lua_scripts/incr_expire.lua,sha256=Uq9NcrrcDI-F87TDAJexoSJn2SDgeXIUEYozCp9S3oA,195
27
- limits/resources/redis/lua_scripts/moving_window.lua,sha256=5hUZghISDh8Cbg8HJediM_OKjjNMF-0CBywWmsc93vA,430
28
- limits/resources/redis/lua_scripts/sliding_window.lua,sha256=qG3Yg30Dq54QpRUcR9AOrKQ5bdJiaYpCacTm6Kxblvc,713
29
- limits/storage/__init__.py,sha256=DArgeRfGilHWsfKz5qT_6OimP3S5u2E9lnzVst0n8Bw,2701
30
- limits/storage/base.py,sha256=6MprvcdNaTIPyN0ei9emcJLajYq07vDGSIKrmiaU6dU,7003
31
- limits/storage/etcd.py,sha256=oaNYOUryK-YmWKfmPtodRLjJGDMV4lGEYW9PU7FI_Ko,4745
32
- limits/storage/memcached.py,sha256=NJqHpbfZjT7YpjlK0dUX1-k_nb7HxPMQvBBapgUaHhY,11217
33
- limits/storage/memory.py,sha256=xLg3NDsnVEI3ds4QGS7MpH-VjjEJqwroOnz3LA8UQQ8,9226
34
- limits/storage/mongodb.py,sha256=UOn1owOnCJ_HxYQS2A82PKqJ_2gg46yWNBV6S73uVeE,18314
35
- limits/storage/redis.py,sha256=b2m5TrPNwS7NBY5btwCN0esGyCVQTwwgCn6f-sTcgFQ,10613
36
- limits/storage/redis_cluster.py,sha256=z6aONMl4p1AY78G3J0BbtK--uztz88krwnpiOsU61BM,4447
37
- limits/storage/redis_sentinel.py,sha256=AN0WtwHN88TvXk0C2uUE8l5Jhsd1ZxU8XSqrEyQSR20,4327
38
- limits/storage/registry.py,sha256=CxSaDBGR5aBJPFAIsfX9axCnbcThN3Bu-EH4wHrXtu8,650
39
- limits-4.7.3.dist-info/licenses/LICENSE.txt,sha256=T6i7kq7F5gIPfcno9FCxU5Hcwm22Bjq0uHZV3ElcjsQ,1061
40
- limits-4.7.3.dist-info/METADATA,sha256=YPclKajNJE8T3MSA5Nk2M3Ib3HW7C5W8WbJsAFrkwic,11379
41
- limits-4.7.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
42
- limits-4.7.3.dist-info/top_level.txt,sha256=C7g5ahldPoU2s6iWTaJayUrbGmPK1d6e9t5Nn0vQ2jM,7
43
- limits-4.7.3.dist-info/RECORD,,
File without changes