limits 4.7.2__py3-none-any.whl → 5.0.0rc1__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.
@@ -12,11 +12,14 @@ local entry = redis.call('lindex', KEYS[1], limit - amount)
12
12
  if entry and tonumber(entry) >= timestamp - expiry then
13
13
  return false
14
14
  end
15
-
15
+ local entries = {}
16
16
  for i = 1, amount do
17
- redis.call('lpush', KEYS[1], timestamp)
17
+ entries[i] = timestamp
18
18
  end
19
19
 
20
+ for i=1,#entries,5000 do
21
+ redis.call('lpush', KEYS[1], unpack(entries, i, math.min(i+4999, #entries)))
22
+ end
20
23
  redis.call('ltrim', KEYS[1], 0, limit - 1)
21
24
  redis.call('expire', KEYS[1], expiry)
22
25
 
@@ -1,21 +1,30 @@
1
- local items = redis.call('lrange', KEYS[1], 0, tonumber(ARGV[2]))
1
+ local len = tonumber(ARGV[2])
2
2
  local expiry = tonumber(ARGV[1])
3
- local a = 0
4
- local oldest = nil
5
3
 
6
- for idx=1,#items do
7
- if tonumber(items[idx]) >= expiry then
8
- a = a + 1
4
+ -- Binary search to find the oldest valid entry in the window
5
+ local function oldest_entry(high, target)
6
+ local low = 0
7
+ local result = nil
9
8
 
10
- local value = tonumber(items[idx])
11
- if oldest == nil or value < oldest then
12
- oldest = value
9
+ while low <= high do
10
+ local mid = math.floor((low + high) / 2)
11
+ local val = tonumber(redis.call('lindex', KEYS[1], mid))
12
+
13
+ if val and val >= target then
14
+ result = mid
15
+ low = mid + 1
16
+ else
17
+ high = mid - 1
13
18
  end
14
- else
15
- break
16
19
  end
20
+
21
+ return result
17
22
  end
18
23
 
19
- if oldest then
20
- return {tostring(oldest), a}
21
- end
24
+ local index = oldest_entry(len - 1, expiry)
25
+
26
+ if index then
27
+ local count = index + 1
28
+ local oldest = tonumber(redis.call('lindex', KEYS[1], index))
29
+ return {tostring(oldest), count}
30
+ end
@@ -12,7 +12,6 @@ import limits # noqa
12
12
  from ..errors import ConfigurationError
13
13
  from ..typing import TypeAlias, cast
14
14
  from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
15
- from .etcd import EtcdStorage
16
15
  from .memcached import MemcachedStorage
17
16
  from .memory import MemoryStorage
18
17
  from .mongodb import MongoDBStorage, MongoDBStorageBase
@@ -67,7 +66,6 @@ def storage_from_string(
67
66
 
68
67
 
69
68
  __all__ = [
70
- "EtcdStorage",
71
69
  "MemcachedStorage",
72
70
  "MemoryStorage",
73
71
  "MongoDBStorage",
limits/storage/base.py CHANGED
@@ -71,16 +71,12 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
71
71
  raise NotImplementedError
72
72
 
73
73
  @abstractmethod
74
- def incr(
75
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
76
- ) -> int:
74
+ def incr(self, key: str, expiry: int, amount: int = 1) -> int:
77
75
  """
78
76
  increments the counter for a given rate limit key
79
77
 
80
78
  :param key: the key to increment
81
79
  :param expiry: amount in seconds for the key to expire in
82
- :param elastic_expiry: whether to keep extending the rate limit
83
- window every hit.
84
80
  :param amount: the number to increment by
85
81
  """
86
82
  raise NotImplementedError
@@ -166,7 +166,6 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
166
166
  self,
167
167
  key: str,
168
168
  expiry: float,
169
- elastic_expiry: bool = False,
170
169
  amount: int = 1,
171
170
  set_expiration_key: bool = True,
172
171
  ) -> int:
@@ -175,43 +174,21 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
175
174
 
176
175
  :param key: the key to increment
177
176
  :param expiry: amount in seconds for the key to expire in
178
- :param elastic_expiry: whether to keep extending the rate limit
179
177
  window every hit.
180
178
  :param amount: the number to increment by
181
179
  :param set_expiration_key: set the expiration key with the expiration time if needed. If set to False, the key will still expire, but memcached cannot provide the expiration time.
182
180
  """
183
- value = self.call_memcached_func(self.storage.incr, key, amount, noreply=False)
184
- if value is not None:
185
- if elastic_expiry:
186
- self.call_memcached_func(self.storage.touch, key, ceil(expiry))
187
- if set_expiration_key:
188
- self.call_memcached_func(
189
- self.storage.set,
190
- self._expiration_key(key),
191
- expiry + time.time(),
192
- expire=ceil(expiry),
193
- noreply=False,
194
- )
195
-
181
+ if (
182
+ value := self.call_memcached_func(
183
+ self.storage.incr, key, amount, noreply=False
184
+ )
185
+ ) is not None:
196
186
  return value
197
187
  else:
198
188
  if not self.call_memcached_func(
199
189
  self.storage.add, key, amount, ceil(expiry), noreply=False
200
190
  ):
201
- value = self.storage.incr(key, amount) or amount
202
-
203
- if elastic_expiry:
204
- self.call_memcached_func(self.storage.touch, key, ceil(expiry))
205
- if set_expiration_key:
206
- self.call_memcached_func(
207
- self.storage.set,
208
- self._expiration_key(key),
209
- expiry + time.time(),
210
- expire=ceil(expiry),
211
- noreply=False,
212
- )
213
-
214
- return value
191
+ return self.storage.incr(key, amount) or amount
215
192
  else:
216
193
  if set_expiration_key:
217
194
  self.call_memcached_func(
limits/storage/memory.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import bisect
3
4
  import threading
4
5
  import time
5
6
  from collections import Counter, defaultdict
@@ -25,7 +26,7 @@ class MemoryStorage(
25
26
  ):
26
27
  """
27
28
  rate limit storage using :class:`collections.Counter`
28
- as an in memory storage for fixed and elastic window strategies,
29
+ as an in memory storage for fixed and sliding window strategies,
29
30
  and a simple list to implement moving window strategy.
30
31
 
31
32
  """
@@ -56,9 +57,11 @@ class MemoryStorage(
56
57
  def __expire_events(self) -> None:
57
58
  for key in list(self.events.keys()):
58
59
  with self.locks[key]:
59
- for event in list(self.events[key]):
60
- if event.expiry <= time.time() and event in self.events[key]:
61
- self.events[key].remove(event)
60
+ 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]
62
65
  if not self.events.get(key, None):
63
66
  self.locks.pop(key, None)
64
67
  for key in list(self.expirations.keys()):
@@ -78,26 +81,20 @@ class MemoryStorage(
78
81
  ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
79
82
  return ValueError
80
83
 
81
- def incr(
82
- self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
83
- ) -> int:
84
+ def incr(self, key: str, expiry: float, amount: int = 1) -> int:
84
85
  """
85
86
  increments the counter for a given rate limit key
86
87
 
87
88
  :param key: the key to increment
88
89
  :param expiry: amount in seconds for the key to expire in
89
- :param elastic_expiry: whether to keep extending the rate limit
90
- window every hit.
91
90
  :param amount: the number to increment by
92
91
  """
93
92
  self.get(key)
94
93
  self.__schedule_expiry()
95
94
  with self.locks[key]:
96
95
  self.storage[key] += amount
97
-
98
- if elastic_expiry or self.storage[key] == amount:
96
+ if self.storage[key] == amount:
99
97
  self.expirations[key] = time.time() + expiry
100
-
101
98
  return self.storage.get(key, 0)
102
99
 
103
100
  def decr(self, key: str, amount: int = 1) -> int:
@@ -157,7 +154,7 @@ class MemoryStorage(
157
154
  if entry and entry.atime >= timestamp - expiry:
158
155
  return False
159
156
  else:
160
- self.events[key][:0] = [Entry(expiry) for _ in range(amount)]
157
+ self.events[key][:0] = [Entry(expiry)] * amount
161
158
  return True
162
159
 
163
160
  def get_expiry(self, key: str) -> float:
@@ -167,21 +164,6 @@ class MemoryStorage(
167
164
 
168
165
  return self.expirations.get(key, time.time())
169
166
 
170
- def get_num_acquired(self, key: str, expiry: int) -> int:
171
- """
172
- returns the number of entries already acquired
173
-
174
- :param key: rate limit key to acquire an entry in
175
- :param expiry: expiry of the entry
176
- """
177
- timestamp = time.time()
178
-
179
- return (
180
- len([k for k in self.events.get(key, []) if k.atime >= timestamp - expiry])
181
- if self.events.get(key)
182
- else 0
183
- )
184
-
185
167
  def get_moving_window(self, key: str, limit: int, expiry: int) -> tuple[float, int]:
186
168
  """
187
169
  returns the starting point and the number of entries in the moving
@@ -192,13 +174,12 @@ class MemoryStorage(
192
174
  :return: (start of window, number of acquired entries)
193
175
  """
194
176
  timestamp = time.time()
195
- acquired = self.get_num_acquired(key, expiry)
196
-
197
- for item in self.events.get(key, [])[::-1]:
198
- if item.atime >= timestamp - expiry:
199
- return item.atime, acquired
200
-
201
- return timestamp, acquired
177
+ if events := self.events.get(key, []):
178
+ oldest = bisect.bisect_left(
179
+ events, -(timestamp - expiry), key=lambda entry: -entry.atime
180
+ )
181
+ return events[oldest - 1].atime, oldest
182
+ return timestamp, 0
202
183
 
203
184
  def acquire_sliding_window_entry(
204
185
  self,
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,6 @@ __all__ = [
155
107
  "Callable",
156
108
  "ClassVar",
157
109
  "Counter",
158
- "EmcacheClientP",
159
- "ItemP",
160
110
  "Literal",
161
111
  "MemcachedClientP",
162
112
  "MongoClient",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: limits
3
- Version: 4.7.2
3
+ Version: 5.0.0rc1
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