steindamm 0.7.0__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.
- steindamm-0.7.0/PKG-INFO +385 -0
- steindamm-0.7.0/README.md +354 -0
- steindamm-0.7.0/pyproject.toml +124 -0
- steindamm-0.7.0/src/steindamm/__init__.py +28 -0
- steindamm-0.7.0/src/steindamm/base.py +49 -0
- steindamm-0.7.0/src/steindamm/exceptions.py +7 -0
- steindamm-0.7.0/src/steindamm/py.typed +0 -0
- steindamm-0.7.0/src/steindamm/semaphore.lua +45 -0
- steindamm-0.7.0/src/steindamm/semaphore.py +127 -0
- steindamm-0.7.0/src/steindamm/token_bucket/local_token_bucket.py +118 -0
- steindamm-0.7.0/src/steindamm/token_bucket/redis_token_bucket.py +137 -0
- steindamm-0.7.0/src/steindamm/token_bucket/token_bucket.lua +89 -0
- steindamm-0.7.0/src/steindamm/token_bucket/token_bucket.py +208 -0
- steindamm-0.7.0/src/steindamm/token_bucket/token_bucket_base.py +161 -0
steindamm-0.7.0/PKG-INFO
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steindamm
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Python rate limiters backed by Redis
|
|
5
|
+
Keywords: redis,async,sync,rate,limiting,limiters
|
|
6
|
+
Author: Emil Aliyev, Sondre Lillebø Gundersen
|
|
7
|
+
Author-email: Emil Aliyev <ea01052002@gmail.com>, Sondre Lillebø Gundersen <sondrelg@live.no>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Dist: pydantic
|
|
25
|
+
Requires-Dist: redis ; extra == 'redis'
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Project-URL: Homepage, https://github.com/Feuerstein-Org/redis-limiters
|
|
28
|
+
Project-URL: Repository, https://github.com/Feuerstein-Org/redis-limiters
|
|
29
|
+
Provides-Extra: redis
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Python Redis Limiters
|
|
33
|
+
|
|
34
|
+
A library which regulates traffic, with respect to concurrency or time.
|
|
35
|
+
It implements sync and async context managers for a [semaphore](#semaphore)- and a [token bucket](#token-bucket)-implementation.
|
|
36
|
+
|
|
37
|
+
The rate limiters can be distributed using Redis, or run locally in-memory for single-process applications.
|
|
38
|
+
|
|
39
|
+
**Redis-based limiters** leverage Lua scripts on Redis, each operation is fully atomic.
|
|
40
|
+
Both standalone Redis instances and clusters are supported with sync and async interfaces.
|
|
41
|
+
|
|
42
|
+
**Local limiters** provide thread-safe (for sync) and asyncio-safe (for async) in-memory rate limiting without requiring Redis.
|
|
43
|
+
|
|
44
|
+
We currently support Python 3.11, 3.12, and 3.13.
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
### Deployment Options
|
|
49
|
+
- **Local (In-Memory)**: Thread-safe and asyncio-safe rate limiting without Redis dependencies
|
|
50
|
+
- Perfect for single-process applications, testing, and development
|
|
51
|
+
- Zero external dependencies required
|
|
52
|
+
- **Redis-Based**: Distributed rate limiting using Redis with Lua scripts
|
|
53
|
+
- Atomic operations for accuracy in distributed systems
|
|
54
|
+
- Supports both standalone Redis and Redis Cluster
|
|
55
|
+
- Scales across multiple processes and servers
|
|
56
|
+
|
|
57
|
+
### Async & Sync Support
|
|
58
|
+
- Full support for both synchronous and asynchronous code
|
|
59
|
+
- Context manager interface (`with` / `async with`)
|
|
60
|
+
|
|
61
|
+
### Flexibility & Control
|
|
62
|
+
- **Factory Classes**: `SyncTokenBucket` and `AsyncTokenBucket` automatically choose implementation based on connection
|
|
63
|
+
- **Explicit Classes**: Direct access to `SyncRedisTokenBucket`, `AsyncRedisTokenBucket`, `SyncLocalTokenBucket`, `AsyncLocalTokenBucket`
|
|
64
|
+
- **Configurable Token Consumption**: `tokens_to_consume` parameter for variable-cost operations
|
|
65
|
+
- **Customizable Behavior**: Control capacity, refill rates, expiry, max sleep time, and initial state
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
### Basic Installation (Local limiters only)
|
|
70
|
+
```bash
|
|
71
|
+
pip install redis-limiters
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### With Redis Support
|
|
75
|
+
```bash
|
|
76
|
+
pip install redis-limiters[redis]
|
|
77
|
+
```
|
|
78
|
+
Or install Redis separately:
|
|
79
|
+
```bash
|
|
80
|
+
pip install redis-limiters redis
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
### Token bucket
|
|
86
|
+
|
|
87
|
+
The `TokenBucket` classes are useful if you're working with time-based
|
|
88
|
+
rate limits. Say, you are allowed 100 requests per minute, for a given API token.
|
|
89
|
+
|
|
90
|
+
If the `max_sleep` limit is exceeded, a `MaxSleepExceededError` is raised. Setting `max_sleep` to 0.0 will sleep "endlessly" - this is also the default value. On the other hand `expiry` is how long the token bucket will persist in Redis without any activity (acquires or releases). You might need to adjust both to your requirements.
|
|
91
|
+
|
|
92
|
+
#### Using Local (In-Memory) Token Bucket
|
|
93
|
+
|
|
94
|
+
The local token bucket doesn't require Redis and runs entirely in-memory.
|
|
95
|
+
Perfect for single-process applications or testing.
|
|
96
|
+
|
|
97
|
+
> **Note:** The local token bucket implementation does not currently support expiry of bucket state. Buckets persist in memory for the lifetime of the process. If you are creating buckets dynamically (e.g., one bucket per user or per API key), this could lead to unbounded memory growth. Consider using the Redis-based implementation for applications with dynamic bucket creation, or ensure buckets are reused for the same resources.
|
|
98
|
+
|
|
99
|
+
**Async version:**
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import asyncio
|
|
103
|
+
|
|
104
|
+
from httpx import AsyncClient
|
|
105
|
+
|
|
106
|
+
from redis_limiters import AsyncTokenBucket
|
|
107
|
+
|
|
108
|
+
# No Redis connection needed - runs in-memory
|
|
109
|
+
limiter = AsyncTokenBucket(
|
|
110
|
+
name="foo", # name of the resource you are limiting traffic for
|
|
111
|
+
capacity=5, # hold up to 5 tokens (default: 5.0)
|
|
112
|
+
refill_frequency=1, # add tokens every second (default: 1.0)
|
|
113
|
+
refill_amount=1, # add 1 token when refilling (default: 1.0)
|
|
114
|
+
initial_tokens=None, # start with full capacity (default: None, which uses capacity)
|
|
115
|
+
max_sleep=30, # raise an error if there are no free tokens for X seconds, 0 never expires (default: 30.0)
|
|
116
|
+
tokens_to_consume=1, # consume 1 token per request (default: 1.0)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
async def get_foo():
|
|
120
|
+
async with AsyncClient() as client:
|
|
121
|
+
async with limiter:
|
|
122
|
+
await client.get(...)
|
|
123
|
+
|
|
124
|
+
async def main():
|
|
125
|
+
await asyncio.gather(
|
|
126
|
+
get_foo() for i in range(100)
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Sync version:**
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import requests
|
|
134
|
+
|
|
135
|
+
from redis_limiters import SyncTokenBucket
|
|
136
|
+
|
|
137
|
+
limiter = SyncTokenBucket(
|
|
138
|
+
name="foo",
|
|
139
|
+
capacity=5,
|
|
140
|
+
refill_frequency=1,
|
|
141
|
+
refill_amount=1,
|
|
142
|
+
tokens_to_consume=1,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def main():
|
|
146
|
+
with limiter:
|
|
147
|
+
requests.get(...)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### Using Redis-Based Token Bucket
|
|
151
|
+
|
|
152
|
+
For distributed rate limiting across multiple processes or servers,
|
|
153
|
+
use the Redis-based implementation by providing a `connection` parameter.
|
|
154
|
+
|
|
155
|
+
**Async version:**
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import asyncio
|
|
159
|
+
|
|
160
|
+
from httpx import AsyncClient
|
|
161
|
+
from redis.asyncio import Redis
|
|
162
|
+
|
|
163
|
+
from redis_limiters import AsyncTokenBucket
|
|
164
|
+
|
|
165
|
+
# With Redis connection - distributed across processes/servers
|
|
166
|
+
limiter = AsyncTokenBucket(
|
|
167
|
+
connection=Redis.from_url("redis://localhost:6379"), # Add Redis connection for distributed limiting
|
|
168
|
+
name="foo",
|
|
169
|
+
capacity=5,
|
|
170
|
+
refill_frequency=1,
|
|
171
|
+
refill_amount=1,
|
|
172
|
+
max_sleep=30,
|
|
173
|
+
expiry=60, # set expiry on Redis keys in seconds (default: 60)
|
|
174
|
+
tokens_to_consume=1,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def get_foo():
|
|
178
|
+
async with AsyncClient() as client:
|
|
179
|
+
async with limiter:
|
|
180
|
+
await client.get(...)
|
|
181
|
+
|
|
182
|
+
async def main():
|
|
183
|
+
await asyncio.gather(
|
|
184
|
+
get_foo() for i in range(100)
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Sync version:**
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import requests
|
|
192
|
+
from redis import Redis
|
|
193
|
+
|
|
194
|
+
from redis_limiters import SyncTokenBucket
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
limiter = SyncTokenBucket(
|
|
198
|
+
connection=Redis.from_url("redis://localhost:6379"),
|
|
199
|
+
name="foo",
|
|
200
|
+
capacity=5,
|
|
201
|
+
refill_frequency=1,
|
|
202
|
+
refill_amount=1,
|
|
203
|
+
max_sleep=30,
|
|
204
|
+
expiry=60,
|
|
205
|
+
tokens_to_consume=1,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def main():
|
|
209
|
+
with limiter:
|
|
210
|
+
requests.get(...)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Semaphore
|
|
214
|
+
|
|
215
|
+
The semaphore classes are useful when you have concurrency restrictions;
|
|
216
|
+
e.g., say you're allowed 5 active requests at the time for a given API token.
|
|
217
|
+
|
|
218
|
+
**Note:** Currently, only Redis-based semaphores are available. Local (in-memory) semaphore implementation is planned for a future release.
|
|
219
|
+
|
|
220
|
+
Beware that the client will block until the Semaphore is acquired,
|
|
221
|
+
or the `max_sleep` limit is exceeded. If the `max_sleep` limit is exceeded, a `MaxSleepExceededError` is raised. Setting `max_sleep` to 0.0 will sleep "endlessly" - default is 30 seconds. On the other hand `expiry` is how long the semaphore will persist in Redis without any activity (acquires or releases). You might need to adjust both to your requirements.
|
|
222
|
+
|
|
223
|
+
Here's how you might use the async version:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
import asyncio
|
|
227
|
+
|
|
228
|
+
from httpx import AsyncClient
|
|
229
|
+
from redis.asyncio import Redis
|
|
230
|
+
|
|
231
|
+
from redis_limiters import AsyncSemaphore
|
|
232
|
+
|
|
233
|
+
# All properties have defaults except name and connection
|
|
234
|
+
limiter = AsyncSemaphore(
|
|
235
|
+
connection=Redis.from_url("redis://localhost:6379"),
|
|
236
|
+
name="foo", # name of the resource you are limiting traffic for
|
|
237
|
+
capacity=5, # allow 5 concurrent requests (default: 5)
|
|
238
|
+
max_sleep=30, # raise an error if it takes longer than 30 seconds to acquire the semaphore (default: 30.0)
|
|
239
|
+
expiry=60, # set expiry on the semaphore keys in Redis to prevent deadlocks (default: 60)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def get_foo():
|
|
243
|
+
async with AsyncClient() as client:
|
|
244
|
+
async with limiter:
|
|
245
|
+
await client.get(...)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def main():
|
|
249
|
+
await asyncio.gather(
|
|
250
|
+
get_foo() for i in range(100)
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
and here is how you might use the sync version:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
import requests
|
|
258
|
+
from redis import Redis
|
|
259
|
+
|
|
260
|
+
from redis_limiters import SyncSemaphore
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
limiter = SyncSemaphore(
|
|
264
|
+
connection=Redis.from_url("redis://localhost:6379"),
|
|
265
|
+
name="foo",
|
|
266
|
+
capacity=5,
|
|
267
|
+
max_sleep=30,
|
|
268
|
+
expiry=60,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def main():
|
|
272
|
+
with limiter:
|
|
273
|
+
requests.get(...)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### Using Explicit Implementation Classes
|
|
277
|
+
|
|
278
|
+
For explicit control over which implementation to use, import the specific classes:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
# Local implementations
|
|
282
|
+
from redis_limiters import SyncLocalTokenBucket, AsyncLocalTokenBucket
|
|
283
|
+
|
|
284
|
+
# Redis implementations
|
|
285
|
+
from redis_limiters import SyncRedisTokenBucket, AsyncRedisTokenBucket
|
|
286
|
+
|
|
287
|
+
# Use directly without factory logic
|
|
288
|
+
local_limiter = SyncLocalTokenBucket(name="api", capacity=10)
|
|
289
|
+
redis_limiter = SyncRedisTokenBucket(connection=redis_conn, name="api", capacity=10)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Consuming Multiple Tokens Per Request
|
|
293
|
+
|
|
294
|
+
You can control how many tokens are consumed per operation using the `tokens_to_consume` parameter.
|
|
295
|
+
This is useful when different operations _on the same api_ have different "costs". Note, how the "name" aka is the same between the limiters which will cause the tokens to be shared.
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from redis_limiters import SyncTokenBucket
|
|
299
|
+
|
|
300
|
+
# Small requests consume 1 token
|
|
301
|
+
small_limiter = SyncTokenBucket(name="api", capacity=100, tokens_to_consume=1)
|
|
302
|
+
|
|
303
|
+
# Large requests consume 5 tokens
|
|
304
|
+
large_limiter = SyncTokenBucket(name="api", capacity=100, tokens_to_consume=5)
|
|
305
|
+
|
|
306
|
+
with small_limiter:
|
|
307
|
+
make_small_request() # Consumes 1 token
|
|
308
|
+
|
|
309
|
+
with large_limiter:
|
|
310
|
+
make_large_request() # Consumes 5 tokens
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Using them as a decorator
|
|
314
|
+
|
|
315
|
+
We don't ship decorators in the package, but if you would
|
|
316
|
+
like to limit the rate at which a whole function is run,
|
|
317
|
+
you can create your own, like this:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
from redis_limiters import AsyncSemaphore
|
|
321
|
+
from redis.asyncio import Redis
|
|
322
|
+
|
|
323
|
+
# Define a decorator function
|
|
324
|
+
def limit(name, capacity, connection):
|
|
325
|
+
def middle(f):
|
|
326
|
+
async def inner(*args, **kwargs):
|
|
327
|
+
async with AsyncSemaphore(connection=connection, name=name, capacity=capacity):
|
|
328
|
+
return await f(*args, **kwargs)
|
|
329
|
+
return inner
|
|
330
|
+
return middle
|
|
331
|
+
|
|
332
|
+
# Or for local token buckets (no Redis needed)
|
|
333
|
+
from redis_limiters import AsyncTokenBucket
|
|
334
|
+
|
|
335
|
+
def rate_limit(name, capacity):
|
|
336
|
+
def middle(f):
|
|
337
|
+
async def inner(*args, **kwargs):
|
|
338
|
+
async with AsyncTokenBucket(name=name, capacity=capacity):
|
|
339
|
+
return await f(*args, **kwargs)
|
|
340
|
+
return inner
|
|
341
|
+
return middle
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# Then pass the relevant limiter arguments like this
|
|
345
|
+
@limit(name="foo", capacity=5, connection=Redis.from_url("redis://localhost:6379"))
|
|
346
|
+
async def fetch_foo(id: UUID) -> Foo:
|
|
347
|
+
...
|
|
348
|
+
|
|
349
|
+
# Or with local rate limiting
|
|
350
|
+
@rate_limit(name="bar", capacity=10)
|
|
351
|
+
async def fetch_bar(id: UUID) -> Bar:
|
|
352
|
+
...
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Contributing
|
|
356
|
+
|
|
357
|
+
Contributions are very welcome. Here's how to get started:
|
|
358
|
+
|
|
359
|
+
- Clone the repo
|
|
360
|
+
- Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and [mise](https://mise.jdx.dev/)
|
|
361
|
+
- Run `mise run install` to install dependencies
|
|
362
|
+
If you prefer not to install mise, check the `mise.toml` file and
|
|
363
|
+
run the commands manually.
|
|
364
|
+
- Make your code changes, with tests
|
|
365
|
+
- Run tests with `mise run test` or `uv run pytest`
|
|
366
|
+
Note that you will need to first spin up the redis docker containers. This can be done with `mise run test-setup` (and shut down with `mise run test-teardown`) or the full cycle can be run with `mise run test-full`.
|
|
367
|
+
- Commit your changes and open a PR
|
|
368
|
+
|
|
369
|
+
## Publishing a new version
|
|
370
|
+
|
|
371
|
+
To publish a new version:
|
|
372
|
+
|
|
373
|
+
- Update the package version in the `pyproject.toml`
|
|
374
|
+
- Open [Github releases](https://github.com/Feuerstein-Org/redis-limiters/releases)
|
|
375
|
+
- Press "Draft a new release"
|
|
376
|
+
- Set a tag matching the new version (for example, `v0.8.0`)
|
|
377
|
+
- Set the title matching the tag
|
|
378
|
+
- Add some release notes, explaining what has changed
|
|
379
|
+
- Publish
|
|
380
|
+
|
|
381
|
+
Once the release is published, our [publish workflow](https://github.com/Feuerstein-Org/redis-limiters/blob/main/.github/workflows/publish.yaml) should be triggered
|
|
382
|
+
to push the new version to PyPI.
|
|
383
|
+
|
|
384
|
+
## Acknowledgment:
|
|
385
|
+
This project was initially forked from [redis-rate-limiters](https://github.com/otovo/redis-rate-limiters) and was mainly created by Sondre Lillebø Gundersen [link](https://github.com/sondrelg). It was no longer maintained and I since rewrote a lot of stuff as well as added a local version of the limiters and new functionality like the initial amount of tokens or how many tokens to consume at once.
|