limits 4.0.1__tar.gz → 4.1__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 (92) hide show
  1. {limits-4.0.1 → limits-4.1}/CLASSIFIERS +1 -1
  2. {limits-4.0.1 → limits-4.1}/CONTRIBUTIONS.rst +1 -0
  3. {limits-4.0.1 → limits-4.1}/HISTORY.rst +17 -0
  4. limits-4.1/PKG-INFO +279 -0
  5. limits-4.1/README.rst +210 -0
  6. {limits-4.0.1 → limits-4.1}/doc/source/api.rst +6 -2
  7. {limits-4.0.1 → limits-4.1}/doc/source/conf.py +1 -0
  8. limits-4.1/doc/source/custom-storage.rst +119 -0
  9. {limits-4.0.1 → limits-4.1}/doc/source/index.rst +7 -5
  10. {limits-4.0.1 → limits-4.1}/doc/source/quickstart.rst +48 -22
  11. {limits-4.0.1 → limits-4.1}/doc/source/storage.rst +18 -18
  12. limits-4.1/doc/source/strategies.rst +145 -0
  13. {limits-4.0.1 → limits-4.1}/limits/_version.py +3 -3
  14. {limits-4.0.1 → limits-4.1}/limits/aio/storage/__init__.py +2 -1
  15. {limits-4.0.1 → limits-4.1}/limits/aio/storage/base.py +70 -24
  16. {limits-4.0.1 → limits-4.1}/limits/aio/storage/etcd.py +6 -2
  17. limits-4.1/limits/aio/storage/memcached.py +276 -0
  18. {limits-4.0.1 → limits-4.1}/limits/aio/storage/memory.py +98 -13
  19. {limits-4.0.1 → limits-4.1}/limits/aio/storage/mongodb.py +217 -9
  20. {limits-4.0.1 → limits-4.1}/limits/aio/storage/redis.py +100 -15
  21. {limits-4.0.1 → limits-4.1}/limits/aio/strategies.py +122 -1
  22. {limits-4.0.1 → limits-4.1}/limits/limits.py +10 -11
  23. limits-4.1/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +45 -0
  24. limits-4.1/limits/resources/redis/lua_scripts/sliding_window.lua +17 -0
  25. {limits-4.0.1 → limits-4.1}/limits/storage/__init__.py +4 -3
  26. {limits-4.0.1 → limits-4.1}/limits/storage/base.py +92 -24
  27. {limits-4.0.1 → limits-4.1}/limits/storage/etcd.py +6 -2
  28. limits-4.1/limits/storage/memcached.py +321 -0
  29. {limits-4.0.1 → limits-4.1}/limits/storage/memory.py +97 -12
  30. {limits-4.0.1 → limits-4.1}/limits/storage/mongodb.py +204 -11
  31. {limits-4.0.1 → limits-4.1}/limits/storage/redis.py +159 -138
  32. {limits-4.0.1 → limits-4.1}/limits/storage/redis_cluster.py +3 -3
  33. {limits-4.0.1 → limits-4.1}/limits/storage/redis_sentinel.py +12 -35
  34. {limits-4.0.1 → limits-4.1}/limits/storage/registry.py +3 -3
  35. {limits-4.0.1 → limits-4.1}/limits/strategies.py +119 -5
  36. {limits-4.0.1 → limits-4.1}/limits/typing.py +43 -15
  37. {limits-4.0.1 → limits-4.1}/limits/util.py +27 -18
  38. limits-4.1/limits.egg-info/PKG-INFO +279 -0
  39. {limits-4.0.1 → limits-4.1}/limits.egg-info/SOURCES.txt +2 -0
  40. {limits-4.0.1 → limits-4.1}/requirements/dev.txt +0 -1
  41. {limits-4.0.1 → limits-4.1}/tests/test_storage.py +88 -8
  42. limits-4.1/tests/test_strategy.py +372 -0
  43. {limits-4.0.1 → limits-4.1}/tests/test_utils.py +2 -2
  44. limits-4.0.1/PKG-INFO +0 -203
  45. limits-4.0.1/README.rst +0 -134
  46. limits-4.0.1/doc/source/custom-storage.rst +0 -91
  47. limits-4.0.1/doc/source/strategies.rst +0 -44
  48. limits-4.0.1/limits/aio/storage/memcached.py +0 -152
  49. limits-4.0.1/limits/storage/memcached.py +0 -214
  50. limits-4.0.1/limits.egg-info/PKG-INFO +0 -203
  51. limits-4.0.1/tests/test_strategy.py +0 -203
  52. {limits-4.0.1 → limits-4.1}/LICENSE.txt +0 -0
  53. {limits-4.0.1 → limits-4.1}/MANIFEST.in +0 -0
  54. {limits-4.0.1 → limits-4.1}/doc/Makefile +0 -0
  55. {limits-4.0.1 → limits-4.1}/doc/source/_static/custom.css +0 -0
  56. {limits-4.0.1 → limits-4.1}/doc/source/async.rst +0 -0
  57. {limits-4.0.1 → limits-4.1}/doc/source/changelog.rst +0 -0
  58. {limits-4.0.1 → limits-4.1}/doc/source/installation.rst +0 -0
  59. {limits-4.0.1 → limits-4.1}/doc/source/theme_config.py +0 -0
  60. {limits-4.0.1 → limits-4.1}/limits/__init__.py +0 -0
  61. {limits-4.0.1 → limits-4.1}/limits/aio/__init__.py +0 -0
  62. {limits-4.0.1 → limits-4.1}/limits/errors.py +0 -0
  63. {limits-4.0.1 → limits-4.1}/limits/py.typed +0 -0
  64. {limits-4.0.1 → limits-4.1}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
  65. {limits-4.0.1 → limits-4.1}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
  66. {limits-4.0.1 → limits-4.1}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
  67. {limits-4.0.1 → limits-4.1}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
  68. {limits-4.0.1 → limits-4.1}/limits/version.py +0 -0
  69. {limits-4.0.1 → limits-4.1}/limits.egg-info/dependency_links.txt +0 -0
  70. {limits-4.0.1 → limits-4.1}/limits.egg-info/not-zip-safe +0 -0
  71. {limits-4.0.1 → limits-4.1}/limits.egg-info/requires.txt +0 -0
  72. {limits-4.0.1 → limits-4.1}/limits.egg-info/top_level.txt +0 -0
  73. {limits-4.0.1 → limits-4.1}/pyproject.toml +0 -0
  74. {limits-4.0.1 → limits-4.1}/requirements/ci.txt +0 -0
  75. {limits-4.0.1 → limits-4.1}/requirements/docs.txt +0 -0
  76. {limits-4.0.1 → limits-4.1}/requirements/main.txt +0 -0
  77. {limits-4.0.1 → limits-4.1}/requirements/storage/async-etcd.txt +0 -0
  78. {limits-4.0.1 → limits-4.1}/requirements/storage/async-memcached.txt +0 -0
  79. {limits-4.0.1 → limits-4.1}/requirements/storage/async-mongodb.txt +0 -0
  80. {limits-4.0.1 → limits-4.1}/requirements/storage/async-redis.txt +0 -0
  81. {limits-4.0.1 → limits-4.1}/requirements/storage/etcd.txt +0 -0
  82. {limits-4.0.1 → limits-4.1}/requirements/storage/memcached.txt +0 -0
  83. {limits-4.0.1 → limits-4.1}/requirements/storage/mongodb.txt +0 -0
  84. {limits-4.0.1 → limits-4.1}/requirements/storage/redis.txt +0 -0
  85. {limits-4.0.1 → limits-4.1}/requirements/storage/rediscluster.txt +0 -0
  86. {limits-4.0.1 → limits-4.1}/requirements/test.txt +0 -0
  87. {limits-4.0.1 → limits-4.1}/setup.cfg +0 -0
  88. {limits-4.0.1 → limits-4.1}/setup.py +0 -0
  89. {limits-4.0.1 → limits-4.1}/tests/test_limit_granularities.py +0 -0
  90. {limits-4.0.1 → limits-4.1}/tests/test_limits.py +0 -0
  91. {limits-4.0.1 → limits-4.1}/tests/test_ratelimit_parser.py +0 -0
  92. {limits-4.0.1 → limits-4.1}/versioneer.py +0 -0
@@ -5,9 +5,9 @@ Operating System :: MacOS
5
5
  Operating System :: POSIX :: Linux
6
6
  Operating System :: OS Independent
7
7
  Topic :: Software Development :: Libraries :: Python Modules
8
- Programming Language :: Python :: 3.8
9
8
  Programming Language :: Python :: 3.9
10
9
  Programming Language :: Python :: 3.10
11
10
  Programming Language :: Python :: 3.11
12
11
  Programming Language :: Python :: 3.12
12
+ Programming Language :: Python :: 3.13
13
13
  Programming Language :: Python :: Implementation :: PyPy
@@ -5,3 +5,4 @@ Contributors
5
5
  - `Zehua Liu <https://github.com/zehua>`_
6
6
  - `David Czarnecki <https://github.com/czarneckid>`_
7
7
  - `Laurent Savaete <https://github.com/laurentS>`_
8
+ - `Antoine Merino <https://github.com/merinorus>`_
@@ -3,6 +3,22 @@
3
3
  Changelog
4
4
  =========
5
5
 
6
+ v4.1
7
+ ----
8
+ Release Date: 2025-03-07
9
+
10
+ * Feature
11
+
12
+ * Add new Sliding Window Counter strategy
13
+
14
+ * Deprecation
15
+
16
+ * Deprecate the Fixed window with elastic expiry strategy
17
+
18
+ * Documentation
19
+
20
+ * Re-write strategy documentation with concrete examples
21
+
6
22
  v4.0.1
7
23
  ------
8
24
  Release Date: 2025-01-16
@@ -765,5 +781,6 @@ Release Date: 2015-01-08
765
781
 
766
782
 
767
783
 
784
+
768
785
 
769
786
 
limits-4.1/PKG-INFO ADDED
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.2
2
+ Name: limits
3
+ Version: 4.1
4
+ Summary: Rate limiting utilities
5
+ Home-page: https://limits.readthedocs.org
6
+ Author: Ali-Akber Saifee
7
+ Author-email: ali@indydevs.org
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/alisaifee/limits
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
23
+ Requires-Python: >=3.9
24
+ License-File: LICENSE.txt
25
+ Requires-Dist: deprecated>=1.2
26
+ Requires-Dist: packaging<25,>=21
27
+ Requires-Dist: typing_extensions
28
+ Provides-Extra: redis
29
+ Requires-Dist: redis!=4.5.2,!=4.5.3,<6.0.0,>3; extra == "redis"
30
+ Provides-Extra: rediscluster
31
+ Requires-Dist: redis!=4.5.2,!=4.5.3,>=4.2.0; extra == "rediscluster"
32
+ Provides-Extra: memcached
33
+ Requires-Dist: pymemcache<5.0.0,>3; extra == "memcached"
34
+ Provides-Extra: mongodb
35
+ Requires-Dist: pymongo<5,>4.1; extra == "mongodb"
36
+ Provides-Extra: etcd
37
+ Requires-Dist: etcd3; extra == "etcd"
38
+ Provides-Extra: async-redis
39
+ Requires-Dist: coredis<5,>=3.4.0; extra == "async-redis"
40
+ Provides-Extra: async-memcached
41
+ Requires-Dist: emcache>=0.6.1; python_version < "3.11" and extra == "async-memcached"
42
+ Requires-Dist: emcache>=1; (python_version >= "3.11" and python_version < "3.13.0") and extra == "async-memcached"
43
+ Provides-Extra: async-mongodb
44
+ Requires-Dist: motor<4,>=3; extra == "async-mongodb"
45
+ Provides-Extra: async-etcd
46
+ Requires-Dist: aetcd; extra == "async-etcd"
47
+ Provides-Extra: all
48
+ Requires-Dist: redis!=4.5.2,!=4.5.3,<6.0.0,>3; extra == "all"
49
+ Requires-Dist: redis!=4.5.2,!=4.5.3,>=4.2.0; extra == "all"
50
+ Requires-Dist: pymemcache<5.0.0,>3; extra == "all"
51
+ Requires-Dist: pymongo<5,>4.1; extra == "all"
52
+ Requires-Dist: etcd3; extra == "all"
53
+ Requires-Dist: coredis<5,>=3.4.0; extra == "all"
54
+ Requires-Dist: emcache>=0.6.1; python_version < "3.11" and extra == "all"
55
+ Requires-Dist: emcache>=1; (python_version >= "3.11" and python_version < "3.13.0") and extra == "all"
56
+ Requires-Dist: motor<4,>=3; extra == "all"
57
+ Requires-Dist: aetcd; extra == "all"
58
+ Dynamic: author
59
+ Dynamic: author-email
60
+ Dynamic: classifier
61
+ Dynamic: description
62
+ Dynamic: home-page
63
+ Dynamic: license
64
+ Dynamic: project-url
65
+ Dynamic: provides-extra
66
+ Dynamic: requires-dist
67
+ Dynamic: requires-python
68
+ Dynamic: summary
69
+
70
+ .. |ci| image:: https://github.com/alisaifee/limits/actions/workflows/main.yml/badge.svg?branch=master
71
+ :target: https://github.com/alisaifee/limits/actions?query=branch%3Amaster+workflow%3ACI
72
+ .. |codecov| image:: https://codecov.io/gh/alisaifee/limits/branch/master/graph/badge.svg
73
+ :target: https://codecov.io/gh/alisaifee/limits
74
+ .. |pypi| image:: https://img.shields.io/pypi/v/limits.svg?style=flat-square
75
+ :target: https://pypi.python.org/pypi/limits
76
+ .. |pypi-versions| image:: https://img.shields.io/pypi/pyversions/limits?style=flat-square
77
+ :target: https://pypi.python.org/pypi/limits
78
+ .. |license| image:: https://img.shields.io/pypi/l/limits.svg?style=flat-square
79
+ :target: https://pypi.python.org/pypi/limits
80
+ .. |docs| image:: https://readthedocs.org/projects/limits/badge/?version=latest
81
+ :target: https://limits.readthedocs.org
82
+
83
+ limits
84
+ ------
85
+ |docs| |ci| |codecov| |pypi| |pypi-versions| |license|
86
+
87
+
88
+ **limits** is a python library for rate limiting via multiple strategies
89
+ with commonly used storage backends (Redis, Memcached, MongoDB & Etcd).
90
+
91
+ The library provides identical APIs for use in sync and
92
+ `async <https://limits.readthedocs.io/en/stable/async.html>`_ codebases.
93
+
94
+
95
+ Supported Strategies
96
+ ====================
97
+
98
+ All strategies support the follow methods:
99
+
100
+ - `hit <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.hit>`_: consume a request.
101
+ - `test <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.test>`_: check if a request is allowed.
102
+ - `get_window_stats <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.get_window_stats>`_: retrieve remaining quota and reset time.
103
+
104
+ Fixed Window
105
+ ------------
106
+ `Fixed Window <https://limits.readthedocs.io/en/latest/strategies.html#fixed-window>`_
107
+
108
+ This strategy is the most memory‑efficient because it uses a single counter per resource and
109
+ rate limit. When the first request arrives, a window is started for a fixed duration
110
+ (e.g., for a rate limit of 10 requests per minute the window expires in 60 seconds from the first request).
111
+ All requests in that window increment the counter and when the window expires, the counter resets.
112
+
113
+ Burst traffic that bypasses the rate limit may occur at window boundaries.
114
+
115
+ For example, with a rate limit of 10 requests per minute:
116
+
117
+ - At **00:00:45**, the first request arrives, starting a window from **00:00:45** to **00:01:45**.
118
+ - All requests between **00:00:45** and **00:01:45** count toward the limit.
119
+ - If 10 requests occur at any time in that window, any further request before **00:01:45** is rejected.
120
+ - At **00:01:45**, the counter resets and a new window starts which would allow 10 requests
121
+ until **00:02:45**.
122
+
123
+ Moving Window
124
+ -------------
125
+ `Moving Window <https://limits.readthedocs.io/en/latest/strategies.html#moving-window>`_
126
+
127
+ This strategy adds each request’s timestamp to a log if the ``nth`` oldest entry (where ``n``
128
+ is the limit) is either not present or is older than the duration of the window (for example with a rate limit of
129
+ ``10 requests per minute`` if there are either less than 10 entries or the 10th oldest entry is atleast
130
+ 60 seconds old). Upon adding a new entry to the log "expired" entries are truncated.
131
+
132
+ For example, with a rate limit of 10 requests per minute:
133
+
134
+ - At **00:00:10**, a client sends 1 requests which are allowed.
135
+ - At **00:00:20**, a client sends 2 requests which are allowed.
136
+ - At **00:00:30**, the client sends 4 requests which are allowed.
137
+ - At **00:00:50**, the client sends 3 requests which are allowed (total = 10).
138
+ - At **00:01:11**, the client sends 1 request. The strategy checks the timestamp of the
139
+ 10th oldest entry (**00:00:10**) which is now 61 seconds old and thus expired. The request
140
+ is allowed.
141
+ - At **00:01:12**, the client sends 1 request. The 10th oldest entry's timestamp is **00:00:20**
142
+ which is only 52 seconds old. The request is rejected.
143
+
144
+ Sliding Window Counter
145
+ ------------------------
146
+ `Sliding Window Counter <https://limits.readthedocs.io/en/latest/strategies.html#sliding-window-counter>`_
147
+
148
+ This strategy approximates the moving window while using less memory by maintaining
149
+ two counters:
150
+
151
+ - **Current bucket:** counts requests in the ongoing period.
152
+ - **Previous bucket:** counts requests in the immediately preceding period.
153
+
154
+ When a request arrives, the effective request count is calculated as::
155
+
156
+ weighted_count = current_count + floor(previous_count * weight)
157
+
158
+ The weight is based on how much time has elapsed in the current bucket::
159
+
160
+ weight = (bucket_duration - elapsed_time) / bucket_duration
161
+
162
+ If ``weighted_count`` is below the limit, the request is allowed.
163
+
164
+ For example, with a rate limit of 10 requests per minute:
165
+
166
+ Assume:
167
+
168
+ - The current bucket (spanning **00:01:00** to **00:02:00**) has 8 hits.
169
+ - The previous bucket (spanning **00:00:00** to **00:01:00**) has 4 hits.
170
+
171
+ Scenario 1:
172
+
173
+ - A new request arrives at **00:01:30**, 30 seconds into the current bucket.
174
+ - ``weight = (60 - 30) / 60 = 0.5``.
175
+ - ``weighted_count = floor(8 + (4 * 0.5)) = floor(8 + 2) = 10``.
176
+ - Since the weighted count equals the limit, the request is rejected.
177
+
178
+ Scenario 2:
179
+
180
+ - A new request arrives at **00:01:40**, 40 seconds into the current bucket.
181
+ - ``weight = (60 - 40) / 60 ≈ 0.33``.
182
+ - ``weighted_count = floor(8 + (4 * 0.33)) = floor(8 + 1.32) = 9``.
183
+ - Since the weighted count is below the limit, the request is allowed.
184
+
185
+ Storage backends
186
+ ================
187
+
188
+ - `Redis <https://limits.readthedocs.io/en/latest/storage.html#redis-storage>`_
189
+ - `Memcached <https://limits.readthedocs.io/en/latest/storage.html#memcached-storage>`_
190
+ - `MongoDB <https://limits.readthedocs.io/en/latest/storage.html#mongodb-storage>`_
191
+ - `Etcd <https://limits.readthedocs.io/en/latest/storage.html#etcd-storage>`_
192
+ - `In-Memory <https://limits.readthedocs.io/en/latest/storage.html#in-memory-storage>`_
193
+
194
+ Dive right in
195
+ =============
196
+
197
+ Initialize the storage backend
198
+
199
+ .. code-block:: python
200
+
201
+ from limits import storage
202
+ backend = storage.MemoryStorage()
203
+ # or memcached
204
+ backend = storage.MemcachedStorage("memcached://localhost:11211")
205
+ # or redis
206
+ backend = storage.RedisStorage("redis://localhost:6379")
207
+ # or mongodb
208
+ backend = storage.MongoDbStorage("mongodb://localhost:27017")
209
+ # or use the factory
210
+ storage_uri = "memcached://localhost:11211"
211
+ backend = storage.storage_from_string(storage_uri)
212
+
213
+ Initialize a rate limiter with a strategy
214
+
215
+ .. code-block:: python
216
+
217
+ from limits import strategies
218
+ strategy = strategies.MovingWindowRateLimiter(backend)
219
+ # or fixed window
220
+ strategy = strategies.FixedWindowRateLimiter(backend)
221
+ # or sliding window
222
+ strategy = strategies.SlidingWindowCounterRateLimiter(backend)
223
+
224
+
225
+ Initialize a rate limit
226
+
227
+ .. code-block:: python
228
+
229
+ from limits import parse
230
+ one_per_minute = parse("1/minute")
231
+
232
+ Initialize a rate limit explicitly
233
+
234
+ .. code-block:: python
235
+
236
+ from limits import RateLimitItemPerSecond
237
+ one_per_second = RateLimitItemPerSecond(1, 1)
238
+
239
+ Test the limits
240
+
241
+ .. code-block:: python
242
+
243
+ import time
244
+ assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
245
+ assert False == strategy.hit(one_per_minute, "test_namespace", "foo")
246
+ assert True == strategy.hit(one_per_minute, "test_namespace", "bar")
247
+
248
+ assert True == strategy.hit(one_per_second, "test_namespace", "foo")
249
+ assert False == strategy.hit(one_per_second, "test_namespace", "foo")
250
+ time.sleep(1)
251
+ assert True == strategy.hit(one_per_second, "test_namespace", "foo")
252
+
253
+ Check specific limits without hitting them
254
+
255
+ .. code-block:: python
256
+
257
+ assert True == strategy.hit(one_per_second, "test_namespace", "foo")
258
+ while not strategy.test(one_per_second, "test_namespace", "foo"):
259
+ time.sleep(0.01)
260
+ assert True == strategy.hit(one_per_second, "test_namespace", "foo")
261
+
262
+ Query available capacity and reset time for a limit
263
+
264
+ .. code-block:: python
265
+
266
+ assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
267
+ window = strategy.get_window_stats(one_per_minute, "test_namespace", "foo")
268
+ assert window.remaining == 0
269
+ assert False == strategy.hit(one_per_minute, "test_namespace", "foo")
270
+ time.sleep(window.reset_time - time.time())
271
+ assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
272
+
273
+
274
+ Links
275
+ =====
276
+
277
+ * `Documentation <http://limits.readthedocs.org/en/latest>`_
278
+ * `Changelog <http://limits.readthedocs.org/en/stable/changelog.html>`_
279
+
limits-4.1/README.rst ADDED
@@ -0,0 +1,210 @@
1
+ .. |ci| image:: https://github.com/alisaifee/limits/actions/workflows/main.yml/badge.svg?branch=master
2
+ :target: https://github.com/alisaifee/limits/actions?query=branch%3Amaster+workflow%3ACI
3
+ .. |codecov| image:: https://codecov.io/gh/alisaifee/limits/branch/master/graph/badge.svg
4
+ :target: https://codecov.io/gh/alisaifee/limits
5
+ .. |pypi| image:: https://img.shields.io/pypi/v/limits.svg?style=flat-square
6
+ :target: https://pypi.python.org/pypi/limits
7
+ .. |pypi-versions| image:: https://img.shields.io/pypi/pyversions/limits?style=flat-square
8
+ :target: https://pypi.python.org/pypi/limits
9
+ .. |license| image:: https://img.shields.io/pypi/l/limits.svg?style=flat-square
10
+ :target: https://pypi.python.org/pypi/limits
11
+ .. |docs| image:: https://readthedocs.org/projects/limits/badge/?version=latest
12
+ :target: https://limits.readthedocs.org
13
+
14
+ limits
15
+ ------
16
+ |docs| |ci| |codecov| |pypi| |pypi-versions| |license|
17
+
18
+
19
+ **limits** is a python library for rate limiting via multiple strategies
20
+ with commonly used storage backends (Redis, Memcached, MongoDB & Etcd).
21
+
22
+ The library provides identical APIs for use in sync and
23
+ `async <https://limits.readthedocs.io/en/stable/async.html>`_ codebases.
24
+
25
+
26
+ Supported Strategies
27
+ ====================
28
+
29
+ All strategies support the follow methods:
30
+
31
+ - `hit <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.hit>`_: consume a request.
32
+ - `test <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.test>`_: check if a request is allowed.
33
+ - `get_window_stats <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.get_window_stats>`_: retrieve remaining quota and reset time.
34
+
35
+ Fixed Window
36
+ ------------
37
+ `Fixed Window <https://limits.readthedocs.io/en/latest/strategies.html#fixed-window>`_
38
+
39
+ This strategy is the most memory‑efficient because it uses a single counter per resource and
40
+ rate limit. When the first request arrives, a window is started for a fixed duration
41
+ (e.g., for a rate limit of 10 requests per minute the window expires in 60 seconds from the first request).
42
+ All requests in that window increment the counter and when the window expires, the counter resets.
43
+
44
+ Burst traffic that bypasses the rate limit may occur at window boundaries.
45
+
46
+ For example, with a rate limit of 10 requests per minute:
47
+
48
+ - At **00:00:45**, the first request arrives, starting a window from **00:00:45** to **00:01:45**.
49
+ - All requests between **00:00:45** and **00:01:45** count toward the limit.
50
+ - If 10 requests occur at any time in that window, any further request before **00:01:45** is rejected.
51
+ - At **00:01:45**, the counter resets and a new window starts which would allow 10 requests
52
+ until **00:02:45**.
53
+
54
+ Moving Window
55
+ -------------
56
+ `Moving Window <https://limits.readthedocs.io/en/latest/strategies.html#moving-window>`_
57
+
58
+ This strategy adds each request’s timestamp to a log if the ``nth`` oldest entry (where ``n``
59
+ is the limit) is either not present or is older than the duration of the window (for example with a rate limit of
60
+ ``10 requests per minute`` if there are either less than 10 entries or the 10th oldest entry is atleast
61
+ 60 seconds old). Upon adding a new entry to the log "expired" entries are truncated.
62
+
63
+ For example, with a rate limit of 10 requests per minute:
64
+
65
+ - At **00:00:10**, a client sends 1 requests which are allowed.
66
+ - At **00:00:20**, a client sends 2 requests which are allowed.
67
+ - At **00:00:30**, the client sends 4 requests which are allowed.
68
+ - At **00:00:50**, the client sends 3 requests which are allowed (total = 10).
69
+ - At **00:01:11**, the client sends 1 request. The strategy checks the timestamp of the
70
+ 10th oldest entry (**00:00:10**) which is now 61 seconds old and thus expired. The request
71
+ is allowed.
72
+ - At **00:01:12**, the client sends 1 request. The 10th oldest entry's timestamp is **00:00:20**
73
+ which is only 52 seconds old. The request is rejected.
74
+
75
+ Sliding Window Counter
76
+ ------------------------
77
+ `Sliding Window Counter <https://limits.readthedocs.io/en/latest/strategies.html#sliding-window-counter>`_
78
+
79
+ This strategy approximates the moving window while using less memory by maintaining
80
+ two counters:
81
+
82
+ - **Current bucket:** counts requests in the ongoing period.
83
+ - **Previous bucket:** counts requests in the immediately preceding period.
84
+
85
+ When a request arrives, the effective request count is calculated as::
86
+
87
+ weighted_count = current_count + floor(previous_count * weight)
88
+
89
+ The weight is based on how much time has elapsed in the current bucket::
90
+
91
+ weight = (bucket_duration - elapsed_time) / bucket_duration
92
+
93
+ If ``weighted_count`` is below the limit, the request is allowed.
94
+
95
+ For example, with a rate limit of 10 requests per minute:
96
+
97
+ Assume:
98
+
99
+ - The current bucket (spanning **00:01:00** to **00:02:00**) has 8 hits.
100
+ - The previous bucket (spanning **00:00:00** to **00:01:00**) has 4 hits.
101
+
102
+ Scenario 1:
103
+
104
+ - A new request arrives at **00:01:30**, 30 seconds into the current bucket.
105
+ - ``weight = (60 - 30) / 60 = 0.5``.
106
+ - ``weighted_count = floor(8 + (4 * 0.5)) = floor(8 + 2) = 10``.
107
+ - Since the weighted count equals the limit, the request is rejected.
108
+
109
+ Scenario 2:
110
+
111
+ - A new request arrives at **00:01:40**, 40 seconds into the current bucket.
112
+ - ``weight = (60 - 40) / 60 ≈ 0.33``.
113
+ - ``weighted_count = floor(8 + (4 * 0.33)) = floor(8 + 1.32) = 9``.
114
+ - Since the weighted count is below the limit, the request is allowed.
115
+
116
+ Storage backends
117
+ ================
118
+
119
+ - `Redis <https://limits.readthedocs.io/en/latest/storage.html#redis-storage>`_
120
+ - `Memcached <https://limits.readthedocs.io/en/latest/storage.html#memcached-storage>`_
121
+ - `MongoDB <https://limits.readthedocs.io/en/latest/storage.html#mongodb-storage>`_
122
+ - `Etcd <https://limits.readthedocs.io/en/latest/storage.html#etcd-storage>`_
123
+ - `In-Memory <https://limits.readthedocs.io/en/latest/storage.html#in-memory-storage>`_
124
+
125
+ Dive right in
126
+ =============
127
+
128
+ Initialize the storage backend
129
+
130
+ .. code-block:: python
131
+
132
+ from limits import storage
133
+ backend = storage.MemoryStorage()
134
+ # or memcached
135
+ backend = storage.MemcachedStorage("memcached://localhost:11211")
136
+ # or redis
137
+ backend = storage.RedisStorage("redis://localhost:6379")
138
+ # or mongodb
139
+ backend = storage.MongoDbStorage("mongodb://localhost:27017")
140
+ # or use the factory
141
+ storage_uri = "memcached://localhost:11211"
142
+ backend = storage.storage_from_string(storage_uri)
143
+
144
+ Initialize a rate limiter with a strategy
145
+
146
+ .. code-block:: python
147
+
148
+ from limits import strategies
149
+ strategy = strategies.MovingWindowRateLimiter(backend)
150
+ # or fixed window
151
+ strategy = strategies.FixedWindowRateLimiter(backend)
152
+ # or sliding window
153
+ strategy = strategies.SlidingWindowCounterRateLimiter(backend)
154
+
155
+
156
+ Initialize a rate limit
157
+
158
+ .. code-block:: python
159
+
160
+ from limits import parse
161
+ one_per_minute = parse("1/minute")
162
+
163
+ Initialize a rate limit explicitly
164
+
165
+ .. code-block:: python
166
+
167
+ from limits import RateLimitItemPerSecond
168
+ one_per_second = RateLimitItemPerSecond(1, 1)
169
+
170
+ Test the limits
171
+
172
+ .. code-block:: python
173
+
174
+ import time
175
+ assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
176
+ assert False == strategy.hit(one_per_minute, "test_namespace", "foo")
177
+ assert True == strategy.hit(one_per_minute, "test_namespace", "bar")
178
+
179
+ assert True == strategy.hit(one_per_second, "test_namespace", "foo")
180
+ assert False == strategy.hit(one_per_second, "test_namespace", "foo")
181
+ time.sleep(1)
182
+ assert True == strategy.hit(one_per_second, "test_namespace", "foo")
183
+
184
+ Check specific limits without hitting them
185
+
186
+ .. code-block:: python
187
+
188
+ assert True == strategy.hit(one_per_second, "test_namespace", "foo")
189
+ while not strategy.test(one_per_second, "test_namespace", "foo"):
190
+ time.sleep(0.01)
191
+ assert True == strategy.hit(one_per_second, "test_namespace", "foo")
192
+
193
+ Query available capacity and reset time for a limit
194
+
195
+ .. code-block:: python
196
+
197
+ assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
198
+ window = strategy.get_window_stats(one_per_minute, "test_namespace", "foo")
199
+ assert window.remaining == 0
200
+ assert False == strategy.hit(one_per_minute, "test_namespace", "foo")
201
+ time.sleep(window.reset_time - time.time())
202
+ assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
203
+
204
+
205
+ Links
206
+ =====
207
+
208
+ * `Documentation <http://limits.readthedocs.org/en/latest>`_
209
+ * `Changelog <http://limits.readthedocs.org/en/stable/changelog.html>`_
210
+
@@ -30,6 +30,7 @@ Provided by :mod:`limits.strategies`
30
30
  .. autoclass:: FixedWindowRateLimiter
31
31
  .. autoclass:: FixedWindowElasticExpiryRateLimiter
32
32
  .. autoclass:: MovingWindowRateLimiter
33
+ .. autoclass:: SlidingWindowCounterRateLimiter
33
34
 
34
35
  All strategies implement the same abstract base class:
35
36
 
@@ -48,6 +49,7 @@ Provided by :mod:`limits.aio.strategies`
48
49
  .. autoclass:: FixedWindowRateLimiter
49
50
  .. autoclass:: FixedWindowElasticExpiryRateLimiter
50
51
  .. autoclass:: MovingWindowRateLimiter
52
+ .. autoclass:: SlidingWindowCounterRateLimiter
51
53
 
52
54
  All strategies implement the same abstract base class:
53
55
 
@@ -152,13 +154,15 @@ Abstract storage classes
152
154
 
153
155
  .. autoclass:: limits.storage.Storage
154
156
  .. autoclass:: limits.storage.MovingWindowSupport
157
+ .. autoclass:: limits.storage.SlidingWindowCounterSupport
155
158
 
156
159
 
157
- Async variants
158
- ^^^^^^^^^^^^^^
160
+ Async Abstract storage classes
161
+ --------------------------------
159
162
 
160
163
  .. autoclass:: limits.aio.storage.Storage
161
164
  .. autoclass:: limits.aio.storage.MovingWindowSupport
165
+ .. autoclass:: limits.aio.storage.SlidingWindowCounterSupport
162
166
 
163
167
 
164
168
  Rate Limits
@@ -47,6 +47,7 @@ extensions = [
47
47
  "sphinx.ext.intersphinx",
48
48
  "sphinx.ext.todo",
49
49
  "sphinx.ext.viewcode",
50
+ "sphinx.ext.mathjax",
50
51
  "sphinxext.opengraph",
51
52
  "sphinxcontrib.programoutput",
52
53
  "sphinx_copybutton",