aboutsummaryrefslogtreecommitdiff
path: root/pandora/ratelimit.py
diff options
context:
space:
mode:
Diffstat (limited to 'pandora/ratelimit.py')
-rw-r--r--pandora/ratelimit.py164
1 files changed, 164 insertions, 0 deletions
diff --git a/pandora/ratelimit.py b/pandora/ratelimit.py
new file mode 100644
index 0000000..427ede9
--- /dev/null
+++ b/pandora/ratelimit.py
@@ -0,0 +1,164 @@
1"""
2Pandora Rate Limiter
3"""
4import time
5import warnings
6
7from .errors import PandoraException
8
9
10class RateLimitExceeded(PandoraException):
11 """Exception thrown when rate limit is exceeded
12 """
13
14 code = 0
15 message = "Rate Limit Exceeded"
16
17
18class TokenBucketCallbacks(object):
19 """Interface for TokenBucket Callbacks
20
21 These methods get called by the token bucket during certain parts of the
22 bucket lifecycle.
23 """
24
25 def near_depletion(self, tokens_left):
26 """Bucket near depletion callback
27
28 This callback is called when the token bucket is nearly depleted, as
29 defined by the depletion_level member of the TokenBucket class. This
30 method provides a hook for clients to warn when the token bucket is
31 nearly consumed. The return value of this method is ignored.
32 """
33 return
34
35 def depleted(self, tokens_wanted, tokens_left):
36 """Bucket depletion callback
37
38 This callback is called when the token bucket has been depleted.
39 Returns a boolean indicating if the bucket should call its default
40 depletion handler. If this method returns False the token bucket will
41 not return any tokens but will also not call the default token
42 depletion handler.
43
44 This method is useful if clients want to customize depletion behavior.
45 """
46 return True
47
48 def consumed(self, tokens_wanted, tokens_left):
49 """Bucket consumption callback
50
51 This callback is called when a client successfully consumes tokens from
52 a bucket. The return value of this method is ignored.
53 """
54 return
55
56
57class TokenBucket(object):
58 def __init__(self, capacity, refill_tokens, refill_rate, callbacks=None):
59 """Initialize a rate limiter
60
61 capacity
62 the number of tokens the bucket holds when completely full
63 refill_tokens
64 number of tokens to add to the bucket during each refill cycle
65 refill_rate
66 the number of seconds between token refills
67 """
68 self.capacity = capacity
69 self.refill_tokens = refill_tokens
70 self.refill_rate = refill_rate
71 self.callbacks = callbacks or TokenBucketCallbacks()
72
73 # Depletion level at which the near_depletion callback is called.
74 # Defaults to 20% of the bucket available. Not exposed in the
75 # initializer because this should generally be good enough.
76 self.depletion_level = self.capacity / 5
77
78 self._available_tokens = capacity
79 self._last_refill = time.time()
80
81 @classmethod
82 def creator(cls, callbacks=None):
83 """Returns a TokenBucket creator
84
85 This method is used when clients want to customize the callbacks but
86 defer class construction to the real consumer.
87 """
88
89 def constructor(capacity, refill_tokens, refill_rate):
90 return cls(capacity, refill_tokens, refill_rate, callbacks)
91
92 return constructor
93
94 def _replentish(self):
95 now = time.time()
96
97 refill_unit = round((now - self._last_refill) / self.refill_rate)
98 if refill_unit > 0:
99 self._last_refill = now
100 self._available_tokens = min(
101 self.capacity, refill_unit * self.refill_tokens
102 )
103
104 def _insufficient_tokens(self, tokens_wanted):
105 return
106
107 def _sufficient_tokens(self, tokens_wanted):
108 return
109
110 def consume(self, tokens=1):
111 """Consume a number of tokens from the bucket
112
113 May return a boolean indicating that sufficient tokens were available
114 for consumption. Implementations may have different behaviour when the
115 bucket is empty. This method may block or throw.
116 """
117 self._replentish()
118
119 if self._available_tokens >= tokens:
120 self._available_tokens -= tokens
121
122 if self._available_tokens <= self.depletion_level:
123 self.callbacks.near_depletion(self._available_tokens)
124
125 self.callbacks.consumed(tokens, self._available_tokens)
126 self._sufficient_tokens(tokens)
127
128 return True
129 else:
130 if self.callbacks.depleted(tokens, self._available_tokens):
131 self._insufficient_tokens(tokens)
132 return False
133
134
135class BlockingTokenBucket(TokenBucket):
136 """Token bucket that blocks on exhaustion
137 """
138
139 def _insufficient_tokens(self, tokens_wanted):
140 tokens_per_second = self.refill_rate / self.refill_tokens
141 excess_tokens = tokens_wanted - self._available_tokens
142 time.sleep(tokens_per_second * excess_tokens)
143
144
145class ThrowingTokenBucket(TokenBucket):
146 """Token bucket that throws on exhaustion
147 """
148
149 def _insufficient_tokens(self, tokens_wanted):
150 raise RateLimitExceeded("Unable to acquire enough tokens")
151
152
153class WarningTokenBucket(TokenBucket):
154 """Token bucket that warns on exhaustion
155
156 This token bucket doesn't enforce the rate limit and is designed to be a
157 softer policy for backwards compatability with code that was written before
158 rate limiting was added.
159 """
160
161 def _insufficient_tokens(self, tokens_wanted):
162 warnings.warn(
163 "Pandora API rate limit exceeded!", warnings.ResourceWarning
164 )