limits 4.7.2__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.
@@ -169,16 +169,12 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
169
169
 
170
170
  return counter and counter["count"] or 0
171
171
 
172
- async def incr(
173
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
174
- ) -> int:
172
+ async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
175
173
  """
176
174
  increments the counter for a given rate limit key
177
175
 
178
176
  :param key: the key to increment
179
177
  :param expiry: amount in seconds for the key to expire in
180
- :param elastic_expiry: whether to keep extending the rate limit
181
- window every hit.
182
178
  :param amount: the number to increment by
183
179
  """
184
180
  await self.create_indices()
@@ -205,7 +201,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
205
201
  "$cond": {
206
202
  "if": {"$lt": ["$expireAt", "$$NOW"]},
207
203
  "then": expiration,
208
- "else": (expiration if elastic_expiry else "$expireAt"),
204
+ "else": "$expireAt",
209
205
  }
210
206
  },
211
207
  }
@@ -241,15 +237,16 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
241
237
  :param int expiry: expiry of entry
242
238
  :return: (start of window, number of acquired entries)
243
239
  """
240
+
244
241
  timestamp = time.time()
245
- if result := (
246
- await self.database[self.__collection_mapping["windows"]]
242
+ if (
243
+ result := await self.database[self.__collection_mapping["windows"]]
247
244
  .aggregate(
248
245
  [
249
246
  {"$match": {"_id": key}},
250
247
  {
251
248
  "$project": {
252
- "entries": {
249
+ "filteredEntries": {
253
250
  "$filter": {
254
251
  "input": "$entries",
255
252
  "as": "entry",
@@ -258,12 +255,10 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
258
255
  }
259
256
  }
260
257
  },
261
- {"$unwind": "$entries"},
262
258
  {
263
- "$group": {
264
- "_id": "$_id",
265
- "min": {"$min": "$entries"},
266
- "count": {"$sum": 1},
259
+ "$project": {
260
+ "min": {"$min": "$filteredEntries"},
261
+ "count": {"$size": "$filteredEntries"},
267
262
  }
268
263
  },
269
264
  ]
@@ -337,7 +332,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
337
332
  "$cond": {
338
333
  "if": {
339
334
  "$lte": [
340
- {"$subtract": ["$expiresAt", "$$NOW"]},
335
+ {"$subtract": ["$expireAt", "$$NOW"]},
341
336
  expiry_ms,
342
337
  ]
343
338
  },
@@ -353,7 +348,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
353
348
  "$cond": {
354
349
  "if": {
355
350
  "$lte": [
356
- {"$subtract": ["$expiresAt", "$$NOW"]},
351
+ {"$subtract": ["$expireAt", "$$NOW"]},
357
352
  expiry_ms,
358
353
  ]
359
354
  },
@@ -361,22 +356,22 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
361
356
  "else": {"$ifNull": ["$currentCount", 0]},
362
357
  }
363
358
  },
364
- "expiresAt": {
359
+ "expireAt": {
365
360
  "$cond": {
366
361
  "if": {
367
362
  "$lte": [
368
- {"$subtract": ["$expiresAt", "$$NOW"]},
363
+ {"$subtract": ["$expireAt", "$$NOW"]},
369
364
  expiry_ms,
370
365
  ]
371
366
  },
372
367
  "then": {
373
368
  "$cond": {
374
- "if": {"$gt": ["$expiresAt", 0]},
375
- "then": {"$add": ["$expiresAt", expiry_ms]},
369
+ "if": {"$gt": ["$expireAt", 0]},
370
+ "then": {"$add": ["$expireAt", expiry_ms]},
376
371
  "else": {"$add": ["$$NOW", 2 * expiry_ms]},
377
372
  }
378
373
  },
379
- "else": "$expiresAt",
374
+ "else": "$expireAt",
380
375
  }
381
376
  },
382
377
  }
@@ -396,7 +391,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
396
391
  0,
397
392
  {
398
393
  "$subtract": [
399
- "$expiresAt",
394
+ "$expireAt",
400
395
  {
401
396
  "$add": [
402
397
  "$$NOW",
@@ -464,7 +459,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
464
459
  "$cond": {
465
460
  "if": {
466
461
  "$lte": [
467
- {"$subtract": ["$expiresAt", "$$NOW"]},
462
+ {"$subtract": ["$expireAt", "$$NOW"]},
468
463
  expiry_ms,
469
464
  ]
470
465
  },
@@ -476,7 +471,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
476
471
  "$cond": {
477
472
  "if": {
478
473
  "$lte": [
479
- {"$subtract": ["$expiresAt", "$$NOW"]},
474
+ {"$subtract": ["$expireAt", "$$NOW"]},
480
475
  expiry_ms,
481
476
  ]
482
477
  },
@@ -484,27 +479,27 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
484
479
  "else": {"$ifNull": ["$currentCount", 0]},
485
480
  }
486
481
  },
487
- "expiresAt": {
482
+ "expireAt": {
488
483
  "$cond": {
489
484
  "if": {
490
485
  "$lte": [
491
- {"$subtract": ["$expiresAt", "$$NOW"]},
486
+ {"$subtract": ["$expireAt", "$$NOW"]},
492
487
  expiry_ms,
493
488
  ]
494
489
  },
495
- "then": {"$add": ["$expiresAt", expiry_ms]},
496
- "else": "$expiresAt",
490
+ "then": {"$add": ["$expireAt", expiry_ms]},
491
+ "else": "$expireAt",
497
492
  }
498
493
  },
499
494
  }
500
495
  }
501
496
  ],
502
497
  return_document=self.proxy_dependency.module.ReturnDocument.AFTER,
503
- projection=["currentCount", "previousCount", "expiresAt"],
498
+ projection=["currentCount", "previousCount", "expireAt"],
504
499
  ):
505
500
  expires_at = (
506
- (result["expiresAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
507
- if result.get("expiresAt")
501
+ (result["expireAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
502
+ if result.get("expireAt")
508
503
  else time.time()
509
504
  )
510
505
  current_ttl = max(0, expires_at - time.time())
@@ -139,9 +139,7 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
139
139
  ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
140
140
  return self.bridge.base_exceptions
141
141
 
142
- async def incr(
143
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
144
- ) -> int:
142
+ async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
145
143
  """
146
144
  increments the counter for a given rate limit key
147
145
 
@@ -150,7 +148,7 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
150
148
  :param amount: the number to increment by
151
149
  """
152
150
 
153
- return await self.bridge.incr(key, expiry, elastic_expiry, amount)
151
+ return await self.bridge.incr(key, expiry, amount)
154
152
 
155
153
  async def get(self, key: str) -> int:
156
154
  """
@@ -68,7 +68,6 @@ class RedisBridge(ABC):
68
68
  self,
69
69
  key: str,
70
70
  expiry: int,
71
- elastic_expiry: bool = False,
72
71
  amount: int = 1,
73
72
  ) -> int: ...
74
73
 
@@ -112,14 +112,10 @@ class CoredisBridge(RedisBridge):
112
112
  self.SCRIPT_ACQUIRE_SLIDING_WINDOW
113
113
  )
114
114
 
115
- async def incr(
116
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
117
- ) -> int:
115
+ async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
118
116
  key = self.prefixed_key(key)
119
- value = await self.get_connection().incrby(key, amount)
120
- if elastic_expiry or value == amount:
117
+ if (value := await self.get_connection().incrby(key, amount)) == amount:
121
118
  await self.get_connection().expire(key, expiry)
122
-
123
119
  return value
124
120
 
125
121
  async def get(self, key: str) -> int:
@@ -119,7 +119,6 @@ class RedispyBridge(RedisBridge):
119
119
  self,
120
120
  key: str,
121
121
  expiry: int,
122
- elastic_expiry: bool = False,
123
122
  amount: int = 1,
124
123
  ) -> int:
125
124
  """
@@ -131,13 +130,7 @@ class RedispyBridge(RedisBridge):
131
130
  :param amount: the number to increment by
132
131
  """
133
132
  key = self.prefixed_key(key)
134
-
135
- if elastic_expiry:
136
- value = await self.get_connection().incrby(key, amount)
137
- await self.get_connection().expire(key, expiry)
138
- return value
139
- else:
140
- return cast(int, await self.lua_incr_expire([key], [expiry, amount]))
133
+ return cast(int, await self.lua_incr_expire([key], [expiry, amount]))
141
134
 
142
135
  async def get(self, key: str) -> int:
143
136
  """
limits/aio/strategies.py CHANGED
@@ -8,7 +8,7 @@ import time
8
8
  from abc import ABC, 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 import RateLimitItem
14
14
  from ..storage import StorageTypes
@@ -150,7 +150,6 @@ class FixedWindowRateLimiter(RateLimiter):
150
150
  await self.storage.incr(
151
151
  item.key_for(*identifiers),
152
152
  item.get_expiry(),
153
- elastic_expiry=False,
154
153
  amount=cost,
155
154
  )
156
155
  <= item.amount
@@ -304,34 +303,8 @@ class SlidingWindowCounterRateLimiter(RateLimiter):
304
303
  return WindowStats(now + min(previous_reset_in, current_reset_in), remaining)
305
304
 
306
305
 
307
- @deprecated(version="4.1")
308
- class FixedWindowElasticExpiryRateLimiter(FixedWindowRateLimiter):
309
- """
310
- Reference: :ref:`strategies:fixed window with elastic expiry`
311
- """
312
-
313
- async def hit(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
314
- """
315
- Consume the rate limit
316
-
317
- :param item: a :class:`limits.limits.RateLimitItem` instance
318
- :param identifiers: variable list of strings to uniquely identify the
319
- limit
320
- :param cost: The cost of this hit, default 1
321
- """
322
- amount = await self.storage.incr(
323
- item.key_for(*identifiers),
324
- item.get_expiry(),
325
- elastic_expiry=True,
326
- amount=cost,
327
- )
328
-
329
- return amount <= item.amount
330
-
331
-
332
306
  STRATEGIES = {
333
307
  "sliding-window-counter": SlidingWindowCounterRateLimiter,
334
308
  "fixed-window": FixedWindowRateLimiter,
335
- "fixed-window-elastic-expiry": FixedWindowElasticExpiryRateLimiter,
336
309
  "moving-window": MovingWindowRateLimiter,
337
310
  }
@@ -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
@@ -153,6 +153,8 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
153
153
  Return multiple counters at once
154
154
 
155
155
  :param keys: the keys to get the counter values for
156
+
157
+ :meta private:
156
158
  """
157
159
  return self.storage.get_many(keys)
158
160
 
@@ -166,7 +168,6 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
166
168
  self,
167
169
  key: str,
168
170
  expiry: float,
169
- elastic_expiry: bool = False,
170
171
  amount: int = 1,
171
172
  set_expiration_key: bool = True,
172
173
  ) -> int:
@@ -175,43 +176,21 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
175
176
 
176
177
  :param key: the key to increment
177
178
  :param expiry: amount in seconds for the key to expire in
178
- :param elastic_expiry: whether to keep extending the rate limit
179
179
  window every hit.
180
180
  :param amount: the number to increment by
181
181
  :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
182
  """
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
-
183
+ if (
184
+ value := self.call_memcached_func(
185
+ self.storage.incr, key, amount, noreply=False
186
+ )
187
+ ) is not None:
196
188
  return value
197
189
  else:
198
190
  if not self.call_memcached_func(
199
191
  self.storage.add, key, amount, ceil(expiry), noreply=False
200
192
  ):
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
193
+ return self.storage.incr(key, amount) or amount
215
194
  else:
216
195
  if set_expiration_key:
217
196
  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
+ 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]
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,