limit_context_decorator.py 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. from functools import wraps
  4. from logging import getLogger
  5. from time import sleep
  6. from .exceptions import BucketFullException
  7. logger = getLogger(__name__)
  8. class LimitContextDecorator(object):
  9. """A class that can be used as a:
  10. * decorator
  11. * async decorator
  12. * contextmanager
  13. * async contextmanager
  14. Mainly used via ``Limiter.ratelimit()``. Depending on arguments, calls that exceed the rate
  15. limit will either raise an exception, or sleep until space is available in the bucket.
  16. Args:
  17. limiter: Limiter object
  18. identities: Bucket identities
  19. delay: Delay until the next request instead of raising an exception
  20. max_delay: The maximum allowed delay time (in seconds); anything over this will raise
  21. an exception
  22. """
  23. def __init__(
  24. self,
  25. limiter,
  26. delay = False,
  27. max_delay = None,
  28. *identities
  29. ):
  30. self.delay = delay
  31. self.max_delay = max_delay
  32. self.try_acquire = lambda: limiter.try_acquire(*identities)
  33. def __call__(self, func):
  34. """Allows usage as a decorator for both normal and async functions"""
  35. @wraps(func)
  36. def wrapper(*args, **kwargs):
  37. self.delayed_acquire()
  38. return func(*args, **kwargs)
  39. return wrapper
  40. def __enter__(self):
  41. """Allows usage as a contextmanager"""
  42. self.delayed_acquire()
  43. def __exit__(self, *exc):
  44. pass
  45. def delayed_acquire(self):
  46. """Delay and retry until we can successfully acquire an available bucket item"""
  47. while True:
  48. try:
  49. self.try_acquire()
  50. except BucketFullException as err:
  51. delay_time = self.delay_or_reraise(err)
  52. sleep(delay_time)
  53. else:
  54. break
  55. def delay_or_reraise(self, err):
  56. """Determine if we should delay after exceeding a rate limit. If so, return the delay time,
  57. otherwise re-raise the exception.
  58. """
  59. delay_time = err.meta_info["remaining_time"]
  60. logger.debug(err.meta_info)
  61. logger.info("Rate limit reached; % seconds remaining before next request", delay_time)
  62. exceeded_max_delay = bool(self.max_delay) and (delay_time > self.max_delay)
  63. if self.delay and not exceeded_max_delay:
  64. return delay_time
  65. raise err