limits 3.11.0__tar.gz → 3.13.0__tar.gz

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.
Files changed (82) hide show
  1. {limits-3.11.0 → limits-3.13.0}/HISTORY.rst +28 -0
  2. {limits-3.11.0 → limits-3.13.0}/PKG-INFO +1 -1
  3. {limits-3.11.0 → limits-3.13.0}/doc/source/index.rst +1 -0
  4. {limits-3.11.0 → limits-3.13.0}/limits/_version.py +3 -3
  5. {limits-3.11.0 → limits-3.13.0}/limits/aio/storage/base.py +1 -1
  6. {limits-3.11.0 → limits-3.13.0}/limits/aio/strategies.py +10 -5
  7. {limits-3.11.0 → limits-3.13.0}/limits/storage/__init__.py +2 -1
  8. {limits-3.11.0 → limits-3.13.0}/limits/storage/mongodb.py +53 -16
  9. {limits-3.11.0 → limits-3.13.0}/limits/strategies.py +8 -5
  10. {limits-3.11.0 → limits-3.13.0}/limits/typing.py +10 -1
  11. {limits-3.11.0 → limits-3.13.0}/limits.egg-info/PKG-INFO +1 -1
  12. {limits-3.11.0 → limits-3.13.0}/requirements/docs.txt +1 -1
  13. {limits-3.11.0 → limits-3.13.0}/requirements/test.txt +1 -1
  14. {limits-3.11.0 → limits-3.13.0}/tests/test_strategy.py +5 -3
  15. {limits-3.11.0 → limits-3.13.0}/CLASSIFIERS +0 -0
  16. {limits-3.11.0 → limits-3.13.0}/CONTRIBUTIONS.rst +0 -0
  17. {limits-3.11.0 → limits-3.13.0}/LICENSE.txt +0 -0
  18. {limits-3.11.0 → limits-3.13.0}/MANIFEST.in +0 -0
  19. {limits-3.11.0 → limits-3.13.0}/README.rst +0 -0
  20. {limits-3.11.0 → limits-3.13.0}/doc/Makefile +0 -0
  21. {limits-3.11.0 → limits-3.13.0}/doc/source/_static/custom.css +0 -0
  22. {limits-3.11.0 → limits-3.13.0}/doc/source/api.rst +0 -0
  23. {limits-3.11.0 → limits-3.13.0}/doc/source/async.rst +0 -0
  24. {limits-3.11.0 → limits-3.13.0}/doc/source/changelog.rst +0 -0
  25. {limits-3.11.0 → limits-3.13.0}/doc/source/conf.py +0 -0
  26. {limits-3.11.0 → limits-3.13.0}/doc/source/custom-storage.rst +0 -0
  27. {limits-3.11.0 → limits-3.13.0}/doc/source/installation.rst +0 -0
  28. {limits-3.11.0 → limits-3.13.0}/doc/source/quickstart.rst +0 -0
  29. {limits-3.11.0 → limits-3.13.0}/doc/source/storage.rst +0 -0
  30. {limits-3.11.0 → limits-3.13.0}/doc/source/strategies.rst +0 -0
  31. {limits-3.11.0 → limits-3.13.0}/doc/source/theme_config.py +0 -0
  32. {limits-3.11.0 → limits-3.13.0}/limits/__init__.py +0 -0
  33. {limits-3.11.0 → limits-3.13.0}/limits/aio/__init__.py +0 -0
  34. {limits-3.11.0 → limits-3.13.0}/limits/aio/storage/__init__.py +0 -0
  35. {limits-3.11.0 → limits-3.13.0}/limits/aio/storage/etcd.py +0 -0
  36. {limits-3.11.0 → limits-3.13.0}/limits/aio/storage/memcached.py +0 -0
  37. {limits-3.11.0 → limits-3.13.0}/limits/aio/storage/memory.py +0 -0
  38. {limits-3.11.0 → limits-3.13.0}/limits/aio/storage/mongodb.py +0 -0
  39. {limits-3.11.0 → limits-3.13.0}/limits/aio/storage/redis.py +0 -0
  40. {limits-3.11.0 → limits-3.13.0}/limits/errors.py +0 -0
  41. {limits-3.11.0 → limits-3.13.0}/limits/limits.py +0 -0
  42. {limits-3.11.0 → limits-3.13.0}/limits/py.typed +0 -0
  43. {limits-3.11.0 → limits-3.13.0}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
  44. {limits-3.11.0 → limits-3.13.0}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
  45. {limits-3.11.0 → limits-3.13.0}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
  46. {limits-3.11.0 → limits-3.13.0}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
  47. {limits-3.11.0 → limits-3.13.0}/limits/storage/base.py +0 -0
  48. {limits-3.11.0 → limits-3.13.0}/limits/storage/etcd.py +0 -0
  49. {limits-3.11.0 → limits-3.13.0}/limits/storage/memcached.py +0 -0
  50. {limits-3.11.0 → limits-3.13.0}/limits/storage/memory.py +0 -0
  51. {limits-3.11.0 → limits-3.13.0}/limits/storage/redis.py +0 -0
  52. {limits-3.11.0 → limits-3.13.0}/limits/storage/redis_cluster.py +0 -0
  53. {limits-3.11.0 → limits-3.13.0}/limits/storage/redis_sentinel.py +0 -0
  54. {limits-3.11.0 → limits-3.13.0}/limits/storage/registry.py +0 -0
  55. {limits-3.11.0 → limits-3.13.0}/limits/util.py +0 -0
  56. {limits-3.11.0 → limits-3.13.0}/limits/version.py +0 -0
  57. {limits-3.11.0 → limits-3.13.0}/limits.egg-info/SOURCES.txt +0 -0
  58. {limits-3.11.0 → limits-3.13.0}/limits.egg-info/dependency_links.txt +0 -0
  59. {limits-3.11.0 → limits-3.13.0}/limits.egg-info/not-zip-safe +0 -0
  60. {limits-3.11.0 → limits-3.13.0}/limits.egg-info/requires.txt +0 -0
  61. {limits-3.11.0 → limits-3.13.0}/limits.egg-info/top_level.txt +0 -0
  62. {limits-3.11.0 → limits-3.13.0}/pyproject.toml +0 -0
  63. {limits-3.11.0 → limits-3.13.0}/requirements/ci.txt +0 -0
  64. {limits-3.11.0 → limits-3.13.0}/requirements/dev.txt +0 -0
  65. {limits-3.11.0 → limits-3.13.0}/requirements/main.txt +0 -0
  66. {limits-3.11.0 → limits-3.13.0}/requirements/storage/async-etcd.txt +0 -0
  67. {limits-3.11.0 → limits-3.13.0}/requirements/storage/async-memcached.txt +0 -0
  68. {limits-3.11.0 → limits-3.13.0}/requirements/storage/async-mongodb.txt +0 -0
  69. {limits-3.11.0 → limits-3.13.0}/requirements/storage/async-redis.txt +0 -0
  70. {limits-3.11.0 → limits-3.13.0}/requirements/storage/etcd.txt +0 -0
  71. {limits-3.11.0 → limits-3.13.0}/requirements/storage/memcached.txt +0 -0
  72. {limits-3.11.0 → limits-3.13.0}/requirements/storage/mongodb.txt +0 -0
  73. {limits-3.11.0 → limits-3.13.0}/requirements/storage/redis.txt +0 -0
  74. {limits-3.11.0 → limits-3.13.0}/requirements/storage/rediscluster.txt +0 -0
  75. {limits-3.11.0 → limits-3.13.0}/setup.cfg +0 -0
  76. {limits-3.11.0 → limits-3.13.0}/setup.py +0 -0
  77. {limits-3.11.0 → limits-3.13.0}/tests/test_limit_granularities.py +0 -0
  78. {limits-3.11.0 → limits-3.13.0}/tests/test_limits.py +0 -0
  79. {limits-3.11.0 → limits-3.13.0}/tests/test_ratelimit_parser.py +0 -0
  80. {limits-3.11.0 → limits-3.13.0}/tests/test_storage.py +0 -0
  81. {limits-3.11.0 → limits-3.13.0}/tests/test_utils.py +0 -0
  82. {limits-3.11.0 → limits-3.13.0}/versioneer.py +0 -0
@@ -3,6 +3,32 @@
3
3
  Changelog
4
4
  =========
5
5
 
6
+ v3.13.0
7
+ -------
8
+ Release Date: 2024-06-22
9
+
10
+ * Feature
11
+
12
+ * Add ``cost`` parameter to ``test`` methods in strategies.
13
+
14
+ v3.12.0
15
+ -------
16
+ Release Date: 2024-05-12
17
+
18
+ * Enhancements
19
+
20
+ * Lazily initialize pymongo client
21
+
22
+ * Documentation
23
+
24
+ * Add django-ratelimiter in docs
25
+
26
+ * Chores
27
+
28
+ * Update development dependencies
29
+ * Update github actions to latest
30
+
31
+
6
32
  v3.11.0
7
33
  -------
8
34
  Release Date: 2024-04-20
@@ -687,6 +713,8 @@ Release Date: 2015-01-08
687
713
 
688
714
 
689
715
 
716
+
717
+
690
718
 
691
719
 
692
720
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: limits
3
- Version: 3.11.0
3
+ Version: 3.13.0
4
4
  Summary: Rate limiting utilities
5
5
  Home-page: https://limits.readthedocs.org
6
6
  Author: Ali-Akber Saifee
@@ -81,6 +81,7 @@ Projects using *limits*
81
81
  - `djlimiter <http://djlimiter.readthedocs.org>`_: Rate limiting middleware for Django applications.
82
82
  - `sanic-limiter <https://github.com/bohea/sanic-limiter>`_: Rate limiting middleware for Sanic applications.
83
83
  - `Falcon-Limiter <https://falcon-limiter.readthedocs.org>`_ : Rate limiting extension for Falcon applications.
84
+ - `django-ratelimiter <https://andriykohut.github.io/django-ratelimiter/>`_: Rate limiting decorator and middleware for Django applications.
84
85
 
85
86
  References
86
87
  ==========
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-04-20T13:09:37-0700",
11
+ "date": "2024-06-22T18:39:54-0700",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "0a1a9c70cbffe4c51a5f34500fe4edfe342de638",
15
- "version": "3.11.0"
14
+ "full-revisionid": "7b87c4d37659ae5fe0a8bf7216bfff789facd5f3",
15
+ "version": "3.13.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -27,7 +27,7 @@ def _wrap_errors(
27
27
  fn: Callable[P, Awaitable[R]],
28
28
  ) -> Callable[P, Awaitable[R]]:
29
29
  @functools.wraps(fn)
30
- async def inner(*args: P.args, **kwargs: P.kwargs) -> R:
30
+ async def inner(*args: P.args, **kwargs: P.kwargs) -> R: # type: ignore[misc]
31
31
  try:
32
32
  return await fn(*args, **kwargs)
33
33
  except storage.base_exceptions as exc:
@@ -29,13 +29,14 @@ class RateLimiter(ABC):
29
29
  raise NotImplementedError
30
30
 
31
31
  @abstractmethod
32
- async def test(self, item: RateLimitItem, *identifiers: str) -> bool:
32
+ async def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
33
33
  """
34
34
  Check if the rate limit can be consumed
35
35
 
36
36
  :param item: the rate limit item
37
37
  :param identifiers: variable list of strings to uniquely identify the
38
38
  limit
39
+ :param cost: The expected cost to be consumed, default 1
39
40
  """
40
41
  raise NotImplementedError
41
42
 
@@ -86,13 +87,14 @@ class MovingWindowRateLimiter(RateLimiter):
86
87
  item.key_for(*identifiers), item.amount, item.get_expiry(), amount=cost
87
88
  )
88
89
 
89
- async def test(self, item: RateLimitItem, *identifiers: str) -> bool:
90
+ async def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
90
91
  """
91
92
  Check if the rate limit can be consumed
92
93
 
93
94
  :param item: the rate limit item
94
95
  :param identifiers: variable list of strings to uniquely identify the
95
96
  limit
97
+ :param cost: The expected cost to be consumed, default 1
96
98
  """
97
99
  res = await cast(MovingWindowSupport, self.storage).get_moving_window(
98
100
  item.key_for(*identifiers),
@@ -101,7 +103,7 @@ class MovingWindowRateLimiter(RateLimiter):
101
103
  )
102
104
  amount = res[1]
103
105
 
104
- return amount < item.amount
106
+ return amount <= item.amount - cost
105
107
 
106
108
  async def get_window_stats(
107
109
  self, item: RateLimitItem, *identifiers: str
@@ -147,16 +149,19 @@ class FixedWindowRateLimiter(RateLimiter):
147
149
  <= item.amount
148
150
  )
149
151
 
150
- async def test(self, item: RateLimitItem, *identifiers: str) -> bool:
152
+ async def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
151
153
  """
152
154
  Check if the rate limit can be consumed
153
155
 
154
156
  :param item: the rate limit item
155
157
  :param identifiers: variable list of strings to uniquely identify the
156
158
  limit
159
+ :param cost: The expected cost to be consumed, default 1
157
160
  """
158
161
 
159
- return await self.storage.get(item.key_for(*identifiers)) < item.amount
162
+ return (
163
+ await self.storage.get(item.key_for(*identifiers)) < item.amount - cost + 1
164
+ )
160
165
 
161
166
  async def get_window_stats(
162
167
  self, item: RateLimitItem, *identifiers: str
@@ -13,7 +13,7 @@ from .base import MovingWindowSupport, Storage
13
13
  from .etcd import EtcdStorage
14
14
  from .memcached import MemcachedStorage
15
15
  from .memory import MemoryStorage
16
- from .mongodb import MongoDBStorage
16
+ from .mongodb import MongoDBStorage, MongoDBStorageBase
17
17
  from .redis import RedisStorage
18
18
  from .redis_cluster import RedisClusterStorage
19
19
  from .redis_sentinel import RedisSentinelStorage
@@ -68,6 +68,7 @@ __all__ = [
68
68
  "Storage",
69
69
  "MovingWindowSupport",
70
70
  "EtcdStorage",
71
+ "MongoDBStorageBase",
71
72
  "MemoryStorage",
72
73
  "MongoDBStorage",
73
74
  "RedisStorage",
@@ -3,29 +3,33 @@ from __future__ import annotations
3
3
  import calendar
4
4
  import datetime
5
5
  import time
6
- from typing import TYPE_CHECKING, Any
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, cast
7
8
 
8
9
  from deprecated.sphinx import versionadded
9
10
 
10
- from limits.typing import Dict, Optional, Tuple, Type, Union
11
+ from limits.typing import (
12
+ Dict,
13
+ MongoClient,
14
+ MongoCollection,
15
+ MongoDatabase,
16
+ Optional,
17
+ Tuple,
18
+ Type,
19
+ Union,
20
+ )
11
21
 
12
22
  from ..util import get_dependency
13
23
  from .base import MovingWindowSupport, Storage
14
24
 
15
- if TYPE_CHECKING:
16
- import pymongo
17
25
 
18
-
19
- @versionadded(version="2.1")
20
- class MongoDBStorage(Storage, MovingWindowSupport):
26
+ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
21
27
  """
22
28
  Rate limit storage with MongoDB as backend.
23
29
 
24
30
  Depends on :pypi:`pymongo`.
25
31
  """
26
32
 
27
- STORAGE_SCHEME = ["mongodb", "mongodb+srv"]
28
-
29
33
  DEPENDENCIES = ["pymongo"]
30
34
 
31
35
  def __init__(
@@ -48,16 +52,39 @@ class MongoDBStorage(Storage, MovingWindowSupport):
48
52
  """
49
53
 
50
54
  super().__init__(uri, wrap_exceptions=wrap_exceptions, **options)
51
-
55
+ self._database_name = database_name
52
56
  self.lib = self.dependencies["pymongo"].module
53
57
  self.lib_errors, _ = get_dependency("pymongo.errors")
58
+ self._storage_uri = uri
59
+ self._storage_options = options
60
+ self._storage: Optional[MongoClient] = None
54
61
 
55
- self.storage: "pymongo.MongoClient" = self.lib.MongoClient( # type: ignore[type-arg]
56
- uri, **options
57
- )
58
- self.counters = self.storage.get_database(database_name).counters
59
- self.windows = self.storage.get_database(database_name).windows
60
- self.__initialize_database()
62
+ @property
63
+ def storage(self) -> MongoClient:
64
+ if self._storage is None:
65
+ self._storage = self._init_mongo_client(
66
+ self._storage_uri, **self._storage_options
67
+ )
68
+ self.__initialize_database()
69
+ return self._storage
70
+
71
+ @property
72
+ def _database(self) -> MongoDatabase:
73
+ return self.storage[self._database_name]
74
+
75
+ @property
76
+ def counters(self) -> MongoCollection:
77
+ return self._database["counters"]
78
+
79
+ @property
80
+ def windows(self) -> MongoCollection:
81
+ return self._database["windows"]
82
+
83
+ @abstractmethod
84
+ def _init_mongo_client(
85
+ self, uri: Optional[str], **options: Union[int, str, bool]
86
+ ) -> MongoClient:
87
+ raise NotImplementedError()
61
88
 
62
89
  @property
63
90
  def base_exceptions(
@@ -248,3 +275,13 @@ class MongoDBStorage(Storage, MovingWindowSupport):
248
275
  return True
249
276
  except self.lib.errors.DuplicateKeyError:
250
277
  return False
278
+
279
+
280
+ @versionadded(version="2.1")
281
+ class MongoDBStorage(MongoDBStorageBase):
282
+ STORAGE_SCHEME = ["mongodb", "mongodb+srv"]
283
+
284
+ def _init_mongo_client(
285
+ self, uri: Optional[str], **options: Union[int, str, bool]
286
+ ) -> MongoClient:
287
+ return cast(MongoClient, self.lib.MongoClient(uri, **options))
@@ -28,13 +28,14 @@ class RateLimiter(metaclass=ABCMeta):
28
28
  raise NotImplementedError
29
29
 
30
30
  @abstractmethod
31
- def test(self, item: RateLimitItem, *identifiers: str) -> bool:
31
+ def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
32
32
  """
33
33
  Check the rate limit without consuming from it.
34
34
 
35
35
  :param item: The rate limit item
36
36
  :param identifiers: variable list of strings to uniquely identify this
37
37
  instance of the limit
38
+ :param cost: The expected cost to be consumed, default 1
38
39
  """
39
40
  raise NotImplementedError
40
41
 
@@ -84,13 +85,14 @@ class MovingWindowRateLimiter(RateLimiter):
84
85
  item.key_for(*identifiers), item.amount, item.get_expiry(), amount=cost
85
86
  )
86
87
 
87
- def test(self, item: RateLimitItem, *identifiers: str) -> bool:
88
+ def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
88
89
  """
89
90
  Check if the rate limit can be consumed
90
91
 
91
92
  :param item: The rate limit item
92
93
  :param identifiers: variable list of strings to uniquely identify this
93
94
  instance of the limit
95
+ :param cost: The expected cost to be consumed, default 1
94
96
  """
95
97
 
96
98
  return (
@@ -99,7 +101,7 @@ class MovingWindowRateLimiter(RateLimiter):
99
101
  item.amount,
100
102
  item.get_expiry(),
101
103
  )[1]
102
- < item.amount
104
+ <= item.amount - cost
103
105
  )
104
106
 
105
107
  def get_window_stats(self, item: RateLimitItem, *identifiers: str) -> WindowStats:
@@ -144,16 +146,17 @@ class FixedWindowRateLimiter(RateLimiter):
144
146
  <= item.amount
145
147
  )
146
148
 
147
- def test(self, item: RateLimitItem, *identifiers: str) -> bool:
149
+ def test(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
148
150
  """
149
151
  Check if the rate limit can be consumed
150
152
 
151
153
  :param item: The rate limit item
152
154
  :param identifiers: variable list of strings to uniquely identify this
153
155
  instance of the limit
156
+ :param cost: The expected cost to be consumed, default 1
154
157
  """
155
158
 
156
- return self.storage.get(item.key_for(*identifiers)) < item.amount
159
+ return self.storage.get(item.key_for(*identifiers)) < item.amount - cost + 1
157
160
 
158
161
  def get_window_stats(self, item: RateLimitItem, *identifiers: str) -> WindowStats:
159
162
  """
@@ -1,5 +1,6 @@
1
1
  from typing import (
2
2
  TYPE_CHECKING,
3
+ Any,
3
4
  Awaitable,
4
5
  Callable,
5
6
  Dict,
@@ -12,7 +13,7 @@ from typing import (
12
13
  Union,
13
14
  )
14
15
 
15
- from typing_extensions import ClassVar, Counter, ParamSpec, Protocol
16
+ from typing_extensions import ClassVar, Counter, ParamSpec, Protocol, TypeAlias
16
17
 
17
18
  Serializable = Union[int, str, float]
18
19
 
@@ -24,6 +25,7 @@ P = ParamSpec("P")
24
25
  if TYPE_CHECKING:
25
26
  import coredis
26
27
  import coredis.commands.script
28
+ import pymongo
27
29
  import redis
28
30
 
29
31
 
@@ -107,6 +109,10 @@ class ScriptP(Protocol[R_co]):
107
109
  def __call__(self, keys: List[Serializable], args: List[Serializable]) -> R_co: ...
108
110
 
109
111
 
112
+ MongoClient: TypeAlias = "pymongo.MongoClient[Dict[str, Any]]" # type:ignore[misc]
113
+ MongoDatabase: TypeAlias = "pymongo.database.Database[Dict[str, Any]]" # type:ignore[misc]
114
+ MongoCollection: TypeAlias = "pymongo.collection.Collection[Dict[str, Any]]" # type:ignore[misc]
115
+
110
116
  __all__ = [
111
117
  "AsyncRedisClient",
112
118
  "Awaitable",
@@ -118,6 +124,9 @@ __all__ = [
118
124
  "ItemP",
119
125
  "List",
120
126
  "MemcachedClientP",
127
+ "MongoClient",
128
+ "MongoCollection",
129
+ "MongoDatabase",
121
130
  "NamedTuple",
122
131
  "Optional",
123
132
  "P",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: limits
3
- Version: 3.11.0
3
+ Version: 3.13.0
4
4
  Summary: Rate limiting utilities
5
5
  Home-page: https://limits.readthedocs.org
6
6
  Author: Ali-Akber Saifee
@@ -1,5 +1,5 @@
1
1
  -r main.txt
2
- furo==2024.1.29
2
+ furo==2024.5.6
3
3
  Sphinx>=4,<8
4
4
  sphinx-copybutton==0.5.2
5
5
  sphinx-autobuild==2021.3.14
@@ -18,7 +18,7 @@ hiro>0.1.6
18
18
  flaky
19
19
  lovely-pytest-docker
20
20
  pytest<8
21
- pytest-asyncio==0.21.1
21
+ pytest-asyncio==0.21.2
22
22
  pytest-benchmark[histogram]
23
23
  pytest-cov
24
24
  pytest-lazy-fixture
@@ -48,6 +48,7 @@ class TestWindow:
48
48
  assert not limiter.hit(limit, "k1", cost=11)
49
49
  assert limiter.hit(limit, "k2", cost=5)
50
50
  assert limiter.get_window_stats(limit, "k2").remaining == 5
51
+ assert not limiter.test(limit, "k2", cost=6)
51
52
  assert not limiter.hit(limit, "k2", cost=6)
52
53
 
53
54
  @all_storage
@@ -130,9 +131,10 @@ class TestWindow:
130
131
  limiter.hit(limit, "k2", cost=5)
131
132
  # 5 hits in the last 100ms
132
133
  with window(2, delay=1.8):
133
- assert all(limiter.hit(limit, "k2") for i in range(5))
134
- # 11th fails
135
- assert not limiter.hit(limit, "k2")
134
+ assert all(limiter.hit(limit, "k2") for i in range(4))
135
+ assert not limiter.test(limit, "k2", cost=2)
136
+ assert not limiter.hit(limit, "k2", cost=2)
137
+ assert limiter.hit(limit, "k2")
136
138
 
137
139
  # 5 more succeed since there were only 5 in the last 2 seconds
138
140
  assert all([limiter.hit(limit, "k2") for i in range(5)])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes