limits 4.0.1__py3-none-any.whl → 4.1__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,12 +1,18 @@
1
1
  import asyncio
2
2
  import time
3
- from collections import Counter
3
+ from collections import Counter, defaultdict
4
+ from math import floor
4
5
 
5
6
  from deprecated.sphinx import versionadded
6
7
 
7
8
  import limits.typing
8
- from limits.aio.storage.base import MovingWindowSupport, Storage
9
- from limits.typing import Dict, List, Optional, Tuple, Type, Union
9
+ from limits.aio.storage.base import (
10
+ MovingWindowSupport,
11
+ SlidingWindowCounterSupport,
12
+ Storage,
13
+ )
14
+ from limits.storage.base import TimestampedSlidingWindow
15
+ from limits.typing import Optional, Type, Union
10
16
 
11
17
 
12
18
  class LockableEntry(asyncio.Lock):
@@ -17,7 +23,9 @@ class LockableEntry(asyncio.Lock):
17
23
 
18
24
 
19
25
  @versionadded(version="2.1")
20
- class MemoryStorage(Storage, MovingWindowSupport):
26
+ class MemoryStorage(
27
+ Storage, MovingWindowSupport, SlidingWindowCounterSupport, TimestampedSlidingWindow
28
+ ):
21
29
  """
22
30
  rate limit storage using :class:`collections.Counter`
23
31
  as an in memory storage for fixed and elastic window strategies,
@@ -34,15 +42,16 @@ class MemoryStorage(Storage, MovingWindowSupport):
34
42
  self, uri: Optional[str] = None, wrap_exceptions: bool = False, **_: str
35
43
  ) -> None:
36
44
  self.storage: limits.typing.Counter[str] = Counter()
37
- self.expirations: Dict[str, float] = {}
38
- self.events: Dict[str, List[LockableEntry]] = {}
45
+ self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
46
+ self.expirations: dict[str, float] = {}
47
+ self.events: dict[str, list[LockableEntry]] = {}
39
48
  self.timer: Optional[asyncio.Task[None]] = None
40
49
  super().__init__(uri, wrap_exceptions=wrap_exceptions, **_)
41
50
 
42
51
  @property
43
52
  def base_exceptions(
44
53
  self,
45
- ) -> Union[Type[Exception], Tuple[Type[Exception], ...]]: # pragma: no cover
54
+ ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
46
55
  return ValueError
47
56
 
48
57
  async def __expire_events(self) -> None:
@@ -56,13 +65,14 @@ class MemoryStorage(Storage, MovingWindowSupport):
56
65
  if self.expirations[key] <= time.time():
57
66
  self.storage.pop(key, None)
58
67
  self.expirations.pop(key, None)
68
+ self.locks.pop(key, None)
59
69
 
60
70
  async def __schedule_expiry(self) -> None:
61
71
  if not self.timer or self.timer.done():
62
72
  self.timer = asyncio.create_task(self.__expire_events())
63
73
 
64
74
  async def incr(
65
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
75
+ self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
66
76
  ) -> int:
67
77
  """
68
78
  increments the counter for a given rate limit key
@@ -75,10 +85,24 @@ class MemoryStorage(Storage, MovingWindowSupport):
75
85
  """
76
86
  await self.get(key)
77
87
  await self.__schedule_expiry()
78
- self.storage[key] += amount
88
+ async with self.locks[key]:
89
+ self.storage[key] += amount
79
90
 
80
- if elastic_expiry or self.storage[key] == amount:
81
- self.expirations[key] = time.time() + expiry
91
+ if elastic_expiry or self.storage[key] == amount:
92
+ self.expirations[key] = time.time() + expiry
93
+
94
+ return self.storage.get(key, amount)
95
+
96
+ async def decr(self, key: str, amount: int = 1) -> int:
97
+ """
98
+ decrements the counter for a given rate limit key. 0 is the minimum allowed value.
99
+
100
+ :param amount: the number to increment by
101
+ """
102
+ await self.get(key)
103
+ await self.__schedule_expiry()
104
+ async with self.locks[key]:
105
+ self.storage[key] = max(self.storage[key] - amount, 0)
82
106
 
83
107
  return self.storage.get(key, amount)
84
108
 
@@ -86,10 +110,10 @@ class MemoryStorage(Storage, MovingWindowSupport):
86
110
  """
87
111
  :param key: the key to get the counter value for
88
112
  """
89
-
90
113
  if self.expirations.get(key, 0) <= time.time():
91
114
  self.storage.pop(key, None)
92
115
  self.expirations.pop(key, None)
116
+ self.locks.pop(key, None)
93
117
 
94
118
  return self.storage.get(key, 0)
95
119
 
@@ -100,6 +124,7 @@ class MemoryStorage(Storage, MovingWindowSupport):
100
124
  self.storage.pop(key, None)
101
125
  self.expirations.pop(key, None)
102
126
  self.events.pop(key, None)
127
+ self.locks.pop(key, None)
103
128
 
104
129
  async def acquire_entry(
105
130
  self, key: str, limit: int, expiry: int, amount: int = 1
@@ -153,7 +178,7 @@ class MemoryStorage(Storage, MovingWindowSupport):
153
178
  # FIXME: arg limit is not used
154
179
  async def get_moving_window(
155
180
  self, key: str, limit: int, expiry: int
156
- ) -> Tuple[float, int]:
181
+ ) -> tuple[float, int]:
157
182
  """
158
183
  returns the starting point and the number of entries in the moving
159
184
  window
@@ -171,6 +196,65 @@ class MemoryStorage(Storage, MovingWindowSupport):
171
196
 
172
197
  return timestamp, acquired
173
198
 
199
+ async def acquire_sliding_window_entry(
200
+ self,
201
+ key: str,
202
+ limit: int,
203
+ expiry: int,
204
+ amount: int = 1,
205
+ ) -> bool:
206
+ if amount > limit:
207
+ return False
208
+ now = time.time()
209
+ previous_key, current_key = self.sliding_window_keys(key, expiry, now)
210
+ (
211
+ previous_count,
212
+ previous_ttl,
213
+ current_count,
214
+ _,
215
+ ) = await self._get_sliding_window_info(previous_key, current_key, expiry, now)
216
+ weighted_count = previous_count * previous_ttl / expiry + current_count
217
+ if floor(weighted_count) + amount > limit:
218
+ return False
219
+ else:
220
+ # Hit, increase the current counter.
221
+ # If the counter doesn't exist yet, set twice the theorical expiry.
222
+ current_count = await self.incr(current_key, 2 * expiry, amount=amount)
223
+ weighted_count = previous_count * previous_ttl / expiry + current_count
224
+ if floor(weighted_count) > limit:
225
+ # Another hit won the race condition: revert the incrementation and refuse this hit
226
+ # Limitation: during high concurrency at the end of the window,
227
+ # the counter is shifted and cannot be decremented, so less requests than expected are allowed.
228
+ await self.decr(current_key, amount)
229
+ # print("Concurrent call, reverting the counter increase")
230
+ return False
231
+ return True
232
+
233
+ async def get_sliding_window(
234
+ self, key: str, expiry: int
235
+ ) -> tuple[int, float, int, float]:
236
+ now = time.time()
237
+ previous_key, current_key = self.sliding_window_keys(key, expiry, now)
238
+ return await self._get_sliding_window_info(
239
+ previous_key, current_key, expiry, now
240
+ )
241
+
242
+ async def _get_sliding_window_info(
243
+ self,
244
+ previous_key: str,
245
+ current_key: str,
246
+ expiry: int,
247
+ now: float,
248
+ ) -> tuple[int, float, int, float]:
249
+ previous_count = await self.get(previous_key)
250
+ current_count = await self.get(current_key)
251
+ if previous_count == 0:
252
+ previous_ttl = float(0)
253
+ else:
254
+ previous_ttl = (1 - (((now - expiry) / expiry) % 1)) * expiry
255
+ current_ttl = (1 - ((now / expiry) % 1)) * expiry + expiry
256
+ return previous_count, previous_ttl, current_count, current_ttl
257
+
174
258
  async def check(self) -> bool:
175
259
  """
176
260
  check if storage is healthy
@@ -183,5 +267,6 @@ class MemoryStorage(Storage, MovingWindowSupport):
183
267
  self.storage.clear()
184
268
  self.expirations.clear()
185
269
  self.events.clear()
270
+ self.locks.clear()
186
271
 
187
272
  return num_items
@@ -3,12 +3,22 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import datetime
5
5
  import time
6
- from typing import cast
7
6
 
8
7
  from deprecated.sphinx import versionadded, versionchanged
9
8
 
10
- from limits.aio.storage.base import MovingWindowSupport, Storage
11
- from limits.typing import Dict, List, Optional, ParamSpec, Tuple, Type, TypeVar, Union
9
+ from limits.aio.storage.base import (
10
+ MovingWindowSupport,
11
+ SlidingWindowCounterSupport,
12
+ Storage,
13
+ )
14
+ from limits.typing import (
15
+ Optional,
16
+ ParamSpec,
17
+ Type,
18
+ TypeVar,
19
+ Union,
20
+ cast,
21
+ )
12
22
  from limits.util import get_dependency
13
23
 
14
24
  P = ParamSpec("P")
@@ -20,7 +30,7 @@ R = TypeVar("R")
20
30
  version="3.14.0",
21
31
  reason="Added option to select custom collection names for windows & counters",
22
32
  )
23
- class MongoDBStorage(Storage, MovingWindowSupport):
33
+ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
24
34
  """
25
35
  Rate limit storage with MongoDB as backend.
26
36
 
@@ -50,7 +60,8 @@ class MongoDBStorage(Storage, MovingWindowSupport):
50
60
  collections.
51
61
  :param counter_collection_name: The collection name to use for individual counters
52
62
  used in fixed window strategies
53
- :param window_collection_name: The collection name to use for moving window storage
63
+ :param window_collection_name: The collection name to use for sliding & moving window
64
+ storage
54
65
  :param wrap_exceptions: Whether to wrap storage exceptions in
55
66
  :exc:`limits.errors.StorageError` before raising it.
56
67
  :param options: all remaining keyword arguments are passed
@@ -83,7 +94,7 @@ class MongoDBStorage(Storage, MovingWindowSupport):
83
94
  @property
84
95
  def base_exceptions(
85
96
  self,
86
- ) -> Union[Type[Exception], Tuple[Type[Exception], ...]]: # pragma: no cover
97
+ ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
87
98
  return self.lib_errors.PyMongoError # type: ignore
88
99
 
89
100
  @property
@@ -224,7 +235,7 @@ class MongoDBStorage(Storage, MovingWindowSupport):
224
235
 
225
236
  async def get_moving_window(
226
237
  self, key: str, limit: int, expiry: int
227
- ) -> Tuple[float, int]:
238
+ ) -> tuple[float, int]:
228
239
  """
229
240
  returns the starting point and the number of entries in the moving
230
241
  window
@@ -281,9 +292,9 @@ class MongoDBStorage(Storage, MovingWindowSupport):
281
292
 
282
293
  timestamp = time.time()
283
294
  try:
284
- updates: Dict[
295
+ updates: dict[
285
296
  str,
286
- Dict[str, Union[datetime.datetime, Dict[str, Union[List[float], int]]]],
297
+ dict[str, Union[datetime.datetime, dict[str, Union[list[float], int]]]],
287
298
  ] = {
288
299
  "$push": {
289
300
  "entries": {
@@ -312,3 +323,200 @@ class MongoDBStorage(Storage, MovingWindowSupport):
312
323
  return True
313
324
  except self.proxy_dependency.module.errors.DuplicateKeyError:
314
325
  return False
326
+
327
+ async def acquire_sliding_window_entry(
328
+ self, key: str, limit: int, expiry: int, amount: int = 1
329
+ ) -> bool:
330
+ await self.create_indices()
331
+ expiry_ms = expiry * 1000
332
+ result = await self.database[
333
+ self.__collection_mapping["windows"]
334
+ ].find_one_and_update(
335
+ {"_id": key},
336
+ [
337
+ {
338
+ "$set": {
339
+ "previousCount": {
340
+ "$cond": {
341
+ "if": {
342
+ "$lte": [
343
+ {"$subtract": ["$expiresAt", "$$NOW"]},
344
+ expiry_ms,
345
+ ]
346
+ },
347
+ "then": {"$ifNull": ["$currentCount", 0]},
348
+ "else": {"$ifNull": ["$previousCount", 0]},
349
+ }
350
+ },
351
+ }
352
+ },
353
+ {
354
+ "$set": {
355
+ "currentCount": {
356
+ "$cond": {
357
+ "if": {
358
+ "$lte": [
359
+ {"$subtract": ["$expiresAt", "$$NOW"]},
360
+ expiry_ms,
361
+ ]
362
+ },
363
+ "then": 0,
364
+ "else": {"$ifNull": ["$currentCount", 0]},
365
+ }
366
+ },
367
+ "expiresAt": {
368
+ "$cond": {
369
+ "if": {
370
+ "$lte": [
371
+ {"$subtract": ["$expiresAt", "$$NOW"]},
372
+ expiry_ms,
373
+ ]
374
+ },
375
+ "then": {
376
+ "$cond": {
377
+ "if": {"$gt": ["$expiresAt", 0]},
378
+ "then": {"$add": ["$expiresAt", expiry_ms]},
379
+ "else": {"$add": ["$$NOW", 2 * expiry_ms]},
380
+ }
381
+ },
382
+ "else": "$expiresAt",
383
+ }
384
+ },
385
+ }
386
+ },
387
+ {
388
+ "$set": {
389
+ "curWeightedCount": {
390
+ "$floor": {
391
+ "$add": [
392
+ {
393
+ "$multiply": [
394
+ "$previousCount",
395
+ {
396
+ "$divide": [
397
+ {
398
+ "$max": [
399
+ 0,
400
+ {
401
+ "$subtract": [
402
+ "$expiresAt",
403
+ {
404
+ "$add": [
405
+ "$$NOW",
406
+ expiry_ms,
407
+ ]
408
+ },
409
+ ]
410
+ },
411
+ ]
412
+ },
413
+ expiry_ms,
414
+ ]
415
+ },
416
+ ]
417
+ },
418
+ "$currentCount",
419
+ ]
420
+ }
421
+ }
422
+ }
423
+ },
424
+ {
425
+ "$set": {
426
+ "currentCount": {
427
+ "$cond": {
428
+ "if": {
429
+ "$lte": [
430
+ {"$add": ["$curWeightedCount", amount]},
431
+ limit,
432
+ ]
433
+ },
434
+ "then": {"$add": ["$currentCount", amount]},
435
+ "else": "$currentCount",
436
+ }
437
+ }
438
+ }
439
+ },
440
+ {
441
+ "$set": {
442
+ "_acquired": {
443
+ "$lte": [{"$add": ["$curWeightedCount", amount]}, limit]
444
+ }
445
+ }
446
+ },
447
+ {"$unset": ["curWeightedCount"]},
448
+ ],
449
+ return_document=self.proxy_dependency.module.ReturnDocument.AFTER,
450
+ upsert=True,
451
+ )
452
+
453
+ return cast(bool, result["_acquired"])
454
+
455
+ async def get_sliding_window(
456
+ self, key: str, expiry: int
457
+ ) -> tuple[int, float, int, float]:
458
+ expiry_ms = expiry * 1000
459
+ if result := await self.database[
460
+ self.__collection_mapping["windows"]
461
+ ].find_one_and_update(
462
+ {"_id": key},
463
+ [
464
+ {
465
+ "$set": {
466
+ "previousCount": {
467
+ "$cond": {
468
+ "if": {
469
+ "$lte": [
470
+ {"$subtract": ["$expiresAt", "$$NOW"]},
471
+ expiry_ms,
472
+ ]
473
+ },
474
+ "then": {"$ifNull": ["$currentCount", 0]},
475
+ "else": {"$ifNull": ["$previousCount", 0]},
476
+ }
477
+ },
478
+ "currentCount": {
479
+ "$cond": {
480
+ "if": {
481
+ "$lte": [
482
+ {"$subtract": ["$expiresAt", "$$NOW"]},
483
+ expiry_ms,
484
+ ]
485
+ },
486
+ "then": 0,
487
+ "else": {"$ifNull": ["$currentCount", 0]},
488
+ }
489
+ },
490
+ "expiresAt": {
491
+ "$cond": {
492
+ "if": {
493
+ "$lte": [
494
+ {"$subtract": ["$expiresAt", "$$NOW"]},
495
+ expiry_ms,
496
+ ]
497
+ },
498
+ "then": {"$add": ["$expiresAt", expiry_ms]},
499
+ "else": "$expiresAt",
500
+ }
501
+ },
502
+ }
503
+ }
504
+ ],
505
+ return_document=self.proxy_dependency.module.ReturnDocument.AFTER,
506
+ projection=["currentCount", "previousCount", "expiresAt"],
507
+ ):
508
+ expires_at = (
509
+ (result["expiresAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
510
+ if result.get("expiresAt")
511
+ else time.time()
512
+ )
513
+ current_ttl = max(0, expires_at - time.time())
514
+ prev_ttl = max(0, current_ttl - expiry if result["previousCount"] else 0)
515
+
516
+ return (
517
+ result["previousCount"],
518
+ prev_ttl,
519
+ result["currentCount"],
520
+ current_ttl,
521
+ )
522
+ return 0, 0.0, 0, 0.0
@@ -5,9 +5,13 @@ from typing import TYPE_CHECKING, cast
5
5
  from deprecated.sphinx import versionadded
6
6
  from packaging.version import Version
7
7
 
8
- from limits.aio.storage.base import MovingWindowSupport, Storage
8
+ from limits.aio.storage.base import (
9
+ MovingWindowSupport,
10
+ SlidingWindowCounterSupport,
11
+ Storage,
12
+ )
9
13
  from limits.errors import ConfigurationError
10
- from limits.typing import AsyncRedisClient, Dict, Optional, Tuple, Type, Union
14
+ from limits.typing import AsyncRedisClient, Optional, Type, Union
11
15
  from limits.util import get_package_data
12
16
 
13
17
  if TYPE_CHECKING:
@@ -24,9 +28,15 @@ class RedisInteractor:
24
28
  )
25
29
  SCRIPT_CLEAR_KEYS = get_package_data(f"{RES_DIR}/clear_keys.lua")
26
30
  SCRIPT_INCR_EXPIRE = get_package_data(f"{RES_DIR}/incr_expire.lua")
31
+ SCRIPT_SLIDING_WINDOW = get_package_data(f"{RES_DIR}/sliding_window.lua")
32
+ SCRIPT_ACQUIRE_SLIDING_WINDOW = get_package_data(
33
+ f"{RES_DIR}/acquire_sliding_window.lua"
34
+ )
27
35
 
28
36
  lua_moving_window: "coredis.commands.Script[bytes]"
29
- lua_acquire_window: "coredis.commands.Script[bytes]"
37
+ lua_acquire_moving_window: "coredis.commands.Script[bytes]"
38
+ lua_sliding_window: "coredis.commands.Script[bytes]"
39
+ lua_acquire_sliding_window: "coredis.commands.Script[bytes]"
30
40
  lua_clear_keys: "coredis.commands.Script[bytes]"
31
41
  lua_incr_expire: "coredis.commands.Script[bytes]"
32
42
 
@@ -78,14 +88,14 @@ class RedisInteractor:
78
88
 
79
89
  async def get_moving_window(
80
90
  self, key: str, limit: int, expiry: int
81
- ) -> Tuple[float, int]:
91
+ ) -> tuple[float, int]:
82
92
  """
83
93
  returns the starting point and the number of entries in the moving
84
94
  window
85
95
 
86
96
  :param key: rate limit key
87
97
  :param expiry: expiry of entry
88
- :return: (start of window, number of acquired entries)
98
+ :return: (previous count, previous TTL, current count, current TTL)
89
99
  """
90
100
  key = self.prefixed_key(key)
91
101
  timestamp = time.time()
@@ -96,6 +106,23 @@ class RedisInteractor:
96
106
  return float(window[0]), window[1] # type: ignore
97
107
  return timestamp, 0
98
108
 
109
+ async def get_sliding_window(
110
+ self, key: str, expiry: int
111
+ ) -> tuple[int, float, int, float]:
112
+ previous_key = self.prefixed_key(self._previous_window_key(key))
113
+ current_key = self.prefixed_key(self._current_window_key(key))
114
+
115
+ if window := await self.lua_sliding_window.execute(
116
+ [previous_key, current_key], [expiry]
117
+ ):
118
+ return (
119
+ int(window[0] or 0), # type: ignore
120
+ max(0, float(window[1] or 0)) / 1000, # type: ignore
121
+ int(window[2] or 0), # type: ignore
122
+ max(0, float(window[3] or 0)) / 1000, # type: ignore
123
+ )
124
+ return 0, 0.0, 0, 0.0
125
+
99
126
  async def _acquire_entry(
100
127
  self,
101
128
  key: str,
@@ -112,12 +139,27 @@ class RedisInteractor:
112
139
  """
113
140
  key = self.prefixed_key(key)
114
141
  timestamp = time.time()
115
- acquired = await self.lua_acquire_window.execute(
142
+ acquired = await self.lua_acquire_moving_window.execute(
116
143
  [key], [timestamp, limit, expiry, amount]
117
144
  )
118
145
 
119
146
  return bool(acquired)
120
147
 
148
+ async def _acquire_sliding_window_entry(
149
+ self,
150
+ previous_key: str,
151
+ current_key: str,
152
+ limit: int,
153
+ expiry: int,
154
+ amount: int = 1,
155
+ ) -> bool:
156
+ previous_key = self.prefixed_key(previous_key)
157
+ current_key = self.prefixed_key(current_key)
158
+ acquired = await self.lua_acquire_sliding_window.execute(
159
+ [previous_key, current_key], [limit, expiry, amount]
160
+ )
161
+ return bool(acquired)
162
+
121
163
  async def _get_expiry(self, key: str, connection: AsyncRedisClient) -> float:
122
164
  """
123
165
  :param key: the key to get the expiry for
@@ -140,9 +182,35 @@ class RedisInteractor:
140
182
  except: # noqa
141
183
  return False
142
184
 
185
+ def _current_window_key(self, key: str) -> str:
186
+ """
187
+ Return the current window's storage key (Sliding window strategy)
188
+
189
+ Contrary to other strategies that have one key per rate limit item,
190
+ this strategy has two keys per rate limit item than must be on the same machine.
191
+ To keep the current key and the previous key on the same Redis cluster node,
192
+ curly braces are added.
193
+
194
+ Eg: "{constructed_key}"
195
+ """
196
+ return f"{{{key}}}"
197
+
198
+ def _previous_window_key(self, key: str) -> str:
199
+ """
200
+ Return the previous window's storage key (Sliding window strategy).
201
+
202
+ Curvy braces are added on the common pattern with the current window's key,
203
+ so the current and the previous key are stored on the same Redis cluster node.
204
+
205
+ Eg: "{constructed_key}/-1"
206
+ """
207
+ return f"{self._current_window_key(key)}/-1"
208
+
143
209
 
144
210
  @versionadded(version="2.1")
145
- class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
211
+ class RedisStorage(
212
+ RedisInteractor, Storage, MovingWindowSupport, SlidingWindowCounterSupport
213
+ ):
146
214
  """
147
215
  Rate limit storage with redis as backend.
148
216
 
@@ -200,18 +268,22 @@ class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
200
268
  @property
201
269
  def base_exceptions(
202
270
  self,
203
- ) -> Union[Type[Exception], Tuple[Type[Exception], ...]]: # pragma: no cover
271
+ ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
204
272
  return self.dependency.exceptions.RedisError # type: ignore[no-any-return]
205
273
 
206
274
  def initialize_storage(self, _uri: str) -> None:
207
275
  # all these methods are coroutines, so must be called with await
208
276
  self.lua_moving_window = self.storage.register_script(self.SCRIPT_MOVING_WINDOW)
209
- self.lua_acquire_window = self.storage.register_script(
277
+ self.lua_acquire_moving_window = self.storage.register_script(
210
278
  self.SCRIPT_ACQUIRE_MOVING_WINDOW
211
279
  )
212
280
  self.lua_clear_keys = self.storage.register_script(self.SCRIPT_CLEAR_KEYS)
213
- self.lua_incr_expire = self.storage.register_script(
214
- RedisStorage.SCRIPT_INCR_EXPIRE
281
+ self.lua_incr_expire = self.storage.register_script(self.SCRIPT_INCR_EXPIRE)
282
+ self.lua_sliding_window = self.storage.register_script(
283
+ self.SCRIPT_SLIDING_WINDOW
284
+ )
285
+ self.lua_acquire_sliding_window = self.storage.register_script(
286
+ self.SCRIPT_ACQUIRE_SLIDING_WINDOW
215
287
  )
216
288
 
217
289
  async def incr(
@@ -261,6 +333,19 @@ class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
261
333
 
262
334
  return await super()._acquire_entry(key, limit, expiry, self.storage, amount)
263
335
 
336
+ async def acquire_sliding_window_entry(
337
+ self,
338
+ key: str,
339
+ limit: int,
340
+ expiry: int,
341
+ amount: int = 1,
342
+ ) -> bool:
343
+ current_key = self._current_window_key(key)
344
+ previous_key = self._previous_window_key(key)
345
+ return await super()._acquire_sliding_window_entry(
346
+ previous_key, current_key, limit, expiry, amount
347
+ )
348
+
264
349
  async def get_expiry(self, key: str) -> float:
265
350
  """
266
351
  :param key: the key to get the expiry for
@@ -302,7 +387,7 @@ class RedisClusterStorage(RedisStorage):
302
387
  The storage schemes for redis cluster to be used in an async context
303
388
  """
304
389
 
305
- DEFAULT_OPTIONS: Dict[str, Union[float, str, bool]] = {
390
+ DEFAULT_OPTIONS: dict[str, Union[float, str, bool]] = {
306
391
  "max_connections": 1000,
307
392
  }
308
393
  "Default options passed to :class:`coredis.RedisCluster`"
@@ -322,7 +407,7 @@ class RedisClusterStorage(RedisStorage):
322
407
  available or if the redis host cannot be pinged.
323
408
  """
324
409
  parsed = urllib.parse.urlparse(uri)
325
- parsed_auth: Dict[str, Union[float, str, bool]] = {}
410
+ parsed_auth: dict[str, Union[float, str, bool]] = {}
326
411
 
327
412
  if parsed.username:
328
413
  parsed_auth["username"] = parsed.username
@@ -386,7 +471,7 @@ class RedisSentinelStorage(RedisStorage):
386
471
  uri: str,
387
472
  service_name: Optional[str] = None,
388
473
  use_replicas: bool = True,
389
- sentinel_kwargs: Optional[Dict[str, Union[float, str, bool]]] = None,
474
+ sentinel_kwargs: Optional[dict[str, Union[float, str, bool]]] = None,
390
475
  **options: Union[float, str, bool],
391
476
  ):
392
477
  """
@@ -407,7 +492,7 @@ class RedisSentinelStorage(RedisStorage):
407
492
  sentinel_configuration = []
408
493
  connection_options = options.copy()
409
494
  sentinel_options = sentinel_kwargs.copy() if sentinel_kwargs else {}
410
- parsed_auth: Dict[str, Union[float, str, bool]] = {}
495
+ parsed_auth: dict[str, Union[float, str, bool]] = {}
411
496
 
412
497
  if parsed.username:
413
498
  parsed_auth["username"] = parsed.username