token-bucket 0.4.0__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.
@@ -0,0 +1,18 @@
1
+ """token_bucket package."""
2
+
3
+ # NOTE(kgriffs): The following imports are to be used by consumers of
4
+ # the token_bucket package; modules within the package itself should
5
+ # not use this "front-door" module, but rather import using the
6
+ # fully-qualified paths.
7
+
8
+ from .limiter import Limiter
9
+ from .storage import MemoryStorage
10
+ from .storage_base import StorageBase
11
+ from .version import __version__
12
+
13
+ __all__ = (
14
+ 'Limiter',
15
+ 'MemoryStorage',
16
+ 'StorageBase',
17
+ '__version__',
18
+ )
@@ -0,0 +1,129 @@
1
+ # Copyright 2016 by Rackspace Hosting, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .storage_base import StorageBase
16
+
17
+
18
+ class Limiter(object):
19
+ """Limits demand for a finite resource via keyed token buckets.
20
+
21
+ A limiter manages a set of token buckets that have an identical
22
+ rate, capacity, and storage backend. Each bucket is referenced
23
+ by a key, allowing for the independent tracking and limiting
24
+ of multiple consumers of a resource.
25
+
26
+ Args:
27
+ rate (float): Number of tokens per second to add to the
28
+ bucket. Over time, the number of tokens that can be
29
+ consumed is limited by this rate. Each token represents
30
+ some percentage of a finite resource that may be
31
+ utilized by a consumer.
32
+ capacity (int): Maximum number of tokens that the bucket
33
+ can hold. Once the bucket is full, additional tokens
34
+ are discarded.
35
+
36
+ The bucket capacity has a direct impact on burst duration.
37
+ Let M be the maximum possible token request rate, r the
38
+ token generation rate (tokens/sec), and b the bucket
39
+ capacity.
40
+
41
+ If r < M the maximum burst duration, in seconds, is:
42
+
43
+ T = b / (M - r)
44
+
45
+ Otherwise, if r >= M, it is not possible to exceed the
46
+ replenishment rate, and therefore a consumer can burst
47
+ at full speed indefinitely.
48
+
49
+ The maximum number of tokens that any one burst may
50
+ consume is:
51
+
52
+ T * M
53
+
54
+ See also: https://en.wikipedia.org/wiki/Token_bucket#Burst_size
55
+ storage (token_bucket.StorageBase): A storage engine to use for
56
+ persisting the token bucket data. The following engines are
57
+ available out of the box:
58
+
59
+ token_bucket.MemoryStorage
60
+ """
61
+
62
+ __slots__ = (
63
+ '_rate',
64
+ '_capacity',
65
+ '_storage',
66
+ )
67
+
68
+ def __init__(self, rate: float, capacity: int, storage: StorageBase) -> None:
69
+ if not isinstance(rate, (float, int)):
70
+ raise TypeError('rate must be an int or float')
71
+
72
+ if rate <= 0:
73
+ raise ValueError('rate must be > 0')
74
+
75
+ if not isinstance(capacity, int):
76
+ raise TypeError('capacity must be an int')
77
+
78
+ if capacity < 1:
79
+ raise ValueError('capacity must be >= 1')
80
+
81
+ if not isinstance(storage, StorageBase):
82
+ raise TypeError('storage must be a subclass of StorageBase')
83
+
84
+ self._rate = rate
85
+ self._capacity = capacity
86
+ self._storage = storage
87
+
88
+ def consume(self, key: str | bytes, num_tokens: int = 1) -> bool:
89
+ """Attempt to take one or more tokens from a bucket.
90
+
91
+ If the specified token bucket does not yet exist, it will be
92
+ created and initialized to full capacity before proceeding.
93
+
94
+ Args:
95
+ key (bytes): A string or bytes object that specifies the
96
+ token bucket to consume from. If a global limit is
97
+ desired for all consumers, the same key may be used
98
+ for every call to consume(). Otherwise, a key based on
99
+ consumer identity may be used to segregate limits.
100
+ Keyword Args:
101
+ num_tokens (int): The number of tokens to attempt to
102
+ consume, defaulting to 1 if not specified. It may
103
+ be appropriate to ask for more than one token according
104
+ to the proportion of the resource that a given request
105
+ will use, relative to other requests for the same
106
+ resource.
107
+
108
+ Returns:
109
+ bool: True if the requested number of tokens were removed
110
+ from the bucket (conforming), otherwise False (non-
111
+ conforming). The entire number of tokens requested must
112
+ be available in the bucket to be conforming. Otherwise,
113
+ no tokens will be removed (it's all or nothing).
114
+ """
115
+
116
+ if not key:
117
+ if key is None:
118
+ raise TypeError('key may not be None')
119
+
120
+ raise ValueError('key must not be a non-empty string or bytestring')
121
+
122
+ if num_tokens is None:
123
+ raise TypeError('num_tokens may not be None')
124
+
125
+ if num_tokens < 1:
126
+ raise ValueError('num_tokens must be >= 1')
127
+
128
+ self._storage.replenish(key, self._rate, self._capacity)
129
+ return self._storage.consume(key, num_tokens)
token_bucket/py.typed ADDED
File without changes
@@ -0,0 +1,179 @@
1
+ # Copyright 2016 by Rackspace Hosting, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import time
16
+
17
+ from .storage_base import StorageBase
18
+
19
+
20
+ class MemoryStorage(StorageBase):
21
+ """In-memory token bucket storage engine.
22
+
23
+ This storage engine is suitable for multi-threaded applications. For
24
+ performance reasons, race conditions are mitigated but not completely
25
+ eliminated. The remaining effects have the result of reducing the
26
+ effective bucket capacity by a negligible amount. In practice this
27
+ won't be noticeable for the vast majority of applications, but in
28
+ the case that it is, the situation can be remedied by simply
29
+ increasing the bucket capacity by a few tokens.
30
+ """
31
+
32
+ def __init__(self) -> None:
33
+ # NOTE(vytas): Each bucket is a list of two items: the current
34
+ # token count and the timestamp of the last replenishment.
35
+ self._buckets: dict[str | bytes, list[float]] = {}
36
+
37
+ def get_token_count(self, key: str | bytes) -> float:
38
+ """Query the current token count for the given bucket.
39
+
40
+ Note that the bucket is not replenished first, so the count
41
+ will be what it was the last time replenish() was called.
42
+
43
+ Args:
44
+ key (str): Name of the bucket to query.
45
+
46
+ Returns:
47
+ float: Number of tokens currently in the bucket (may be
48
+ fractional).
49
+ """
50
+ try:
51
+ return self._buckets[key][0]
52
+ except KeyError:
53
+ pass
54
+
55
+ return 0
56
+
57
+ def replenish(self, key: str | bytes, rate: float, capacity: int) -> None:
58
+ """Add tokens to a bucket per the given rate.
59
+
60
+ This method is exposed for use by the token_bucket.Limiter
61
+ class.
62
+ """
63
+
64
+ try:
65
+ # NOTE(kgriffs): Correctness of this algorithm assumes
66
+ # that the calculation of the current time is performed
67
+ # in the same order as the updates based on that
68
+ # timestamp, across all threads. If an older "now"
69
+ # completes before a newer "now", the lower token
70
+ # count will overwrite the newer, effectively reducing
71
+ # the bucket's capacity temporarily, by a minor amount.
72
+ #
73
+ # While a lock could be used to fix this race condition,
74
+ # one isn't used here for the following reasons:
75
+ #
76
+ # 1. The condition above will rarely occur, since
77
+ # the window of opportunity is quite small and
78
+ # even so requires many threads contending for a
79
+ # relatively small number of bucket keys.
80
+ # 2. When the condition does occur, the difference
81
+ # in timestamps will be quite small, resulting in
82
+ # a negligible loss in tokens.
83
+ # 3. Depending on the order in which instructions
84
+ # are interleaved between threads, the condition
85
+ # can be detected and mitigated by comparing
86
+ # timestamps. This mitigation is implemented below,
87
+ # and serves to further minimize the effect of this
88
+ # race condition to negligible levels.
89
+ # 4. While locking introduces only a small amount of
90
+ # overhead (less than a microsecond), there's no
91
+ # reason to waste those CPU cycles in light of the
92
+ # points above.
93
+ # 5. If a lock were used, it would only be held for
94
+ # a microsecond or less. We are unlikely to see
95
+ # much contention for the lock during such a short
96
+ # time window, but we might as well remove the
97
+ # possibility in light of the points above.
98
+
99
+ tokens_in_bucket, last_replenished_at = self._buckets[key]
100
+
101
+ now = time.monotonic()
102
+
103
+ # NOTE(kgriffs): This will detect many, but not all,
104
+ # manifestations of the race condition. If a later
105
+ # timestamp was already used to update the bucket, don't
106
+ # regress by setting the token count to a smaller number.
107
+ if now < last_replenished_at: # pragma: no cover
108
+ return
109
+
110
+ self._buckets[key] = [
111
+ # Limit to capacity
112
+ min(
113
+ capacity,
114
+ # NOTE(kgriffs): The new value is the current number
115
+ # of tokens in the bucket plus the number of
116
+ # tokens generated since last time. Fractional
117
+ # tokens are permitted in order to improve
118
+ # accuracy (now is a float, and rate may be also).
119
+ tokens_in_bucket + (rate * (now - last_replenished_at)),
120
+ ),
121
+ # Update the timestamp for use next time
122
+ now,
123
+ ]
124
+
125
+ except KeyError:
126
+ self._buckets[key] = [capacity, time.monotonic()]
127
+
128
+ def consume(self, key: str | bytes, num_tokens: int) -> bool:
129
+ """Attempt to take one or more tokens from a bucket.
130
+
131
+ This method is exposed for use by the token_bucket.Limiter
132
+ class.
133
+ """
134
+
135
+ # NOTE(kgriffs): Assume that the key will be present, since
136
+ # replenish() will always be called before consume().
137
+ tokens_in_bucket = self._buckets[key][0]
138
+ if tokens_in_bucket < num_tokens:
139
+ return False
140
+
141
+ # NOTE(kgriffs): In a multi-threaded application, it is
142
+ # possible for two threads to interleave such that they
143
+ # both pass the check above, while in reality if executed
144
+ # linearly, the second thread would not pass the check
145
+ # since the first thread was able to consume the remaining
146
+ # tokens in the bucket.
147
+ #
148
+ # When this race condition occurs, the count in the bucket
149
+ # will go negative, effectively resulting in a slight
150
+ # reduction in capacity.
151
+ #
152
+ # While a lock could be used to fix this race condition,
153
+ # one isn't used here for the following reasons:
154
+ #
155
+ # 1. The condition above will rarely occur, since
156
+ # the window of opportunity is quite small.
157
+ # 2. When the condition does occur, the tokens will
158
+ # usually be quickly replenished since the rate tends
159
+ # to be much larger relative to the number of tokens
160
+ # that are consumed by any one request, and due to (1)
161
+ # the condition is very rarely likely to happen
162
+ # multiple times in a row.
163
+ # 3. In the case of bursting across a large number of
164
+ # threads, the likelihood for this race condition
165
+ # will increase. Even so, the burst will be quickly
166
+ # negated as requests become non-conforming, allowing
167
+ # the bucket to be replenished.
168
+ # 4. While locking introduces only a small amount of
169
+ # overhead (less than a microsecond), there's no
170
+ # reason to waste those CPU cycles in light of the
171
+ # points above.
172
+ # 5. If a lock were used, it would only be held for
173
+ # less than a microsecond. We are unlikely to see
174
+ # much contention for the lock during such a short
175
+ # time window, but we might as well remove the
176
+ # possibility given the points above.
177
+
178
+ self._buckets[key][0] -= num_tokens
179
+ return True
@@ -0,0 +1,69 @@
1
+ # Copyright 2016 by Rackspace Hosting, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import abc
16
+
17
+
18
+ class StorageBase(abc.ABC):
19
+ @abc.abstractmethod
20
+ def get_token_count(self, key: str | bytes) -> float:
21
+ """Query the current token count for the given bucket.
22
+
23
+ Note that the bucket is not replenished first, so the count
24
+ will be what it was the last time replenish() was called.
25
+
26
+ Args:
27
+ key (str): Name of the bucket to query.
28
+
29
+ Returns:
30
+ float: Number of tokens currently in the bucket (may be
31
+ fractional).
32
+ """
33
+
34
+ @abc.abstractmethod
35
+ def replenish(self, key: str | bytes, rate: float, capacity: int) -> None:
36
+ """Add tokens to a bucket per the given rate.
37
+
38
+ Conceptually, tokens are added to the bucket at a rate of one
39
+ every 1/rate seconds. To accomplish this without requiring an
40
+ out-of-band timer, ``replenish()`` simply calculates the number
41
+ of tokens that should have been added since the last time the
42
+ bucket was replenished.
43
+
44
+ Args:
45
+ key (str): Name of the bucket to replenish.
46
+ rate (float): Number of tokens per second to add to the
47
+ bucket. Over time, the number of tokens that can be
48
+ consumed is limited by this rate.
49
+ capacity (int): Maximum number of tokens that the bucket
50
+ can hold. Once the bucket if full, additional tokens
51
+ are discarded.
52
+ """
53
+
54
+ @abc.abstractmethod
55
+ def consume(self, key: str | bytes, num_tokens: int) -> bool:
56
+ """Attempt to take one or more tokens from a bucket.
57
+
58
+ Args:
59
+ key (str): Name of the bucket to replenish.
60
+ num_tokens (int): Number of tokens to try to consume from
61
+ the bucket. If the bucket contains fewer than the
62
+ requested number, no tokens are removed (i.e., it's all
63
+ or nothing).
64
+
65
+ Returns:
66
+ bool: True if the requested number of tokens were removed
67
+ from the bucket (conforming), otherwise False (non-
68
+ conforming).
69
+ """
@@ -0,0 +1,4 @@
1
+ """Package version."""
2
+
3
+ __version__ = '0.4.0'
4
+ """Current version of token_bucket."""
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: token_bucket
3
+ Version: 0.4.0
4
+ Summary: Very fast implementation of the token bucket algorithm.
5
+ Author-email: kgriffs <mail@kgriffs.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/falconry/token-bucket
8
+ Keywords: web,http,https,cloud,rate,limiting,token,bucket,throttling
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Natural Language :: English
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Programming Language :: Python :: 3.14
28
+ Classifier: Programming Language :: Python :: 3.15
29
+ Classifier: Typing :: Typed
30
+ Requires-Python: >=3.10
31
+ Description-Content-Type: text/x-rst
32
+ License-File: LICENSE
33
+ Dynamic: license-file
34
+
35
+ |tests| |PyPI| |python-versions| |codecov|
36
+
37
+ A Token Bucket Implementation for Python Web Apps
38
+ =================================================
39
+
40
+ The ``token-bucket`` package provides an implementation of the
41
+ `token bucket algorithm <https://en.wikipedia.org/wiki/Token_bucket>`_
42
+ suitable for use in web applications for shaping or policing request
43
+ rates. This implementation does not require the use of an independent
44
+ timer thread to manage the bucket state.
45
+
46
+ Compared to other rate-limiting algorithms that use a simple counter,
47
+ the token bucket algorithm provides the following advantages:
48
+
49
+ * The thundering herd problem is avoided since bucket capacity is
50
+ replenished gradually, rather than being immediately refilled at the
51
+ beginning of each epoch as is common with simple fixed window
52
+ counters.
53
+ * Burst duration can be explicitly controlled.
54
+
55
+ Moving window algorithms are resistant to bursting, but at the cost of
56
+ additional processing and memory overhead vs. the token bucket
57
+ algorithm which uses a simple, fast counter per key. The latter approach
58
+ does allow for bursting, but only for a controlled duration.
59
+
60
+
61
+ .. |tests| image:: https://github.com/falconry/token-bucket/actions/workflows/tests.yaml/badge.svg
62
+ :target: https://github.com/falconry/token-bucket/actions/workflows/tests.yaml
63
+
64
+ .. |PyPI| image:: https://img.shields.io/pypi/v/token-bucket.svg
65
+ :target: https://pypi.org/project/token-bucket/
66
+
67
+ .. |python-versions| image:: https://img.shields.io/pypi/pyversions/token-bucket.svg
68
+ :target: https://pypi.org/project/token-bucket/
69
+
70
+ .. |codecov| image:: https://codecov.io/gh/falconry/token-bucket/branch/master/graph/badge.svg
71
+ :target: https://codecov.io/gh/falconry/token-bucket
@@ -0,0 +1,11 @@
1
+ token_bucket/__init__.py,sha256=mctCom07M8QG3g-79vDUv5acVvdhz4MBEaItORmoMRw,488
2
+ token_bucket/limiter.py,sha256=w1mfIA3UqggCqoiJJKVOk95UhilyHmu_750wXoTHXX4,4943
3
+ token_bucket/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ token_bucket/storage.py,sha256=IDVKQs-zNAO65o-6B1aFU5MgTw9fIBxV1hDUsKYqUys,8170
5
+ token_bucket/storage_base.py,sha256=tTD6oMYt7snCgvncHsWkrWtan10GfEp4s-kE7jlbeDQ,2661
6
+ token_bucket/version.py,sha256=B8-Xybi3gzZ2G19ugV8xmMyhQG-2Ijy-alDLN3rccXA,85
7
+ token_bucket-0.4.0.dist-info/licenses/LICENSE,sha256=LZOp0uraJMtDEzjUG7onDQn6Aefz6yX_XglmMvb94VE,745
8
+ token_bucket-0.4.0.dist-info/METADATA,sha256=4l6NXMoMV94ZEDLOsgWrf-zBSypSZdZgedpNPg5Z3Ps,3192
9
+ token_bucket-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ token_bucket-0.4.0.dist-info/top_level.txt,sha256=NhXGdH_jH4KpgC9DU6CDq1VoU6q3u1zWQaTnyXE2bIo,13
11
+ token_bucket-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,17 @@
1
+ Copyright by various authors, as noted in the individual source files.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+
15
+ By contributing to this project, you agree to also license your source
16
+ code under the terms of the Apache License, Version 2.0, as described
17
+ above.
@@ -0,0 +1 @@
1
+ token_bucket