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.
@@ -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.