limits 4.0.1__py3-none-any.whl → 4.2__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.
Files changed (39) hide show
  1. limits/__init__.py +3 -1
  2. limits/_version.py +4 -4
  3. limits/aio/__init__.py +2 -0
  4. limits/aio/storage/__init__.py +4 -1
  5. limits/aio/storage/base.py +70 -24
  6. limits/aio/storage/etcd.py +8 -2
  7. limits/aio/storage/memcached.py +159 -33
  8. limits/aio/storage/memory.py +100 -13
  9. limits/aio/storage/mongodb.py +217 -9
  10. limits/aio/storage/redis/__init__.py +341 -0
  11. limits/aio/storage/redis/bridge.py +121 -0
  12. limits/aio/storage/redis/coredis.py +209 -0
  13. limits/aio/storage/redis/redispy.py +257 -0
  14. limits/aio/strategies.py +124 -1
  15. limits/errors.py +2 -0
  16. limits/limits.py +10 -11
  17. limits/resources/redis/lua_scripts/acquire_sliding_window.lua +45 -0
  18. limits/resources/redis/lua_scripts/sliding_window.lua +17 -0
  19. limits/storage/__init__.py +6 -3
  20. limits/storage/base.py +92 -24
  21. limits/storage/etcd.py +8 -2
  22. limits/storage/memcached.py +143 -34
  23. limits/storage/memory.py +99 -12
  24. limits/storage/mongodb.py +204 -11
  25. limits/storage/redis.py +159 -138
  26. limits/storage/redis_cluster.py +5 -3
  27. limits/storage/redis_sentinel.py +14 -35
  28. limits/storage/registry.py +3 -3
  29. limits/strategies.py +121 -5
  30. limits/typing.py +55 -19
  31. limits/util.py +29 -18
  32. limits-4.2.dist-info/METADATA +268 -0
  33. limits-4.2.dist-info/RECORD +42 -0
  34. limits/aio/storage/redis.py +0 -470
  35. limits-4.0.1.dist-info/METADATA +0 -192
  36. limits-4.0.1.dist-info/RECORD +0 -37
  37. {limits-4.0.1.dist-info → limits-4.2.dist-info}/LICENSE.txt +0 -0
  38. {limits-4.0.1.dist-info → limits-4.2.dist-info}/WHEEL +0 -0
  39. {limits-4.0.1.dist-info → limits-4.2.dist-info}/top_level.txt +0 -0
@@ -1,470 +0,0 @@
1
- import time
2
- import urllib
3
- from typing import TYPE_CHECKING, cast
4
-
5
- from deprecated.sphinx import versionadded
6
- from packaging.version import Version
7
-
8
- from limits.aio.storage.base import MovingWindowSupport, Storage
9
- from limits.errors import ConfigurationError
10
- from limits.typing import AsyncRedisClient, Dict, Optional, Tuple, Type, Union
11
- from limits.util import get_package_data
12
-
13
- if TYPE_CHECKING:
14
- import coredis
15
- import coredis.commands
16
-
17
-
18
- class RedisInteractor:
19
- RES_DIR = "resources/redis/lua_scripts"
20
-
21
- SCRIPT_MOVING_WINDOW = get_package_data(f"{RES_DIR}/moving_window.lua")
22
- SCRIPT_ACQUIRE_MOVING_WINDOW = get_package_data(
23
- f"{RES_DIR}/acquire_moving_window.lua"
24
- )
25
- SCRIPT_CLEAR_KEYS = get_package_data(f"{RES_DIR}/clear_keys.lua")
26
- SCRIPT_INCR_EXPIRE = get_package_data(f"{RES_DIR}/incr_expire.lua")
27
-
28
- lua_moving_window: "coredis.commands.Script[bytes]"
29
- lua_acquire_window: "coredis.commands.Script[bytes]"
30
- lua_clear_keys: "coredis.commands.Script[bytes]"
31
- lua_incr_expire: "coredis.commands.Script[bytes]"
32
-
33
- PREFIX = "LIMITS"
34
-
35
- def prefixed_key(self, key: str) -> str:
36
- return f"{self.PREFIX}:{key}"
37
-
38
- async def _incr(
39
- self,
40
- key: str,
41
- expiry: int,
42
- connection: AsyncRedisClient,
43
- elastic_expiry: bool = False,
44
- amount: int = 1,
45
- ) -> int:
46
- """
47
- increments the counter for a given rate limit key
48
-
49
- :param connection: Redis connection
50
- :param key: the key to increment
51
- :param expiry: amount in seconds for the key to expire in
52
- :param amount: the number to increment by
53
- """
54
- key = self.prefixed_key(key)
55
- value = await connection.incrby(key, amount)
56
-
57
- if elastic_expiry or value == amount:
58
- await connection.expire(key, expiry)
59
-
60
- return value
61
-
62
- async def _get(self, key: str, connection: AsyncRedisClient) -> int:
63
- """
64
- :param connection: Redis connection
65
- :param key: the key to get the counter value for
66
- """
67
-
68
- key = self.prefixed_key(key)
69
- return int(await connection.get(key) or 0)
70
-
71
- async def _clear(self, key: str, connection: AsyncRedisClient) -> None:
72
- """
73
- :param key: the key to clear rate limits for
74
- :param connection: Redis connection
75
- """
76
- key = self.prefixed_key(key)
77
- await connection.delete([key])
78
-
79
- async def get_moving_window(
80
- self, key: str, limit: int, expiry: int
81
- ) -> Tuple[float, int]:
82
- """
83
- returns the starting point and the number of entries in the moving
84
- window
85
-
86
- :param key: rate limit key
87
- :param expiry: expiry of entry
88
- :return: (start of window, number of acquired entries)
89
- """
90
- key = self.prefixed_key(key)
91
- timestamp = time.time()
92
- window = await self.lua_moving_window.execute(
93
- [key], [timestamp - expiry, limit]
94
- )
95
- if window:
96
- return float(window[0]), window[1] # type: ignore
97
- return timestamp, 0
98
-
99
- async def _acquire_entry(
100
- self,
101
- key: str,
102
- limit: int,
103
- expiry: int,
104
- connection: AsyncRedisClient,
105
- amount: int = 1,
106
- ) -> bool:
107
- """
108
- :param key: rate limit key to acquire an entry in
109
- :param limit: amount of entries allowed
110
- :param expiry: expiry of the entry
111
- :param connection: Redis connection
112
- """
113
- key = self.prefixed_key(key)
114
- timestamp = time.time()
115
- acquired = await self.lua_acquire_window.execute(
116
- [key], [timestamp, limit, expiry, amount]
117
- )
118
-
119
- return bool(acquired)
120
-
121
- async def _get_expiry(self, key: str, connection: AsyncRedisClient) -> float:
122
- """
123
- :param key: the key to get the expiry for
124
- :param connection: Redis connection
125
- """
126
-
127
- key = self.prefixed_key(key)
128
- return max(await connection.ttl(key), 0) + time.time()
129
-
130
- async def _check(self, connection: AsyncRedisClient) -> bool:
131
- """
132
- check if storage is healthy
133
-
134
- :param connection: Redis connection
135
- """
136
- try:
137
- await connection.ping()
138
-
139
- return True
140
- except: # noqa
141
- return False
142
-
143
-
144
- @versionadded(version="2.1")
145
- class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
146
- """
147
- Rate limit storage with redis as backend.
148
-
149
- Depends on :pypi:`coredis`
150
- """
151
-
152
- STORAGE_SCHEME = ["async+redis", "async+rediss", "async+redis+unix"]
153
- """
154
- The storage schemes for redis to be used in an async context
155
- """
156
- DEPENDENCIES = {"coredis": Version("3.4.0")}
157
-
158
- def __init__(
159
- self,
160
- uri: str,
161
- connection_pool: Optional["coredis.ConnectionPool"] = None,
162
- wrap_exceptions: bool = False,
163
- **options: Union[float, str, bool],
164
- ) -> None:
165
- """
166
- :param uri: uri of the form:
167
-
168
- - ``async+redis://[:password]@host:port``
169
- - ``async+redis://[:password]@host:port/db``
170
- - ``async+rediss://[:password]@host:port``
171
- - ``async+redis+unix:///path/to/sock?db=0`` etc...
172
-
173
- This uri is passed directly to :meth:`coredis.Redis.from_url` with
174
- the initial ``async`` removed, except for the case of ``async+redis+unix``
175
- where it is replaced with ``unix``.
176
- :param connection_pool: if provided, the redis client is initialized with
177
- the connection pool and any other params passed as :paramref:`options`
178
- :param wrap_exceptions: Whether to wrap storage exceptions in
179
- :exc:`limits.errors.StorageError` before raising it.
180
- :param options: all remaining keyword arguments are passed
181
- directly to the constructor of :class:`coredis.Redis`
182
- :raise ConfigurationError: when the redis library is not available
183
- """
184
- uri = uri.replace("async+redis", "redis", 1)
185
- uri = uri.replace("redis+unix", "unix")
186
-
187
- super().__init__(uri, wrap_exceptions=wrap_exceptions, **options)
188
-
189
- self.dependency = self.dependencies["coredis"].module
190
-
191
- if connection_pool:
192
- self.storage = self.dependency.Redis(
193
- connection_pool=connection_pool, **options
194
- )
195
- else:
196
- self.storage = self.dependency.Redis.from_url(uri, **options)
197
-
198
- self.initialize_storage(uri)
199
-
200
- @property
201
- def base_exceptions(
202
- self,
203
- ) -> Union[Type[Exception], Tuple[Type[Exception], ...]]: # pragma: no cover
204
- return self.dependency.exceptions.RedisError # type: ignore[no-any-return]
205
-
206
- def initialize_storage(self, _uri: str) -> None:
207
- # all these methods are coroutines, so must be called with await
208
- self.lua_moving_window = self.storage.register_script(self.SCRIPT_MOVING_WINDOW)
209
- self.lua_acquire_window = self.storage.register_script(
210
- self.SCRIPT_ACQUIRE_MOVING_WINDOW
211
- )
212
- 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
215
- )
216
-
217
- async def incr(
218
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
219
- ) -> int:
220
- """
221
- increments the counter for a given rate limit key
222
-
223
- :param key: the key to increment
224
- :param expiry: amount in seconds for the key to expire in
225
- :param amount: the number to increment by
226
- """
227
-
228
- if elastic_expiry:
229
- return await super()._incr(
230
- key, expiry, self.storage, elastic_expiry, amount
231
- )
232
- else:
233
- key = self.prefixed_key(key)
234
- return cast(
235
- int, await self.lua_incr_expire.execute([key], [expiry, amount])
236
- )
237
-
238
- async def get(self, key: str) -> int:
239
- """
240
- :param key: the key to get the counter value for
241
- """
242
-
243
- return await super()._get(key, self.storage)
244
-
245
- async def clear(self, key: str) -> None:
246
- """
247
- :param key: the key to clear rate limits for
248
- """
249
-
250
- return await super()._clear(key, self.storage)
251
-
252
- async def acquire_entry(
253
- self, key: str, limit: int, expiry: int, amount: int = 1
254
- ) -> bool:
255
- """
256
- :param key: rate limit key to acquire an entry in
257
- :param limit: amount of entries allowed
258
- :param expiry: expiry of the entry
259
- :param amount: the number of entries to acquire
260
- """
261
-
262
- return await super()._acquire_entry(key, limit, expiry, self.storage, amount)
263
-
264
- async def get_expiry(self, key: str) -> float:
265
- """
266
- :param key: the key to get the expiry for
267
- """
268
-
269
- return await super()._get_expiry(key, self.storage)
270
-
271
- async def check(self) -> bool:
272
- """
273
- Check if storage is healthy by calling :meth:`coredis.Redis.ping`
274
- """
275
-
276
- return await super()._check(self.storage)
277
-
278
- async def reset(self) -> Optional[int]:
279
- """
280
- This function calls a Lua Script to delete keys prefixed with
281
- ``self.PREFIX`` in blocks of 5000.
282
-
283
- .. warning:: This operation was designed to be fast, but was not tested
284
- on a large production based system. Be careful with its usage as it
285
- could be slow on very large data sets.
286
- """
287
-
288
- prefix = self.prefixed_key("*")
289
- return cast(int, await self.lua_clear_keys.execute([prefix]))
290
-
291
-
292
- @versionadded(version="2.1")
293
- class RedisClusterStorage(RedisStorage):
294
- """
295
- Rate limit storage with redis cluster as backend
296
-
297
- Depends on :pypi:`coredis`
298
- """
299
-
300
- STORAGE_SCHEME = ["async+redis+cluster"]
301
- """
302
- The storage schemes for redis cluster to be used in an async context
303
- """
304
-
305
- DEFAULT_OPTIONS: Dict[str, Union[float, str, bool]] = {
306
- "max_connections": 1000,
307
- }
308
- "Default options passed to :class:`coredis.RedisCluster`"
309
-
310
- def __init__(
311
- self,
312
- uri: str,
313
- wrap_exceptions: bool = False,
314
- **options: Union[float, str, bool],
315
- ) -> None:
316
- """
317
- :param uri: url of the form
318
- ``async+redis+cluster://[:password]@host:port,host:port``
319
- :param options: all remaining keyword arguments are passed
320
- directly to the constructor of :class:`coredis.RedisCluster`
321
- :raise ConfigurationError: when the coredis library is not
322
- available or if the redis host cannot be pinged.
323
- """
324
- parsed = urllib.parse.urlparse(uri)
325
- parsed_auth: Dict[str, Union[float, str, bool]] = {}
326
-
327
- if parsed.username:
328
- parsed_auth["username"] = parsed.username
329
- if parsed.password:
330
- parsed_auth["password"] = parsed.password
331
-
332
- sep = parsed.netloc.find("@") + 1
333
- cluster_hosts = []
334
-
335
- for loc in parsed.netloc[sep:].split(","):
336
- host, port = loc.split(":")
337
- cluster_hosts.append({"host": host, "port": int(port)})
338
-
339
- super(RedisStorage, self).__init__(
340
- uri, wrap_exceptions=wrap_exceptions, **options
341
- )
342
-
343
- self.dependency = self.dependencies["coredis"].module
344
-
345
- self.storage: "coredis.RedisCluster[str]" = self.dependency.RedisCluster(
346
- startup_nodes=cluster_hosts,
347
- **{**self.DEFAULT_OPTIONS, **parsed_auth, **options},
348
- )
349
- self.initialize_storage(uri)
350
-
351
- async def reset(self) -> Optional[int]:
352
- """
353
- Redis Clusters are sharded and deleting across shards
354
- can't be done atomically. Because of this, this reset loops over all
355
- keys that are prefixed with ``self.PREFIX`` and calls delete on them,
356
- one at a time.
357
-
358
- .. warning:: This operation was not tested with extremely large data sets.
359
- On a large production based system, care should be taken with its
360
- usage as it could be slow on very large data sets
361
- """
362
-
363
- prefix = self.prefixed_key("*")
364
- keys = await self.storage.keys(prefix)
365
- count = 0
366
- for key in keys:
367
- count += await self.storage.delete([key])
368
- return count
369
-
370
-
371
- @versionadded(version="2.1")
372
- class RedisSentinelStorage(RedisStorage):
373
- """
374
- Rate limit storage with redis sentinel as backend
375
-
376
- Depends on :pypi:`coredis`
377
- """
378
-
379
- STORAGE_SCHEME = ["async+redis+sentinel"]
380
- """The storage scheme for redis accessed via a redis sentinel installation"""
381
-
382
- DEPENDENCIES = {"coredis.sentinel": Version("3.4.0")}
383
-
384
- def __init__(
385
- self,
386
- uri: str,
387
- service_name: Optional[str] = None,
388
- use_replicas: bool = True,
389
- sentinel_kwargs: Optional[Dict[str, Union[float, str, bool]]] = None,
390
- **options: Union[float, str, bool],
391
- ):
392
- """
393
- :param uri: url of the form
394
- ``async+redis+sentinel://host:port,host:port/service_name``
395
- :param service_name, optional: sentinel service name
396
- (if not provided in `uri`)
397
- :param use_replicas: Whether to use replicas for read only operations
398
- :param sentinel_kwargs, optional: kwargs to pass as
399
- ``sentinel_kwargs`` to :class:`coredis.sentinel.Sentinel`
400
- :param options: all remaining keyword arguments are passed
401
- directly to the constructor of :class:`coredis.sentinel.Sentinel`
402
- :raise ConfigurationError: when the coredis library is not available
403
- or if the redis primary host cannot be pinged.
404
- """
405
-
406
- parsed = urllib.parse.urlparse(uri)
407
- sentinel_configuration = []
408
- connection_options = options.copy()
409
- sentinel_options = sentinel_kwargs.copy() if sentinel_kwargs else {}
410
- parsed_auth: Dict[str, Union[float, str, bool]] = {}
411
-
412
- if parsed.username:
413
- parsed_auth["username"] = parsed.username
414
-
415
- if parsed.password:
416
- parsed_auth["password"] = parsed.password
417
-
418
- sep = parsed.netloc.find("@") + 1
419
-
420
- for loc in parsed.netloc[sep:].split(","):
421
- host, port = loc.split(":")
422
- sentinel_configuration.append((host, int(port)))
423
- self.service_name = (
424
- parsed.path.replace("/", "") if parsed.path else service_name
425
- )
426
-
427
- if self.service_name is None:
428
- raise ConfigurationError("'service_name' not provided")
429
-
430
- super(RedisStorage, self).__init__()
431
-
432
- self.dependency = self.dependencies["coredis.sentinel"].module
433
-
434
- self.sentinel = self.dependency.Sentinel(
435
- sentinel_configuration,
436
- sentinel_kwargs={**parsed_auth, **sentinel_options},
437
- **{**parsed_auth, **connection_options},
438
- )
439
- self.storage = self.sentinel.primary_for(self.service_name)
440
- self.storage_replica = self.sentinel.replica_for(self.service_name)
441
- self.use_replicas = use_replicas
442
- self.initialize_storage(uri)
443
-
444
- async def get(self, key: str) -> int:
445
- """
446
- :param key: the key to get the counter value for
447
- """
448
-
449
- return await super()._get(
450
- key, self.storage_replica if self.use_replicas else self.storage
451
- )
452
-
453
- async def get_expiry(self, key: str) -> float:
454
- """
455
- :param key: the key to get the expiry for
456
- """
457
-
458
- return await super()._get_expiry(
459
- key, self.storage_replica if self.use_replicas else self.storage
460
- )
461
-
462
- async def check(self) -> bool:
463
- """
464
- Check if storage is healthy by calling :meth:`coredis.Redis.ping`
465
- on the replica.
466
- """
467
-
468
- return await super()._check(
469
- self.storage_replica if self.use_replicas else self.storage
470
- )
@@ -1,192 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: limits
3
- Version: 4.0.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.8
18
- Classifier: Programming Language :: Python :: 3.9
19
- Classifier: Programming Language :: Python :: 3.10
20
- Classifier: Programming Language :: Python :: 3.11
21
- Classifier: Programming Language :: Python :: 3.12
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: all
29
- Requires-Dist: redis !=4.5.2,!=4.5.3,<6.0.0,>3 ; extra == 'all'
30
- Requires-Dist: redis !=4.5.2,!=4.5.3,>=4.2.0 ; extra == 'all'
31
- Requires-Dist: pymemcache <5.0.0,>3 ; extra == 'all'
32
- Requires-Dist: pymongo <5,>4.1 ; extra == 'all'
33
- Requires-Dist: etcd3 ; extra == 'all'
34
- Requires-Dist: coredis <5,>=3.4.0 ; extra == 'all'
35
- Requires-Dist: motor <4,>=3 ; extra == 'all'
36
- Requires-Dist: aetcd ; extra == 'all'
37
- Requires-Dist: emcache >=0.6.1 ; (python_version < "3.11") and extra == 'all'
38
- Requires-Dist: emcache >=1 ; (python_version >= "3.11" and python_version < "3.13.0") and extra == 'all'
39
- Provides-Extra: async-etcd
40
- Requires-Dist: aetcd ; extra == 'async-etcd'
41
- Provides-Extra: async-memcached
42
- Requires-Dist: emcache >=0.6.1 ; (python_version < "3.11") and extra == 'async-memcached'
43
- Requires-Dist: emcache >=1 ; (python_version >= "3.11" and python_version < "3.13.0") and extra == 'async-memcached'
44
- Provides-Extra: async-mongodb
45
- Requires-Dist: motor <4,>=3 ; extra == 'async-mongodb'
46
- Provides-Extra: async-redis
47
- Requires-Dist: coredis <5,>=3.4.0 ; extra == 'async-redis'
48
- Provides-Extra: etcd
49
- Requires-Dist: etcd3 ; extra == 'etcd'
50
- Provides-Extra: memcached
51
- Requires-Dist: pymemcache <5.0.0,>3 ; extra == 'memcached'
52
- Provides-Extra: mongodb
53
- Requires-Dist: pymongo <5,>4.1 ; extra == 'mongodb'
54
- Provides-Extra: redis
55
- Requires-Dist: redis !=4.5.2,!=4.5.3,<6.0.0,>3 ; extra == 'redis'
56
- Provides-Extra: rediscluster
57
- Requires-Dist: redis !=4.5.2,!=4.5.3,>=4.2.0 ; extra == 'rediscluster'
58
-
59
- .. |ci| image:: https://github.com/alisaifee/limits/workflows/CI/badge.svg?branch=master
60
- :target: https://github.com/alisaifee/limits/actions?query=branch%3Amaster+workflow%3ACI
61
- .. |codecov| image:: https://codecov.io/gh/alisaifee/limits/branch/master/graph/badge.svg
62
- :target: https://codecov.io/gh/alisaifee/limits
63
- .. |pypi| image:: https://img.shields.io/pypi/v/limits.svg?style=flat-square
64
- :target: https://pypi.python.org/pypi/limits
65
- .. |pypi-versions| image:: https://img.shields.io/pypi/pyversions/limits?style=flat-square
66
- :target: https://pypi.python.org/pypi/limits
67
- .. |license| image:: https://img.shields.io/pypi/l/limits.svg?style=flat-square
68
- :target: https://pypi.python.org/pypi/limits
69
- .. |docs| image:: https://readthedocs.org/projects/limits/badge/?version=latest
70
- :target: https://limits.readthedocs.org
71
-
72
- limits
73
- ------
74
- |docs| |ci| |codecov| |pypi| |pypi-versions| |license|
75
-
76
-
77
- **limits** is a python library to perform rate limiting with commonly used storage backends (Redis, Memcached, MongoDB & Etcd).
78
-
79
-
80
- Supported Strategies
81
- ====================
82
- `Fixed Window <https://limits.readthedocs.io/en/latest/strategies.html#fixed-window>`_
83
- This strategy resets at a fixed interval (start of minute, hour, day etc).
84
- For example, given a rate limit of ``10/minute`` the strategy will:
85
-
86
- - Allow 10 requests between ``00:01:00`` and ``00:02:00``
87
- - Allow 10 requests at ``00:00:59`` and 10 more requests at ``00:01:00``
88
-
89
-
90
- `Fixed Window (Elastic) <https://limits.readthedocs.io/en/latest/strategies.html#fixed-window-with-elastic-expiry>`_
91
- Identical to Fixed window, except every breach of rate limit results in an extension
92
- to the time out. For example a rate limit of `1/minute` hit twice within a minute will
93
- result in a lock-out for two minutes.
94
-
95
- `Moving Window <https://limits.readthedocs.io/en/latest/strategies.html#moving-window>`_
96
- Sliding window strategy enforces a rate limit of N/(m time units)
97
- on the **last m** time units at the second granularity.
98
-
99
- For example, with a rate limit of ``10/minute``:
100
-
101
- - Allow 9 requests that arrive at ``00:00:59``
102
- - Allow another request that arrives at ``00:01:00``
103
- - Reject the request that arrives at ``00:01:01``
104
-
105
- Storage backends
106
- ================
107
-
108
- - `Redis <https://limits.readthedocs.io/en/latest/storage.html#redis-storage>`_
109
- - `Memcached <https://limits.readthedocs.io/en/latest/storage.html#memcached-storage>`_
110
- - `MongoDB <https://limits.readthedocs.io/en/latest/storage.html#mongodb-storage>`_
111
- - `Etcd <https://limits.readthedocs.io/en/latest/storage.html#etcd-storage>`_
112
- - `In-Memory <https://limits.readthedocs.io/en/latest/storage.html#in-memory-storage>`_
113
-
114
- Dive right in
115
- =============
116
-
117
- Initialize the storage backend
118
-
119
- .. code-block:: python
120
-
121
- from limits import storage
122
- memory_storage = storage.MemoryStorage()
123
- # or memcached
124
- memcached_storage = storage.MemcachedStorage("memcached://localhost:11211")
125
- # or redis
126
- redis_storage = storage.RedisStorage("redis://localhost:6379")
127
- # or use the factory
128
- storage_uri = "memcached://localhost:11211"
129
- some_storage = storage.storage_from_string(storage_uri)
130
-
131
- Initialize a rate limiter with the Moving Window Strategy
132
-
133
- .. code-block:: python
134
-
135
- from limits import strategies
136
- moving_window = strategies.MovingWindowRateLimiter(memory_storage)
137
-
138
-
139
- Initialize a rate limit
140
-
141
- .. code-block:: python
142
-
143
- from limits import parse
144
- one_per_minute = parse("1/minute")
145
-
146
- Initialize a rate limit explicitly
147
-
148
- .. code-block:: python
149
-
150
- from limits import RateLimitItemPerSecond
151
- one_per_second = RateLimitItemPerSecond(1, 1)
152
-
153
- Test the limits
154
-
155
- .. code-block:: python
156
-
157
- assert True == moving_window.hit(one_per_minute, "test_namespace", "foo")
158
- assert False == moving_window.hit(one_per_minute, "test_namespace", "foo")
159
- assert True == moving_window.hit(one_per_minute, "test_namespace", "bar")
160
-
161
- assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
162
- assert False == moving_window.hit(one_per_second, "test_namespace", "foo")
163
- time.sleep(1)
164
- assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
165
-
166
- Check specific limits without hitting them
167
-
168
- .. code-block:: python
169
-
170
- assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
171
- while not moving_window.test(one_per_second, "test_namespace", "foo"):
172
- time.sleep(0.01)
173
- assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
174
-
175
- Query available capacity and reset time for a limit
176
-
177
- .. code-block:: python
178
-
179
- assert True == moving_window.hit(one_per_minute, "test_namespace", "foo")
180
- window = moving_window.get_window_stats(one_per_minute, "test_namespace", "foo")
181
- assert window.remaining == 0
182
- assert False == moving_window.hit(one_per_minute, "test_namespace", "foo")
183
- time.sleep(window.reset_time - time.time())
184
- assert True == moving_window.hit(one_per_minute, "test_namespace", "foo")
185
-
186
-
187
- Links
188
- =====
189
-
190
- * `Documentation <http://limits.readthedocs.org/en/latest>`_
191
- * `Changelog <http://limits.readthedocs.org/en/stable/changelog.html>`_
192
-
@@ -1,37 +0,0 @@
1
- limits/__init__.py,sha256=j_yVhgN9pdz8o5rQjVwdJTBSq8F-CTzof9kkiYgjRbw,728
2
- limits/_version.py,sha256=pB8_a1HHuX7Bo8LVuoQ_8GjLU5i4z56J1tr986TiLUA,497
3
- limits/errors.py,sha256=xCKGOVJiD-g8FlsQQb17AW2pTUvalYSuizPpvEVoYJE,626
4
- limits/limits.py,sha256=ZsXESq2e1ji7c2ZKjSkIAasCjiLdjVLPUa9oah_I8U4,4943
5
- limits/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- limits/strategies.py,sha256=Zy6PIhkysPbxnMzFjyXEsxMM6jhRoQ5XT5WskTNruK0,6949
7
- limits/typing.py,sha256=4yitf6iwDK-QEfSxv3EbTdGLOrqowLFffHAqYRUqiYY,3275
8
- limits/util.py,sha256=fTx0JQBT6ZY3fxGefjT07CLimUVCFjS1jLqskXmb7Eo,5726
9
- limits/version.py,sha256=YwkF3dtq1KGzvmL3iVGctA8NNtGlK_0arrzZkZGVjUs,47
10
- limits/aio/__init__.py,sha256=IOetunwQy1c5GefzitK8lewbTzHGiE-kmE9NlqSdr3U,82
11
- limits/aio/strategies.py,sha256=SHjmJnmy7Nh4tBydkA-0qPaULYcLOAM91T4RPybq0Sg,6768
12
- limits/aio/storage/__init__.py,sha256=CbtuSlVl1jPyN_vsEI_ApWblDblVaL46xcZ2M_oM0V8,595
13
- limits/aio/storage/base.py,sha256=V1Ur9Cu29_vP5IYBIsWHTgrc4riW8FEyz5Dcvv6fPoc,4821
14
- limits/aio/storage/etcd.py,sha256=krqjWujvybuaFa2g_FkPr2ZtX9Ac1-oJzErfGW3h27o,4783
15
- limits/aio/storage/memcached.py,sha256=n8b9GVtXMWdc-w4-xP1_MPJ9dgVcgoJ5j53mTdU6E3E,4799
16
- limits/aio/storage/memory.py,sha256=4ah9RpE5r7Q2yOLT-OndhP4ZHmvcwV3rKvCicnK2CJc,5845
17
- limits/aio/storage/mongodb.py,sha256=pC5Ng-5PlV5u1EQlgITfetLvpL4mitcxaaZ_uokwUs0,10846
18
- limits/aio/storage/redis.py,sha256=JQm4pkwynSo1k6wFVB7SyRsh7yav0_61Px1FqUwuGl4,15658
19
- limits/resources/redis/lua_scripts/acquire_moving_window.lua,sha256=5CFJX7D6T6RG5SFr6eVZ6zepmI1EkGWmKeVEO4QNrWo,483
20
- limits/resources/redis/lua_scripts/clear_keys.lua,sha256=zU0cVfLGmapRQF9x9u0GclapM_IB2pJLszNzVQ1QRK4,184
21
- limits/resources/redis/lua_scripts/incr_expire.lua,sha256=Uq9NcrrcDI-F87TDAJexoSJn2SDgeXIUEYozCp9S3oA,195
22
- limits/resources/redis/lua_scripts/moving_window.lua,sha256=5hUZghISDh8Cbg8HJediM_OKjjNMF-0CBywWmsc93vA,430
23
- limits/storage/__init__.py,sha256=_ozbLZtZDVRamfNe4clxcmYbM9Gbu8FzuuAeWQAi_ZA,2588
24
- limits/storage/base.py,sha256=E7ZInoGZqoM1QIpd1f8lvytlic4sMOQdl_eTzD-mlWk,4631
25
- limits/storage/etcd.py,sha256=Q1tndCAAJp0jnir-b-ZBN3-7Kf3v_uwNAqQJLmqB96Q,4440
26
- limits/storage/memcached.py,sha256=4xgcSI7l02KbHP0P_ChFgx5ytDqiby6-kwfJ6_5lua4,6618
27
- limits/storage/memory.py,sha256=ButyS6v7o7DB55bwM3CltFK4Fc5mwKuFEBPgat51CXU,5550
28
- limits/storage/mongodb.py,sha256=LmrkLOlMMveI4hnv69yMOQhrXUqp6RMvdjXkvTiE4DY,9979
29
- limits/storage/redis.py,sha256=R7UbE5ng1NjIHEO17gu-vIZ4qgy91JctbPYGEkZ2iM0,8483
30
- limits/storage/redis_cluster.py,sha256=MsiEpwHphQd0P88AwGw1NVSi3UwVrhsg-pvzkHxU2kw,3739
31
- limits/storage/redis_sentinel.py,sha256=665CvL3UZYB2sB_vVkZ4CCaPKcbIXvQUWuDWnBoSOLU,4124
32
- limits/storage/registry.py,sha256=xcBcxuu6srqmoS4WqDpkCXnRLB19ctH98v21P8S9kS8,708
33
- limits-4.0.1.dist-info/LICENSE.txt,sha256=T6i7kq7F5gIPfcno9FCxU5Hcwm22Bjq0uHZV3ElcjsQ,1061
34
- limits-4.0.1.dist-info/METADATA,sha256=EzpDuoZfOrB3xjvhv-IHK5Q6-R_6-vH5FnUEoJ-ohdI,7662
35
- limits-4.0.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
36
- limits-4.0.1.dist-info/top_level.txt,sha256=C7g5ahldPoU2s6iWTaJayUrbGmPK1d6e9t5Nn0vQ2jM,7
37
- limits-4.0.1.dist-info/RECORD,,