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.
limits/__init__.py CHANGED
@@ -2,6 +2,8 @@
2
2
  Rate limiting with commonly used storage backends
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  from . import _version, aio, storage, strategies
6
8
  from .limits import (
7
9
  RateLimitItem,
@@ -16,18 +18,18 @@ from .util import WindowStats, parse, parse_many
16
18
 
17
19
  __all__ = [
18
20
  "RateLimitItem",
19
- "RateLimitItemPerYear",
20
- "RateLimitItemPerMonth",
21
21
  "RateLimitItemPerDay",
22
22
  "RateLimitItemPerHour",
23
23
  "RateLimitItemPerMinute",
24
+ "RateLimitItemPerMonth",
24
25
  "RateLimitItemPerSecond",
26
+ "RateLimitItemPerYear",
27
+ "WindowStats",
25
28
  "aio",
26
- "storage",
27
- "strategies",
28
29
  "parse",
29
30
  "parse_many",
30
- "WindowStats",
31
+ "storage",
32
+ "strategies",
31
33
  ]
32
34
 
33
- __version__ = _version.get_versions()["version"] # type: ignore
35
+ __version__ = _version.get_versions()["version"]
limits/_version.py CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- # This file was generated by 'versioneer.py' (0.22) from
2
+ # This file was generated by 'versioneer.py' (0.29) from
3
3
  # revision-control system data, or from the parent directory name of an
4
4
  # unpacked source archive. Distribution tarballs contain a pre-generated copy
5
5
  # of this file.
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-03-07T12:07:00-0800",
11
+ "date": "2025-03-14T16:19:10-0700",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "aff8ca13e30c9d754690a2931498f023d35dc62f",
15
- "version": "4.1"
14
+ "full-revisionid": "0bcebd7b69d035e3df82779a50fb2d1e901b9ef9",
15
+ "version": "4.3"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
limits/aio/__init__.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from . import storage, strategies
2
4
 
3
5
  __all__ = [
@@ -3,6 +3,8 @@ Implementations of storage backends to be used with
3
3
  :class:`limits.aio.strategies.RateLimiter` strategies
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
6
8
  from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
7
9
  from .etcd import EtcdStorage
8
10
  from .memcached import MemcachedStorage
@@ -11,14 +13,14 @@ from .mongodb import MongoDBStorage
11
13
  from .redis import RedisClusterStorage, RedisSentinelStorage, RedisStorage
12
14
 
13
15
  __all__ = [
14
- "Storage",
15
- "MovingWindowSupport",
16
- "SlidingWindowCounterSupport",
17
16
  "EtcdStorage",
18
17
  "MemcachedStorage",
19
18
  "MemoryStorage",
20
19
  "MongoDBStorage",
21
- "RedisStorage",
20
+ "MovingWindowSupport",
22
21
  "RedisClusterStorage",
23
22
  "RedisSentinelStorage",
23
+ "RedisStorage",
24
+ "SlidingWindowCounterSupport",
25
+ "Storage",
24
26
  ]
@@ -11,11 +11,8 @@ from limits.typing import (
11
11
  Any,
12
12
  Awaitable,
13
13
  Callable,
14
- Optional,
15
14
  P,
16
15
  R,
17
- Type,
18
- Union,
19
16
  cast,
20
17
  )
21
18
  from limits.util import LazyDependency
@@ -43,7 +40,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
43
40
  Base class to extend when implementing an async storage backend.
44
41
  """
45
42
 
46
- STORAGE_SCHEME: Optional[list[str]]
43
+ STORAGE_SCHEME: list[str] | None
47
44
  """The storage schemes to register against this implementation"""
48
45
 
49
46
  def __init_subclass__(cls, **kwargs: Any) -> None: # type:ignore[explicit-any]
@@ -61,9 +58,9 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
61
58
 
62
59
  def __init__(
63
60
  self,
64
- uri: Optional[str] = None,
61
+ uri: str | None = None,
65
62
  wrap_exceptions: bool = False,
66
- **options: Union[float, str, bool],
63
+ **options: float | str | bool,
67
64
  ) -> None:
68
65
  """
69
66
  :param wrap_exceptions: Whether to wrap storage exceptions in
@@ -74,7 +71,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
74
71
 
75
72
  @property
76
73
  @abstractmethod
77
- def base_exceptions(self) -> Union[Type[Exception], tuple[Type[Exception], ...]]:
74
+ def base_exceptions(self) -> type[Exception] | tuple[type[Exception], ...]:
78
75
  raise NotImplementedError
79
76
 
80
77
  @abstractmethod
@@ -114,7 +111,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
114
111
  raise NotImplementedError
115
112
 
116
113
  @abstractmethod
117
- async def reset(self) -> Optional[int]:
114
+ async def reset(self) -> int | None:
118
115
  """
119
116
  reset storage to clear limits
120
117
  """
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import time
3
5
  import urllib.parse
4
- from typing import TYPE_CHECKING, Optional, Union
6
+ from typing import TYPE_CHECKING
5
7
 
6
8
  from limits.aio.storage.base import Storage
7
9
  from limits.errors import ConcurrentUpdateError
@@ -44,7 +46,7 @@ class EtcdStorage(Storage):
44
46
  """
45
47
  parsed = urllib.parse.urlparse(uri)
46
48
  self.lib = self.dependencies["aetcd"].module
47
- self.storage: "aetcd.Client" = self.lib.Client(
49
+ self.storage: aetcd.Client = self.lib.Client(
48
50
  host=parsed.hostname, port=parsed.port, **options
49
51
  )
50
52
  self.max_retries = max_retries
@@ -53,7 +55,7 @@ class EtcdStorage(Storage):
53
55
  @property
54
56
  def base_exceptions(
55
57
  self,
56
- ) -> Union[type[Exception], tuple[type[Exception], ...]]: # pragma: no cover
58
+ ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
57
59
  return self.lib.ClientError # type: ignore[no-any-return]
58
60
 
59
61
  def prefixed_key(self, key: str) -> bytes:
@@ -134,7 +136,7 @@ class EtcdStorage(Storage):
134
136
  except: # noqa
135
137
  return False
136
138
 
137
- async def reset(self) -> Optional[int]:
139
+ async def reset(self) -> int | None:
138
140
  return (await self.storage.delete_prefix(f"{self.PREFIX}/".encode())).deleted
139
141
 
140
142
  async def clear(self, key: str) -> None:
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
2
4
  import urllib.parse
3
5
  from collections.abc import Iterable
@@ -7,7 +9,7 @@ from deprecated.sphinx import versionadded
7
9
 
8
10
  from limits.aio.storage.base import SlidingWindowCounterSupport, Storage
9
11
  from limits.storage.base import TimestampedSlidingWindow
10
- from limits.typing import EmcacheClientP, ItemP, Optional, Type, Union
12
+ from limits.typing import EmcacheClientP, ItemP
11
13
 
12
14
 
13
15
  @versionadded(version="2.1")
@@ -27,7 +29,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
27
29
  self,
28
30
  uri: str,
29
31
  wrap_exceptions: bool = False,
30
- **options: Union[float, str, bool],
32
+ **options: float | str | bool,
31
33
  ) -> None:
32
34
  """
33
35
  :param uri: memcached location of the form
@@ -54,7 +56,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
54
56
  @property
55
57
  def base_exceptions(
56
58
  self,
57
- ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
59
+ ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
58
60
  return (
59
61
  self.dependency.ClusterNoAvailableNodes,
60
62
  self.dependency.CommandError,
@@ -204,7 +206,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
204
206
  except: # noqa
205
207
  return False
206
208
 
207
- async def reset(self) -> Optional[int]:
209
+ async def reset(self) -> int | None:
208
210
  raise NotImplementedError
209
211
 
210
212
  async def acquire_sliding_window_entry(
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import time
3
5
  from collections import Counter, defaultdict
@@ -12,14 +14,12 @@ from limits.aio.storage.base import (
12
14
  Storage,
13
15
  )
14
16
  from limits.storage.base import TimestampedSlidingWindow
15
- from limits.typing import Optional, Type, Union
16
17
 
17
18
 
18
- class LockableEntry(asyncio.Lock):
19
+ class Entry:
19
20
  def __init__(self, expiry: int) -> None:
20
21
  self.atime = time.time()
21
22
  self.expiry = self.atime + expiry
22
- super().__init__()
23
23
 
24
24
 
25
25
  @versionadded(version="2.1")
@@ -39,27 +39,36 @@ class MemoryStorage(
39
39
  """
40
40
 
41
41
  def __init__(
42
- self, uri: Optional[str] = None, wrap_exceptions: bool = False, **_: str
42
+ self, uri: str | None = None, wrap_exceptions: bool = False, **_: str
43
43
  ) -> None:
44
44
  self.storage: limits.typing.Counter[str] = Counter()
45
45
  self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
46
46
  self.expirations: dict[str, float] = {}
47
- self.events: dict[str, list[LockableEntry]] = {}
48
- self.timer: Optional[asyncio.Task[None]] = None
47
+ self.events: dict[str, list[Entry]] = {}
48
+ self.timer: asyncio.Task[None] | None = None
49
49
  super().__init__(uri, wrap_exceptions=wrap_exceptions, **_)
50
50
 
51
- @property
52
- def base_exceptions(
53
- self,
54
- ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
55
- return ValueError
51
+ def __getstate__(self) -> dict[str, limits.typing.Any]: # type: ignore[explicit-any]
52
+ state = self.__dict__.copy()
53
+ del state["timer"]
54
+ del state["locks"]
55
+ return state
56
+
57
+ def __setstate__(self, state: dict[str, limits.typing.Any]) -> None: # type: ignore[explicit-any]
58
+ self.__dict__.update(state)
59
+ self.timer = None
60
+ self.locks = defaultdict(asyncio.Lock)
61
+ asyncio.ensure_future(self.__schedule_expiry())
56
62
 
57
63
  async def __expire_events(self) -> None:
58
64
  for key in self.events.keys():
59
- for event in list(self.events[key]):
60
- async with event:
65
+ async with self.locks[key]:
66
+ for event in list(self.events[key]):
61
67
  if event.expiry <= time.time() and event in self.events[key]:
62
68
  self.events[key].remove(event)
69
+ if not self.events.get(key, None):
70
+ self.events.pop(key, None)
71
+ self.locks.pop(key, None)
63
72
 
64
73
  for key in list(self.expirations.keys()):
65
74
  if self.expirations[key] <= time.time():
@@ -71,6 +80,12 @@ class MemoryStorage(
71
80
  if not self.timer or self.timer.done():
72
81
  self.timer = asyncio.create_task(self.__expire_events())
73
82
 
83
+ @property
84
+ def base_exceptions(
85
+ self,
86
+ ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
87
+ return ValueError
88
+
74
89
  async def incr(
75
90
  self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
76
91
  ) -> int:
@@ -138,18 +153,19 @@ class MemoryStorage(
138
153
  if amount > limit:
139
154
  return False
140
155
 
141
- self.events.setdefault(key, [])
142
156
  await self.__schedule_expiry()
143
- timestamp = time.time()
144
- try:
145
- entry: Optional[LockableEntry] = self.events[key][limit - amount]
146
- except IndexError:
147
- entry = None
148
-
149
- if entry and entry.atime >= timestamp - expiry:
150
- return False
151
- else:
152
- self.events[key][:0] = [LockableEntry(expiry) for _ in range(amount)]
157
+ async with self.locks[key]:
158
+ self.events.setdefault(key, [])
159
+ timestamp = time.time()
160
+ try:
161
+ entry: Entry | None = self.events[key][limit - amount]
162
+ except IndexError:
163
+ entry = None
164
+
165
+ if entry and entry.atime >= timestamp - expiry:
166
+ return False
167
+ else:
168
+ self.events[key][:0] = [Entry(expiry) for _ in range(amount)]
153
169
 
154
170
  return True
155
171
 
@@ -170,7 +186,7 @@ class MemoryStorage(
170
186
  timestamp = time.time()
171
187
 
172
188
  return (
173
- len([k for k in self.events[key] if k.atime >= timestamp - expiry])
189
+ len([k for k in self.events.get(key, []) if k.atime >= timestamp - expiry])
174
190
  if self.events.get(key)
175
191
  else 0
176
192
  )
@@ -262,7 +278,7 @@ class MemoryStorage(
262
278
 
263
279
  return True
264
280
 
265
- async def reset(self) -> Optional[int]:
281
+ async def reset(self) -> int | None:
266
282
  num_items = max(len(self.storage), len(self.events))
267
283
  self.storage.clear()
268
284
  self.expirations.clear()
@@ -12,11 +12,8 @@ from limits.aio.storage.base import (
12
12
  Storage,
13
13
  )
14
14
  from limits.typing import (
15
- Optional,
16
15
  ParamSpec,
17
- Type,
18
16
  TypeVar,
19
- Union,
20
17
  cast,
21
18
  )
22
19
  from limits.util import get_dependency
@@ -51,7 +48,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
51
48
  counter_collection_name: str = "counters",
52
49
  window_collection_name: str = "windows",
53
50
  wrap_exceptions: bool = False,
54
- **options: Union[float, str, bool],
51
+ **options: float | str | bool,
55
52
  ) -> None:
56
53
  """
57
54
  :param uri: uri of the form ``async+mongodb://[user:password]@host:port?...``,
@@ -94,7 +91,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
94
91
  @property
95
92
  def base_exceptions(
96
93
  self,
97
- ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
94
+ ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
98
95
  return self.lib_errors.PyMongoError # type: ignore
99
96
 
100
97
  @property
@@ -113,7 +110,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
113
110
  )
114
111
  self.__indices_created = True
115
112
 
116
- async def reset(self) -> Optional[int]:
113
+ async def reset(self) -> int | None:
117
114
  """
118
115
  Delete all rate limit keys in the rate limit collections (counters, windows)
119
116
  """
@@ -294,7 +291,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
294
291
  try:
295
292
  updates: dict[
296
293
  str,
297
- dict[str, Union[datetime.datetime, dict[str, Union[list[float], int]]]],
294
+ dict[str, datetime.datetime | dict[str, list[float] | int]],
298
295
  ] = {
299
296
  "$push": {
300
297
  "entries": {