limits 4.7.3__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/storage/mongodb.py CHANGED
@@ -144,9 +144,7 @@ class MongoDBStorageBase(
144
144
 
145
145
  return counter and counter["count"] or 0
146
146
 
147
- def incr(
148
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
149
- ) -> int:
147
+ def incr(self, key: str, expiry: int, amount: int = 1) -> int:
150
148
  """
151
149
  increments the counter for a given rate limit key
152
150
 
@@ -175,9 +173,7 @@ class MongoDBStorageBase(
175
173
  "$cond": {
176
174
  "if": {"$lt": ["$expireAt", "$$NOW"]},
177
175
  "then": expiration,
178
- "else": (
179
- expiration if elastic_expiry else "$expireAt"
180
- ),
176
+ "else": "$expireAt",
181
177
  }
182
178
  },
183
179
  }
@@ -210,13 +206,13 @@ class MongoDBStorageBase(
210
206
  :return: (start of window, number of acquired entries)
211
207
  """
212
208
  timestamp = time.time()
213
- result = list(
209
+ if result := list(
214
210
  self.windows.aggregate(
215
211
  [
216
212
  {"$match": {"_id": key}},
217
213
  {
218
214
  "$project": {
219
- "entries": {
215
+ "filteredEntries": {
220
216
  "$filter": {
221
217
  "input": "$entries",
222
218
  "as": "entry",
@@ -225,21 +221,16 @@ class MongoDBStorageBase(
225
221
  }
226
222
  }
227
223
  },
228
- {"$unwind": "$entries"},
229
224
  {
230
- "$group": {
231
- "_id": "$_id",
232
- "min": {"$min": "$entries"},
233
- "count": {"$sum": 1},
225
+ "$project": {
226
+ "min": {"$min": "$filteredEntries"},
227
+ "count": {"$size": "$filteredEntries"},
234
228
  }
235
229
  },
236
230
  ]
237
231
  )
238
- )
239
-
240
- if result:
232
+ ):
241
233
  return result[0]["min"], result[0]["count"]
242
-
243
234
  return timestamp, 0
244
235
 
245
236
  def acquire_entry(self, key: str, limit: int, expiry: int, amount: int = 1) -> bool:
@@ -299,7 +290,7 @@ class MongoDBStorageBase(
299
290
  "$cond": {
300
291
  "if": {
301
292
  "$lte": [
302
- {"$subtract": ["$expiresAt", "$$NOW"]},
293
+ {"$subtract": ["$expireAt", "$$NOW"]},
303
294
  expiry_ms,
304
295
  ]
305
296
  },
@@ -311,7 +302,7 @@ class MongoDBStorageBase(
311
302
  "$cond": {
312
303
  "if": {
313
304
  "$lte": [
314
- {"$subtract": ["$expiresAt", "$$NOW"]},
305
+ {"$subtract": ["$expireAt", "$$NOW"]},
315
306
  expiry_ms,
316
307
  ]
317
308
  },
@@ -319,29 +310,29 @@ class MongoDBStorageBase(
319
310
  "else": {"$ifNull": ["$currentCount", 0]},
320
311
  }
321
312
  },
322
- "expiresAt": {
313
+ "expireAt": {
323
314
  "$cond": {
324
315
  "if": {
325
316
  "$lte": [
326
- {"$subtract": ["$expiresAt", "$$NOW"]},
317
+ {"$subtract": ["$expireAt", "$$NOW"]},
327
318
  expiry_ms,
328
319
  ]
329
320
  },
330
321
  "then": {
331
- "$add": ["$expiresAt", expiry_ms],
322
+ "$add": ["$expireAt", expiry_ms],
332
323
  },
333
- "else": "$expiresAt",
324
+ "else": "$expireAt",
334
325
  }
335
326
  },
336
327
  }
337
328
  }
338
329
  ],
339
330
  return_document=self.lib.ReturnDocument.AFTER,
340
- projection=["currentCount", "previousCount", "expiresAt"],
331
+ projection=["currentCount", "previousCount", "expireAt"],
341
332
  ):
342
333
  expires_at = (
343
- (result["expiresAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
344
- if result.get("expiresAt")
334
+ (result["expireAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
335
+ if result.get("expireAt")
345
336
  else time.time()
346
337
  )
347
338
  current_ttl = max(0, expires_at - time.time())
@@ -368,7 +359,7 @@ class MongoDBStorageBase(
368
359
  "$cond": {
369
360
  "if": {
370
361
  "$lte": [
371
- {"$subtract": ["$expiresAt", "$$NOW"]},
362
+ {"$subtract": ["$expireAt", "$$NOW"]},
372
363
  expiry_ms,
373
364
  ]
374
365
  },
@@ -384,7 +375,7 @@ class MongoDBStorageBase(
384
375
  "$cond": {
385
376
  "if": {
386
377
  "$lte": [
387
- {"$subtract": ["$expiresAt", "$$NOW"]},
378
+ {"$subtract": ["$expireAt", "$$NOW"]},
388
379
  expiry_ms,
389
380
  ]
390
381
  },
@@ -392,22 +383,22 @@ class MongoDBStorageBase(
392
383
  "else": {"$ifNull": ["$currentCount", 0]},
393
384
  }
394
385
  },
395
- "expiresAt": {
386
+ "expireAt": {
396
387
  "$cond": {
397
388
  "if": {
398
389
  "$lte": [
399
- {"$subtract": ["$expiresAt", "$$NOW"]},
390
+ {"$subtract": ["$expireAt", "$$NOW"]},
400
391
  expiry_ms,
401
392
  ]
402
393
  },
403
394
  "then": {
404
395
  "$cond": {
405
- "if": {"$gt": ["$expiresAt", 0]},
406
- "then": {"$add": ["$expiresAt", expiry_ms]},
396
+ "if": {"$gt": ["$expireAt", 0]},
397
+ "then": {"$add": ["$expireAt", expiry_ms]},
407
398
  "else": {"$add": ["$$NOW", 2 * expiry_ms]},
408
399
  }
409
400
  },
410
- "else": "$expiresAt",
401
+ "else": "$expireAt",
411
402
  }
412
403
  },
413
404
  }
@@ -427,7 +418,7 @@ class MongoDBStorageBase(
427
418
  0,
428
419
  {
429
420
  "$subtract": [
430
- "$expiresAt",
421
+ "$expireAt",
431
422
  {
432
423
  "$add": [
433
424
  "$$NOW",
limits/storage/redis.py CHANGED
@@ -201,7 +201,6 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
201
201
  self,
202
202
  key: str,
203
203
  expiry: int,
204
- elastic_expiry: bool = False,
205
204
  amount: int = 1,
206
205
  ) -> int:
207
206
  """
@@ -213,12 +212,7 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
213
212
  :param amount: the number to increment by
214
213
  """
215
214
  key = self.prefixed_key(key)
216
- if elastic_expiry:
217
- value = self.get_connection().incrby(key, amount)
218
- self.get_connection().expire(key, expiry)
219
- return value
220
- else:
221
- return int(self.lua_incr_expire([key], [expiry, amount]))
215
+ return int(self.lua_incr_expire([key], [expiry, amount]))
222
216
 
223
217
  def get(self, key: str) -> int:
224
218
  """
limits/strategies.py CHANGED
@@ -8,7 +8,7 @@ import time
8
8
  from abc import ABCMeta, abstractmethod
9
9
  from math import floor, inf
10
10
 
11
- from deprecated.sphinx import deprecated, versionadded
11
+ from deprecated.sphinx import versionadded
12
12
 
13
13
  from limits.storage.base import SlidingWindowCounterSupport
14
14
 
@@ -148,7 +148,6 @@ class FixedWindowRateLimiter(RateLimiter):
148
148
  self.storage.incr(
149
149
  item.key_for(*identifiers),
150
150
  item.get_expiry(),
151
- elastic_expiry=False,
152
151
  amount=cost,
153
152
  )
154
153
  <= item.amount
@@ -286,43 +285,14 @@ class SlidingWindowCounterRateLimiter(RateLimiter):
286
285
  return WindowStats(now + min(previous_reset_in, current_reset_in), remaining)
287
286
 
288
287
 
289
- @deprecated(version="4.1", action="always")
290
- class FixedWindowElasticExpiryRateLimiter(FixedWindowRateLimiter):
291
- """
292
- Reference: :ref:`strategies:fixed window with elastic expiry`
293
- """
294
-
295
- def hit(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
296
- """
297
- Consume the rate limit
298
-
299
- :param item: The rate limit item
300
- :param identifiers: variable list of strings to uniquely identify this
301
- instance of the limit
302
- :param cost: The cost of this hit, default 1
303
- """
304
-
305
- return (
306
- self.storage.incr(
307
- item.key_for(*identifiers),
308
- item.get_expiry(),
309
- elastic_expiry=True,
310
- amount=cost,
311
- )
312
- <= item.amount
313
- )
314
-
315
-
316
288
  KnownStrategy = (
317
289
  type[SlidingWindowCounterRateLimiter]
318
290
  | type[FixedWindowRateLimiter]
319
- | type[FixedWindowElasticExpiryRateLimiter]
320
291
  | type[MovingWindowRateLimiter]
321
292
  )
322
293
 
323
294
  STRATEGIES: dict[str, KnownStrategy] = {
324
295
  "sliding-window-counter": SlidingWindowCounterRateLimiter,
325
296
  "fixed-window": FixedWindowRateLimiter,
326
- "fixed-window-elastic-expiry": FixedWindowElasticExpiryRateLimiter,
327
297
  "moving-window": MovingWindowRateLimiter,
328
298
  }
limits/typing.py CHANGED
@@ -30,54 +30,6 @@ if TYPE_CHECKING:
30
30
  import redis
31
31
 
32
32
 
33
- class ItemP(Protocol):
34
- value: bytes
35
- flags: int | None
36
- cas: int | None
37
-
38
-
39
- class EmcacheClientP(Protocol):
40
- async def add(
41
- self,
42
- key: bytes,
43
- value: bytes,
44
- *,
45
- flags: int = 0,
46
- exptime: int = 0,
47
- noreply: bool = False,
48
- ) -> None: ...
49
-
50
- async def get(self, key: bytes, return_flags: bool = False) -> ItemP | None: ...
51
-
52
- async def get_many(self, keys: Iterable[bytes]) -> dict[bytes, ItemP]: ...
53
-
54
- async def gets(self, key: bytes, return_flags: bool = False) -> ItemP | None: ...
55
-
56
- async def increment(
57
- self, key: bytes, value: int, *, noreply: bool = False
58
- ) -> int | None: ...
59
-
60
- async def decrement(
61
- self, key: bytes, value: int, *, noreply: bool = False
62
- ) -> int | None: ...
63
-
64
- async def delete(self, key: bytes, *, noreply: bool = False) -> None: ...
65
-
66
- async def set(
67
- self,
68
- key: bytes,
69
- value: bytes,
70
- *,
71
- flags: int = 0,
72
- exptime: int = 0,
73
- noreply: bool = False,
74
- ) -> None: ...
75
-
76
- async def touch(
77
- self, key: bytes, exptime: int, *, noreply: bool = False
78
- ) -> None: ...
79
-
80
-
81
33
  class MemcachedClientP(Protocol):
82
34
  def add(
83
35
  self,
@@ -155,8 +107,7 @@ __all__ = [
155
107
  "Callable",
156
108
  "ClassVar",
157
109
  "Counter",
158
- "EmcacheClientP",
159
- "ItemP",
110
+ "Iterable",
160
111
  "Literal",
161
112
  "MemcachedClientP",
162
113
  "MongoClient",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: limits
3
- Version: 4.7.3
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
@@ -32,19 +32,14 @@ Provides-Extra: memcached
32
32
  Requires-Dist: pymemcache<5.0.0,>3; extra == "memcached"
33
33
  Provides-Extra: mongodb
34
34
  Requires-Dist: pymongo<5,>4.1; extra == "mongodb"
35
- Provides-Extra: etcd
36
- Requires-Dist: etcd3; extra == "etcd"
37
35
  Provides-Extra: valkey
38
36
  Requires-Dist: valkey>=6; extra == "valkey"
39
37
  Provides-Extra: async-redis
40
38
  Requires-Dist: coredis<5,>=3.4.0; extra == "async-redis"
41
39
  Provides-Extra: async-memcached
42
- Requires-Dist: emcache>=0.6.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"
40
+ Requires-Dist: memcachio>=0.3; extra == "async-memcached"
44
41
  Provides-Extra: async-mongodb
45
42
  Requires-Dist: motor<4,>=3; extra == "async-mongodb"
46
- Provides-Extra: async-etcd
47
- Requires-Dist: aetcd; extra == "async-etcd"
48
43
  Provides-Extra: async-valkey
49
44
  Requires-Dist: valkey>=6; extra == "async-valkey"
50
45
  Provides-Extra: all
@@ -52,13 +47,10 @@ Requires-Dist: redis!=4.5.2,!=4.5.3,<6.0.0,>3; extra == "all"
52
47
  Requires-Dist: redis!=4.5.2,!=4.5.3,>=4.2.0; extra == "all"
53
48
  Requires-Dist: pymemcache<5.0.0,>3; extra == "all"
54
49
  Requires-Dist: pymongo<5,>4.1; extra == "all"
55
- Requires-Dist: etcd3; extra == "all"
56
50
  Requires-Dist: valkey>=6; extra == "all"
57
51
  Requires-Dist: coredis<5,>=3.4.0; extra == "all"
58
- Requires-Dist: emcache>=0.6.1; python_version < "3.11" and extra == "all"
59
- Requires-Dist: emcache>=1; (python_version >= "3.11" and python_version < "3.13.0") and extra == "all"
52
+ Requires-Dist: memcachio>=0.3; extra == "all"
60
53
  Requires-Dist: motor<4,>=3; extra == "all"
61
- Requires-Dist: aetcd; extra == "all"
62
54
  Requires-Dist: valkey>=6; extra == "all"
63
55
  Dynamic: author
64
56
  Dynamic: author-email
@@ -86,13 +78,14 @@ Dynamic: summary
86
78
  .. |docs| image:: https://readthedocs.org/projects/limits/badge/?version=latest
87
79
  :target: https://limits.readthedocs.org
88
80
 
81
+ ######
89
82
  limits
90
- ------
83
+ ######
91
84
  |docs| |ci| |codecov| |pypi| |pypi-versions| |license|
92
85
 
93
86
 
94
87
  **limits** is a python library for rate limiting via multiple strategies
95
- with commonly used storage backends (Redis, Memcached, MongoDB & Etcd).
88
+ with commonly used storage backends (Redis, Memcached & MongoDB).
96
89
 
97
90
  The library provides identical APIs for use in sync and
98
91
  `async <https://limits.readthedocs.io/en/stable/async.html>`_ codebases.
@@ -188,13 +181,13 @@ Scenario 2:
188
181
  - ``weighted_count = floor(8 + (4 * 0.33)) = floor(8 + 1.32) = 9``.
189
182
  - Since the weighted count is below the limit, the request is allowed.
190
183
 
184
+
191
185
  Storage backends
192
186
  ================
193
187
 
194
188
  - `Redis <https://limits.readthedocs.io/en/latest/storage.html#redis-storage>`_
195
189
  - `Memcached <https://limits.readthedocs.io/en/latest/storage.html#memcached-storage>`_
196
190
  - `MongoDB <https://limits.readthedocs.io/en/latest/storage.html#mongodb-storage>`_
197
- - `Etcd <https://limits.readthedocs.io/en/latest/storage.html#etcd-storage>`_
198
191
  - `In-Memory <https://limits.readthedocs.io/en/latest/storage.html#in-memory-storage>`_
199
192
 
200
193
  Dive right in
@@ -281,5 +274,6 @@ Links
281
274
  =====
282
275
 
283
276
  * `Documentation <http://limits.readthedocs.org/en/latest>`_
277
+ * `Benchmarks <http://limits.readthedocs.org/en/latest/performance.html>`_
284
278
  * `Changelog <http://limits.readthedocs.org/en/stable/changelog.html>`_
285
279
 
@@ -0,0 +1,44 @@
1
+ limits/__init__.py,sha256=gPUFrt02kHF_syLjiVRSs-S4UVGpRMcM2VMFNhF6G24,748
2
+ limits/_version.py,sha256=NfHJ8jCFa69gQxFo8GoNxy5o_PAmF3e9sDd1Q4gjLTU,500
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=LeZ6lnE73EIQqQ8TfKaTzlxNvBMrZOOSXFB0l8D17fI,9946
7
+ limits/typing.py,sha256=pVt5D23MhQSUGqi0MBG5FCSqDwta2ygu18BpKvJFxow,3283
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=RzZExH2r6jnHra4SpDHqtZCC0Bo3085zUJYo2boAj6Y,9897
12
+ limits/aio/storage/__init__.py,sha256=vKeArUnN1ld_0mQOBBZPCjaQgM5xI1GBPM7_F2Ydz5c,646
13
+ limits/aio/storage/base.py,sha256=VfHpL9Z3RL76eKhoaSQKLKQsqcF5B2bnF6gfa-8ltWA,6296
14
+ limits/aio/storage/memory.py,sha256=sWrDzOe-6Opy9uFmfP1S38IbN2_wNCBaIHTS4UTRy6g,9562
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
20
+ limits/aio/storage/redis/__init__.py,sha256=lwoKk91YLEBlZ3W6hCnQ1e7Gc6LxpvSzZZW16saCyR4,14143
21
+ limits/aio/storage/redis/bridge.py,sha256=eoRi9h2bSy194cVwoKgRYQV1HQ7SvwarL-4LeazrxeA,3145
22
+ limits/aio/storage/redis/coredis.py,sha256=IzfEyXBvQbr4QUWML9xAd87a2aHCvglOBEjAg-Vq4z0,7420
23
+ limits/aio/storage/redis/redispy.py,sha256=HS1H6E9g0dP3G-8tSUILIFoc8JWpeRQOiBxcpL3I0gM,8310
24
+ limits/aio/storage/redis/valkey.py,sha256=f_-HPZhzNspywGybMNIL0F5uDZk76v8_K9wuC5ZeKhc,248
25
+ limits/resources/redis/lua_scripts/acquire_moving_window.lua,sha256=Vz0HkI_bSFLW668lEVw8paKlTLEuU4jZk1fpdSuz3zg,594
26
+ limits/resources/redis/lua_scripts/acquire_sliding_window.lua,sha256=OhVI1MAN_gT92P6r-2CEmvy1yvQVjYCCZxWIxfXYceY,1329
27
+ limits/resources/redis/lua_scripts/clear_keys.lua,sha256=zU0cVfLGmapRQF9x9u0GclapM_IB2pJLszNzVQ1QRK4,184
28
+ limits/resources/redis/lua_scripts/incr_expire.lua,sha256=Uq9NcrrcDI-F87TDAJexoSJn2SDgeXIUEYozCp9S3oA,195
29
+ limits/resources/redis/lua_scripts/moving_window.lua,sha256=zlieQwfET0BC7sxpfiOuzPa1wwmrwWLy7IF8LxNa_Lw,717
30
+ limits/resources/redis/lua_scripts/sliding_window.lua,sha256=qG3Yg30Dq54QpRUcR9AOrKQ5bdJiaYpCacTm6Kxblvc,713
31
+ limits/storage/__init__.py,sha256=9iNxIlwzLQw2d54EcMa2LBJ47wiWCPOnHgn6ddqKkDI,2652
32
+ limits/storage/base.py,sha256=IdOL_iqR9KhaJO73M_h9c6OYe8Ox632pxx5uXaL9Dbo,6860
33
+ limits/storage/memcached.py,sha256=5GUKGWS_BYTwUss2WmOlCwBtOieGT7AFUcpX65WYXdQ,10217
34
+ limits/storage/memory.py,sha256=rVlsirSp9LDhuqNFp6KMLR85fJc9xwrU58IHIVz6eq4,8719
35
+ limits/storage/mongodb.py,sha256=V4Ib_AwPFX6JpNI7oUUGJx_3MxD8EmYAi4Q6QcWnQ5U,18071
36
+ limits/storage/redis.py,sha256=i_6qh4S6JQd-lG6eRJdTPxNnZIAkm4G0cA0mfow9OOk,10389
37
+ limits/storage/redis_cluster.py,sha256=z6aONMl4p1AY78G3J0BbtK--uztz88krwnpiOsU61BM,4447
38
+ limits/storage/redis_sentinel.py,sha256=AN0WtwHN88TvXk0C2uUE8l5Jhsd1ZxU8XSqrEyQSR20,4327
39
+ limits/storage/registry.py,sha256=CxSaDBGR5aBJPFAIsfX9axCnbcThN3Bu-EH4wHrXtu8,650
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,,
@@ -1,146 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import time
5
- import urllib.parse
6
- from typing import TYPE_CHECKING
7
-
8
- from deprecated.sphinx import deprecated
9
-
10
- from limits.aio.storage.base import Storage
11
- from limits.errors import ConcurrentUpdateError
12
-
13
- if TYPE_CHECKING:
14
- import aetcd
15
-
16
-
17
- @deprecated(version="4.4")
18
- class EtcdStorage(Storage):
19
- """
20
- Rate limit storage with etcd as backend.
21
-
22
- Depends on :pypi:`aetcd`.
23
- """
24
-
25
- STORAGE_SCHEME = ["async+etcd"]
26
- """The async storage scheme for etcd"""
27
- DEPENDENCIES = ["aetcd"]
28
-
29
- PREFIX = "limits"
30
- MAX_RETRIES = 5
31
-
32
- def __init__(
33
- self,
34
- uri: str,
35
- max_retries: int = MAX_RETRIES,
36
- wrap_exceptions: bool = False,
37
- **options: str,
38
- ) -> None:
39
- """
40
- :param uri: etcd location of the form
41
- ``async+etcd://host:port``,
42
- :param max_retries: Maximum number of attempts to retry
43
- in the case of concurrent updates to a rate limit key
44
- :param wrap_exceptions: Whether to wrap storage exceptions in
45
- :exc:`limits.errors.StorageError` before raising it.
46
- :param options: all remaining keyword arguments are passed
47
- directly to the constructor of :class:`aetcd.client.Client`
48
- :raise ConfigurationError: when :pypi:`aetcd` is not available
49
- """
50
- parsed = urllib.parse.urlparse(uri)
51
- self.lib = self.dependencies["aetcd"].module
52
- self.storage: aetcd.Client = self.lib.Client(
53
- host=parsed.hostname, port=parsed.port, **options
54
- )
55
- self.max_retries = max_retries
56
- super().__init__(uri, wrap_exceptions=wrap_exceptions)
57
-
58
- @property
59
- def base_exceptions(
60
- self,
61
- ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
62
- return self.lib.ClientError # type: ignore[no-any-return]
63
-
64
- def prefixed_key(self, key: str) -> bytes:
65
- return f"{self.PREFIX}/{key}".encode()
66
-
67
- async def incr(
68
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
69
- ) -> int:
70
- retries = 0
71
- etcd_key = self.prefixed_key(key)
72
- while retries < self.max_retries:
73
- now = time.time()
74
- lease = await self.storage.lease(expiry)
75
- window_end = now + expiry
76
- create_attempt = await self.storage.transaction(
77
- compare=[self.storage.transactions.create(etcd_key) == b"0"],
78
- success=[
79
- self.storage.transactions.put(
80
- etcd_key, f"{amount}:{window_end}".encode(), 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 = create_attempt[1][0][0][1]
89
- cur_value, window_end = cur.value.split(b":")
90
- window_end = float(window_end)
91
- if window_end <= now:
92
- await asyncio.gather(
93
- self.storage.revoke_lease(cur.lease),
94
- self.storage.delete(etcd_key),
95
- )
96
- else:
97
- if elastic_expiry:
98
- await self.storage.refresh_lease(cur.lease)
99
- window_end = now + expiry
100
- new = int(cur_value) + amount
101
- if (
102
- await self.storage.transaction(
103
- compare=[
104
- self.storage.transactions.value(etcd_key) == cur.value
105
- ],
106
- success=[
107
- self.storage.transactions.put(
108
- etcd_key,
109
- f"{new}:{window_end}".encode(),
110
- lease=cur.lease,
111
- )
112
- ],
113
- failure=[],
114
- )
115
- )[0]:
116
- return new
117
- retries += 1
118
- raise ConcurrentUpdateError(key, retries)
119
-
120
- async def get(self, key: str) -> int:
121
- cur = await self.storage.get(self.prefixed_key(key))
122
- if cur:
123
- amount, expiry = cur.value.split(b":")
124
- if float(expiry) > time.time():
125
- return int(amount)
126
- return 0
127
-
128
- async def get_expiry(self, key: str) -> float:
129
- cur = await self.storage.get(self.prefixed_key(key))
130
- if cur:
131
- window_end = float(cur.value.split(b":")[1])
132
- return window_end
133
- return time.time()
134
-
135
- async def check(self) -> bool:
136
- try:
137
- await self.storage.status()
138
- return True
139
- except: # noqa
140
- return False
141
-
142
- async def reset(self) -> int | None:
143
- return (await self.storage.delete_prefix(f"{self.PREFIX}/".encode())).deleted
144
-
145
- async def clear(self, key: str) -> None:
146
- await self.storage.delete(self.prefixed_key(key))