limits 4.1__py3-none-any.whl → 4.3__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.
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING, cast
5
+
6
+ from limits.aio.storage.redis.bridge import RedisBridge
7
+ from limits.errors import ConfigurationError
8
+ from limits.typing import AsyncCoRedisClient, Callable
9
+
10
+ if TYPE_CHECKING:
11
+ import coredis
12
+
13
+
14
+ class CoredisBridge(RedisBridge):
15
+ DEFAULT_CLUSTER_OPTIONS: dict[str, float | str | bool] = {
16
+ "max_connections": 1000,
17
+ }
18
+ "Default options passed to :class:`coredis.RedisCluster`"
19
+
20
+ @property
21
+ def base_exceptions(self) -> type[Exception] | tuple[type[Exception], ...]:
22
+ return (self.dependency.exceptions.RedisError,)
23
+
24
+ def use_sentinel(
25
+ self,
26
+ service_name: str | None,
27
+ use_replicas: bool,
28
+ sentinel_kwargs: dict[str, str | float | bool] | None,
29
+ **options: str | float | bool,
30
+ ) -> None:
31
+ sentinel_configuration = []
32
+ connection_options = options.copy()
33
+
34
+ sep = self.parsed_uri.netloc.find("@") + 1
35
+
36
+ for loc in self.parsed_uri.netloc[sep:].split(","):
37
+ host, port = loc.split(":")
38
+ sentinel_configuration.append((host, int(port)))
39
+ service_name = (
40
+ self.parsed_uri.path.replace("/", "")
41
+ if self.parsed_uri.path
42
+ else service_name
43
+ )
44
+
45
+ if service_name is None:
46
+ raise ConfigurationError("'service_name' not provided")
47
+
48
+ self.sentinel = self.dependency.sentinel.Sentinel(
49
+ sentinel_configuration,
50
+ sentinel_kwargs={**self.parsed_auth, **(sentinel_kwargs or {})},
51
+ **{**self.parsed_auth, **connection_options},
52
+ )
53
+ self.storage = self.sentinel.primary_for(service_name)
54
+ self.storage_replica = self.sentinel.replica_for(service_name)
55
+ self.connection_getter = lambda readonly: (
56
+ self.storage_replica if readonly and use_replicas else self.storage
57
+ )
58
+
59
+ def use_basic(self, **options: str | float | bool) -> None:
60
+ if connection_pool := options.pop("connection_pool", None):
61
+ self.storage = self.dependency.Redis(
62
+ connection_pool=connection_pool, **options
63
+ )
64
+ else:
65
+ self.storage = self.dependency.Redis.from_url(self.uri, **options)
66
+
67
+ self.connection_getter = lambda _: self.storage
68
+
69
+ def use_cluster(self, **options: str | float | bool) -> None:
70
+ sep = self.parsed_uri.netloc.find("@") + 1
71
+ cluster_hosts: list[dict[str, int | str]] = []
72
+ cluster_hosts.extend(
73
+ {"host": host, "port": int(port)}
74
+ for loc in self.parsed_uri.netloc[sep:].split(",")
75
+ if loc
76
+ for host, port in [loc.split(":")]
77
+ )
78
+ self.storage = self.dependency.RedisCluster(
79
+ startup_nodes=cluster_hosts,
80
+ **{**self.DEFAULT_CLUSTER_OPTIONS, **self.parsed_auth, **options},
81
+ )
82
+ self.connection_getter = lambda _: self.storage
83
+
84
+ lua_moving_window: coredis.commands.Script[bytes]
85
+ lua_acquire_moving_window: coredis.commands.Script[bytes]
86
+ lua_sliding_window: coredis.commands.Script[bytes]
87
+ lua_acquire_sliding_window: coredis.commands.Script[bytes]
88
+ lua_clear_keys: coredis.commands.Script[bytes]
89
+ lua_incr_expire: coredis.commands.Script[bytes]
90
+ connection_getter: Callable[[bool], AsyncCoRedisClient]
91
+
92
+ def get_connection(self, readonly: bool = False) -> AsyncCoRedisClient:
93
+ return self.connection_getter(readonly)
94
+
95
+ def register_scripts(self) -> None:
96
+ self.lua_moving_window = self.get_connection().register_script(
97
+ self.SCRIPT_MOVING_WINDOW
98
+ )
99
+ self.lua_acquire_moving_window = self.get_connection().register_script(
100
+ self.SCRIPT_ACQUIRE_MOVING_WINDOW
101
+ )
102
+ self.lua_clear_keys = self.get_connection().register_script(
103
+ self.SCRIPT_CLEAR_KEYS
104
+ )
105
+ self.lua_incr_expire = self.get_connection().register_script(
106
+ self.SCRIPT_INCR_EXPIRE
107
+ )
108
+ self.lua_sliding_window = self.get_connection().register_script(
109
+ self.SCRIPT_SLIDING_WINDOW
110
+ )
111
+ self.lua_acquire_sliding_window = self.get_connection().register_script(
112
+ self.SCRIPT_ACQUIRE_SLIDING_WINDOW
113
+ )
114
+
115
+ async def incr(
116
+ self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
117
+ ) -> int:
118
+ key = self.prefixed_key(key)
119
+ value = await self.get_connection().incrby(key, amount)
120
+ if elastic_expiry or value == amount:
121
+ await self.get_connection().expire(key, expiry)
122
+
123
+ return value
124
+
125
+ async def get(self, key: str) -> int:
126
+ key = self.prefixed_key(key)
127
+ return int(await self.get_connection(readonly=True).get(key) or 0)
128
+
129
+ async def clear(self, key: str) -> None:
130
+ key = self.prefixed_key(key)
131
+ await self.get_connection().delete([key])
132
+
133
+ async def lua_reset(self) -> int | None:
134
+ return cast(int, await self.lua_clear_keys.execute([self.prefixed_key("*")]))
135
+
136
+ async def get_moving_window(
137
+ self, key: str, limit: int, expiry: int
138
+ ) -> tuple[float, int]:
139
+ key = self.prefixed_key(key)
140
+ timestamp = time.time()
141
+ window = await self.lua_moving_window.execute(
142
+ [key], [timestamp - expiry, limit]
143
+ )
144
+ if window:
145
+ return float(window[0]), window[1] # type: ignore
146
+ return timestamp, 0
147
+
148
+ async def get_sliding_window(
149
+ self, previous_key: str, current_key: str, expiry: int
150
+ ) -> tuple[int, float, int, float]:
151
+ previous_key = self.prefixed_key(previous_key)
152
+ current_key = self.prefixed_key(current_key)
153
+
154
+ if window := await self.lua_sliding_window.execute(
155
+ [previous_key, current_key], [expiry]
156
+ ):
157
+ return (
158
+ int(window[0] or 0), # type: ignore
159
+ max(0, float(window[1] or 0)) / 1000, # type: ignore
160
+ int(window[2] or 0), # type: ignore
161
+ max(0, float(window[3] or 0)) / 1000, # type: ignore
162
+ )
163
+ return 0, 0.0, 0, 0.0
164
+
165
+ async def acquire_entry(
166
+ self, key: str, limit: int, expiry: int, amount: int = 1
167
+ ) -> bool:
168
+ key = self.prefixed_key(key)
169
+ timestamp = time.time()
170
+ acquired = await self.lua_acquire_moving_window.execute(
171
+ [key], [timestamp, limit, expiry, amount]
172
+ )
173
+
174
+ return bool(acquired)
175
+
176
+ async def acquire_sliding_window_entry(
177
+ self,
178
+ previous_key: str,
179
+ current_key: str,
180
+ limit: int,
181
+ expiry: int,
182
+ amount: int = 1,
183
+ ) -> bool:
184
+ previous_key = self.prefixed_key(previous_key)
185
+ current_key = self.prefixed_key(current_key)
186
+ acquired = await self.lua_acquire_sliding_window.execute(
187
+ [previous_key, current_key], [limit, expiry, amount]
188
+ )
189
+ return bool(acquired)
190
+
191
+ async def get_expiry(self, key: str) -> float:
192
+ key = self.prefixed_key(key)
193
+ return max(await self.get_connection().ttl(key), 0) + time.time()
194
+
195
+ async def check(self) -> bool:
196
+ try:
197
+ await self.get_connection().ping()
198
+
199
+ return True
200
+ except: # noqa
201
+ return False
202
+
203
+ async def reset(self) -> int | None:
204
+ prefix = self.prefixed_key("*")
205
+ keys = await self.storage.keys(prefix)
206
+ count = 0
207
+ for key in keys:
208
+ count += await self.storage.delete([key])
209
+ return count
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING, cast
5
+
6
+ from limits.aio.storage.redis.bridge import RedisBridge
7
+ from limits.errors import ConfigurationError
8
+ from limits.typing import AsyncRedisClient, Callable
9
+
10
+ if TYPE_CHECKING:
11
+ import redis.commands
12
+
13
+
14
+ class RedispyBridge(RedisBridge):
15
+ DEFAULT_CLUSTER_OPTIONS: dict[str, float | str | bool] = {
16
+ "max_connections": 1000,
17
+ }
18
+ "Default options passed to :class:`redis.asyncio.RedisCluster`"
19
+
20
+ @property
21
+ def base_exceptions(self) -> type[Exception] | tuple[type[Exception], ...]:
22
+ return (self.dependency.RedisError,)
23
+
24
+ def use_sentinel(
25
+ self,
26
+ service_name: str | None,
27
+ use_replicas: bool,
28
+ sentinel_kwargs: dict[str, str | float | bool] | None,
29
+ **options: str | float | bool,
30
+ ) -> None:
31
+ sentinel_configuration = []
32
+
33
+ connection_options = options.copy()
34
+
35
+ sep = self.parsed_uri.netloc.find("@") + 1
36
+
37
+ for loc in self.parsed_uri.netloc[sep:].split(","):
38
+ host, port = loc.split(":")
39
+ sentinel_configuration.append((host, int(port)))
40
+ service_name = (
41
+ self.parsed_uri.path.replace("/", "")
42
+ if self.parsed_uri.path
43
+ else service_name
44
+ )
45
+
46
+ if service_name is None:
47
+ raise ConfigurationError("'service_name' not provided")
48
+
49
+ self.sentinel = self.dependency.asyncio.Sentinel(
50
+ sentinel_configuration,
51
+ sentinel_kwargs={**self.parsed_auth, **(sentinel_kwargs or {})},
52
+ **{**self.parsed_auth, **connection_options},
53
+ )
54
+ self.storage = self.sentinel.master_for(service_name)
55
+ self.storage_replica = self.sentinel.slave_for(service_name)
56
+ self.connection_getter = lambda readonly: (
57
+ self.storage_replica if readonly and use_replicas else self.storage
58
+ )
59
+
60
+ def use_basic(self, **options: str | float | bool) -> None:
61
+ if connection_pool := options.pop("connection_pool", None):
62
+ self.storage = self.dependency.asyncio.Redis(
63
+ connection_pool=connection_pool, **options
64
+ )
65
+ else:
66
+ self.storage = self.dependency.asyncio.Redis.from_url(self.uri, **options)
67
+
68
+ self.connection_getter = lambda _: self.storage
69
+
70
+ def use_cluster(self, **options: str | float | bool) -> None:
71
+ sep = self.parsed_uri.netloc.find("@") + 1
72
+ cluster_hosts = []
73
+
74
+ for loc in self.parsed_uri.netloc[sep:].split(","):
75
+ host, port = loc.split(":")
76
+ cluster_hosts.append(
77
+ self.dependency.asyncio.cluster.ClusterNode(host=host, port=int(port))
78
+ )
79
+
80
+ self.storage = self.dependency.asyncio.RedisCluster(
81
+ startup_nodes=cluster_hosts,
82
+ **{**self.DEFAULT_CLUSTER_OPTIONS, **self.parsed_auth, **options},
83
+ )
84
+ self.connection_getter = lambda _: self.storage
85
+
86
+ lua_moving_window: redis.commands.core.Script
87
+ lua_acquire_moving_window: redis.commands.core.Script
88
+ lua_sliding_window: redis.commands.core.Script
89
+ lua_acquire_sliding_window: redis.commands.core.Script
90
+ lua_clear_keys: redis.commands.core.Script
91
+ lua_incr_expire: redis.commands.core.Script
92
+ connection_getter: Callable[[bool], AsyncRedisClient]
93
+
94
+ def get_connection(self, readonly: bool = False) -> AsyncRedisClient:
95
+ return self.connection_getter(readonly)
96
+
97
+ def register_scripts(self) -> None:
98
+ # Redis-py uses a slightly different script registration
99
+ self.lua_moving_window = self.get_connection().register_script(
100
+ self.SCRIPT_MOVING_WINDOW
101
+ )
102
+ self.lua_acquire_moving_window = self.get_connection().register_script(
103
+ self.SCRIPT_ACQUIRE_MOVING_WINDOW
104
+ )
105
+ self.lua_clear_keys = self.get_connection().register_script(
106
+ self.SCRIPT_CLEAR_KEYS
107
+ )
108
+ self.lua_incr_expire = self.get_connection().register_script(
109
+ self.SCRIPT_INCR_EXPIRE
110
+ )
111
+ self.lua_sliding_window = self.get_connection().register_script(
112
+ self.SCRIPT_SLIDING_WINDOW
113
+ )
114
+ self.lua_acquire_sliding_window = self.get_connection().register_script(
115
+ self.SCRIPT_ACQUIRE_SLIDING_WINDOW
116
+ )
117
+
118
+ async def incr(
119
+ self,
120
+ key: str,
121
+ expiry: int,
122
+ elastic_expiry: bool = False,
123
+ amount: int = 1,
124
+ ) -> int:
125
+ """
126
+ increments the counter for a given rate limit key
127
+
128
+
129
+ :param key: the key to increment
130
+ :param expiry: amount in seconds for the key to expire in
131
+ :param amount: the number to increment by
132
+ """
133
+ 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]))
141
+
142
+ async def get(self, key: str) -> int:
143
+ """
144
+
145
+ :param key: the key to get the counter value for
146
+ """
147
+
148
+ key = self.prefixed_key(key)
149
+ return int(await self.get_connection(readonly=True).get(key) or 0)
150
+
151
+ async def clear(self, key: str) -> None:
152
+ """
153
+ :param key: the key to clear rate limits for
154
+
155
+ """
156
+ key = self.prefixed_key(key)
157
+ await self.get_connection().delete(key)
158
+
159
+ async def lua_reset(self) -> int | None:
160
+ return cast(int, await self.lua_clear_keys([self.prefixed_key("*")]))
161
+
162
+ async def get_moving_window(
163
+ self, key: str, limit: int, expiry: int
164
+ ) -> tuple[float, int]:
165
+ """
166
+ returns the starting point and the number of entries in the moving
167
+ window
168
+
169
+ :param key: rate limit key
170
+ :param expiry: expiry of entry
171
+ :return: (previous count, previous TTL, current count, current TTL)
172
+ """
173
+ key = self.prefixed_key(key)
174
+ timestamp = time.time()
175
+ window = await self.lua_moving_window([key], [timestamp - expiry, limit])
176
+ if window:
177
+ return float(window[0]), window[1]
178
+ return timestamp, 0
179
+
180
+ async def get_sliding_window(
181
+ self, previous_key: str, current_key: str, expiry: int
182
+ ) -> tuple[int, float, int, float]:
183
+ if window := await self.lua_sliding_window(
184
+ [self.prefixed_key(previous_key), self.prefixed_key(current_key)], [expiry]
185
+ ):
186
+ return (
187
+ int(window[0] or 0),
188
+ max(0, float(window[1] or 0)) / 1000,
189
+ int(window[2] or 0),
190
+ max(0, float(window[3] or 0)) / 1000,
191
+ )
192
+ return 0, 0.0, 0, 0.0
193
+
194
+ async def acquire_entry(
195
+ self,
196
+ key: str,
197
+ limit: int,
198
+ expiry: int,
199
+ amount: int = 1,
200
+ ) -> bool:
201
+ """
202
+ :param key: rate limit key to acquire an entry in
203
+ :param limit: amount of entries allowed
204
+ :param expiry: expiry of the entry
205
+
206
+ """
207
+ key = self.prefixed_key(key)
208
+ timestamp = time.time()
209
+ acquired = await self.lua_acquire_moving_window(
210
+ [key], [timestamp, limit, expiry, amount]
211
+ )
212
+
213
+ return bool(acquired)
214
+
215
+ async def acquire_sliding_window_entry(
216
+ self,
217
+ previous_key: str,
218
+ current_key: str,
219
+ limit: int,
220
+ expiry: int,
221
+ amount: int = 1,
222
+ ) -> bool:
223
+ previous_key = self.prefixed_key(previous_key)
224
+ current_key = self.prefixed_key(current_key)
225
+ acquired = await self.lua_acquire_sliding_window(
226
+ [previous_key, current_key], [limit, expiry, amount]
227
+ )
228
+ return bool(acquired)
229
+
230
+ async def get_expiry(self, key: str) -> float:
231
+ """
232
+ :param key: the key to get the expiry for
233
+ """
234
+
235
+ key = self.prefixed_key(key)
236
+ return max(await self.get_connection().ttl(key), 0) + time.time()
237
+
238
+ async def check(self) -> bool:
239
+ """
240
+ check if storage is healthy
241
+ """
242
+ try:
243
+ await self.get_connection().ping()
244
+
245
+ return True
246
+ except: # noqa
247
+ return False
248
+
249
+ async def reset(self) -> int | None:
250
+ prefix = self.prefixed_key("*")
251
+ keys = await self.storage.keys(
252
+ prefix, target_nodes=self.dependency.asyncio.cluster.RedisCluster.ALL_NODES
253
+ )
254
+ count = 0
255
+ for key in keys:
256
+ count += await self.storage.delete(key)
257
+ return count
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from .redispy import RedispyBridge
4
+
5
+
6
+ class ValkeyBridge(RedispyBridge):
7
+ @property
8
+ def base_exceptions(self) -> type[Exception] | tuple[type[Exception], ...]:
9
+ return (self.dependency.ValkeyError,)
limits/aio/strategies.py CHANGED
@@ -2,6 +2,8 @@
2
2
  Asynchronous rate limiting strategies
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import time
6
8
  from abc import ABC, abstractmethod
7
9
  from math import floor, inf
@@ -74,7 +76,7 @@ class MovingWindowRateLimiter(RateLimiter):
74
76
  ):
75
77
  raise NotImplementedError(
76
78
  "MovingWindowRateLimiting is not implemented for storage "
77
- "of type %s" % storage.__class__
79
+ f"of type {storage.__class__}"
78
80
  )
79
81
  super().__init__(storage)
80
82
 
@@ -200,7 +202,7 @@ class SlidingWindowCounterRateLimiter(RateLimiter):
200
202
  ):
201
203
  raise NotImplementedError(
202
204
  "SlidingWindowCounterRateLimiting is not implemented for storage "
203
- "of type %s" % storage.__class__
205
+ f"of type {storage.__class__}"
204
206
  )
205
207
  super().__init__(storage)
206
208
 
limits/errors.py CHANGED
@@ -2,6 +2,8 @@
2
2
  errors and exceptions
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
 
6
8
  class ConfigurationError(Exception):
7
9
  """
@@ -3,12 +3,14 @@ Implementations of storage backends to be used with
3
3
  :class:`limits.strategies.RateLimiter` strategies
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
6
8
  import urllib
7
9
 
8
10
  import limits # noqa
9
11
 
10
12
  from ..errors import ConfigurationError
11
- from ..typing import Union, cast
13
+ from ..typing import TypeAlias, cast
12
14
  from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
13
15
  from .etcd import EtcdStorage
14
16
  from .memcached import MemcachedStorage
@@ -19,11 +21,11 @@ from .redis_cluster import RedisClusterStorage
19
21
  from .redis_sentinel import RedisSentinelStorage
20
22
  from .registry import SCHEMES
21
23
 
22
- StorageTypes = Union[Storage, "limits.aio.storage.Storage"]
24
+ StorageTypes: TypeAlias = "Storage | limits.aio.storage.Storage"
23
25
 
24
26
 
25
27
  def storage_from_string(
26
- storage_string: str, **options: Union[float, str, bool]
28
+ storage_string: str, **options: float | str | bool
27
29
  ) -> StorageTypes:
28
30
  """
29
31
  Factory function to get an instance of the storage class based
@@ -59,21 +61,22 @@ def storage_from_string(
59
61
  scheme = urllib.parse.urlparse(storage_string).scheme
60
62
 
61
63
  if scheme not in SCHEMES:
62
- raise ConfigurationError("unknown storage scheme : %s" % storage_string)
64
+ raise ConfigurationError(f"unknown storage scheme : {storage_string}")
65
+
63
66
  return cast(StorageTypes, SCHEMES[scheme](storage_string, **options))
64
67
 
65
68
 
66
69
  __all__ = [
67
- "storage_from_string",
68
- "Storage",
69
- "MovingWindowSupport",
70
- "SlidingWindowCounterSupport",
71
70
  "EtcdStorage",
72
- "MongoDBStorageBase",
71
+ "MemcachedStorage",
73
72
  "MemoryStorage",
74
73
  "MongoDBStorage",
75
- "RedisStorage",
74
+ "MongoDBStorageBase",
75
+ "MovingWindowSupport",
76
76
  "RedisClusterStorage",
77
77
  "RedisSentinelStorage",
78
- "MemcachedStorage",
78
+ "RedisStorage",
79
+ "SlidingWindowCounterSupport",
80
+ "Storage",
81
+ "storage_from_string",
79
82
  ]
limits/storage/base.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import functools
4
- import threading
5
4
  from abc import ABC, abstractmethod
6
5
 
7
6
  from limits import errors
@@ -9,11 +8,8 @@ from limits.storage.registry import StorageRegistry
9
8
  from limits.typing import (
10
9
  Any,
11
10
  Callable,
12
- Optional,
13
11
  P,
14
12
  R,
15
- Type,
16
- Union,
17
13
  cast,
18
14
  )
19
15
  from limits.util import LazyDependency
@@ -40,7 +36,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
40
36
  Base class to extend when implementing a storage backend.
41
37
  """
42
38
 
43
- STORAGE_SCHEME: Optional[list[str]]
39
+ STORAGE_SCHEME: list[str] | None
44
40
  """The storage schemes to register against this implementation"""
45
41
 
46
42
  def __init_subclass__(cls, **kwargs: Any) -> None: # type: ignore[explicit-any]
@@ -57,22 +53,21 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
57
53
 
58
54
  def __init__(
59
55
  self,
60
- uri: Optional[str] = None,
56
+ uri: str | None = None,
61
57
  wrap_exceptions: bool = False,
62
- **options: Union[float, str, bool],
58
+ **options: float | str | bool,
63
59
  ):
64
60
  """
65
61
  :param wrap_exceptions: Whether to wrap storage exceptions in
66
62
  :exc:`limits.errors.StorageError` before raising it.
67
63
  """
68
64
 
69
- self.lock = threading.RLock()
70
65
  super().__init__()
71
66
  self.wrap_exceptions = wrap_exceptions
72
67
 
73
68
  @property
74
69
  @abstractmethod
75
- def base_exceptions(self) -> Union[Type[Exception], tuple[Type[Exception], ...]]:
70
+ def base_exceptions(self) -> type[Exception] | tuple[type[Exception], ...]:
76
71
  raise NotImplementedError
77
72
 
78
73
  @abstractmethod
@@ -112,7 +107,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
112
107
  raise NotImplementedError
113
108
 
114
109
  @abstractmethod
115
- def reset(self) -> Optional[int]:
110
+ def reset(self) -> int | None:
116
111
  """
117
112
  reset storage to clear limits
118
113
  """
limits/storage/etcd.py CHANGED
@@ -1,9 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
2
4
  import urllib.parse
3
5
 
4
6
  from limits.errors import ConcurrentUpdateError
5
7
  from limits.storage.base import Storage
6
- from limits.typing import TYPE_CHECKING, Optional, Union
8
+ from limits.typing import TYPE_CHECKING
7
9
 
8
10
  if TYPE_CHECKING:
9
11
  import etcd3
@@ -42,7 +44,7 @@ class EtcdStorage(Storage):
42
44
  """
43
45
  parsed = urllib.parse.urlparse(uri)
44
46
  self.lib = self.dependencies["etcd3"].module
45
- self.storage: "etcd3.Etcd3Client" = self.lib.client(
47
+ self.storage: etcd3.Etcd3Client = self.lib.client(
46
48
  parsed.hostname, parsed.port, **options
47
49
  )
48
50
  self.max_retries = max_retries
@@ -51,7 +53,7 @@ class EtcdStorage(Storage):
51
53
  @property
52
54
  def base_exceptions(
53
55
  self,
54
- ) -> Union[type[Exception], tuple[type[Exception], ...]]: # pragma: no cover
56
+ ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
55
57
  return self.lib.Etcd3Exception # type: ignore[no-any-return]
56
58
 
57
59
  def prefixed_key(self, key: str) -> bytes:
@@ -127,7 +129,7 @@ class EtcdStorage(Storage):
127
129
  except: # noqa
128
130
  return False
129
131
 
130
- def reset(self) -> Optional[int]:
132
+ def reset(self) -> int | None:
131
133
  return self.storage.delete_prefix(f"{self.PREFIX}/").deleted
132
134
 
133
135
  def clear(self, key: str) -> None: