throttlekit 0.1.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,2 @@
1
+ from .limiter import TokenBucketRateLimiter
2
+ from .leaky_limiter import LeakyBucketRateLimiter
@@ -0,0 +1,72 @@
1
+ import asyncio
2
+ import logging
3
+ from functools import wraps
4
+ from typing import Callable, Awaitable, Optional, TypeVar, Union
5
+
6
+ try:
7
+ from typing import ParamSpec
8
+ except ImportError:
9
+ from typing_extensions import ParamSpec # type: ignore
10
+
11
+ T = TypeVar("T")
12
+ P = ParamSpec("P")
13
+
14
+ class LeakyBucketRateLimiter:
15
+ def __init__(
16
+ self,
17
+ rate: float = 1.0, # requests per second
18
+ max_queue_size: int = 100,
19
+ logger: Optional[logging.Logger] = None
20
+ ) -> None:
21
+ if rate <= 0:
22
+ raise ValueError("rate must be > 0")
23
+ self.leak_interval = 1.0 / rate # convert to delay between each request
24
+ self.queue: asyncio.Queue[asyncio.Future[None]] = asyncio.Queue(maxsize=max_queue_size)
25
+ self._started = False
26
+ self._logger = logger or logging.getLogger(__name__)
27
+ self._drain_task: Optional[asyncio.Task] = None
28
+
29
+ async def start(self) -> None:
30
+ if self._started:
31
+ return
32
+ self._started = True
33
+ self._drain_task = asyncio.create_task(self._drain_loop())
34
+ self._logger.debug("LeakyBucket started with leak_interval=%s", self.leak_interval)
35
+
36
+ async def _drain_loop(self) -> None:
37
+ while True:
38
+ fut = await self.queue.get()
39
+ if not fut.done():
40
+ fut.set_result(None)
41
+ await asyncio.sleep(self.leak_interval)
42
+
43
+ async def acquire(self) -> None:
44
+ if not self._started:
45
+ raise RuntimeError("LeakyBucket not started. Call await limiter.start() before use.")
46
+ fut: asyncio.Future[None] = asyncio.get_event_loop().create_future()
47
+ await self.queue.put(fut)
48
+ await fut
49
+
50
+ async def __aenter__(self) -> "LeakyBucketRateLimiter":
51
+ await self.acquire()
52
+ return self
53
+
54
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> Optional[bool]:
55
+ return None
56
+
57
+ def limit(
58
+ self,
59
+ fn: Optional[Callable[..., Awaitable[T]]] = None
60
+ ) -> Union[
61
+ Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]],
62
+ Callable[..., Awaitable[T]]
63
+ ]:
64
+
65
+ def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
66
+ @wraps(func)
67
+ async def wrapper(*args, **kwargs) -> T:
68
+ await self.acquire()
69
+ return await func(*args, **kwargs)
70
+ return wrapper
71
+
72
+ return decorator if fn is None else decorator(fn)
throttlekit/limiter.py ADDED
@@ -0,0 +1,104 @@
1
+ import asyncio
2
+ from functools import wraps
3
+ from typing import Callable, Awaitable, TypeVar, Optional, Union
4
+ import logging
5
+
6
+ try:
7
+ from typing import ParamSpec
8
+ except ImportError:
9
+ from typing_extensions import ParamSpec # type: ignore
10
+
11
+ T = TypeVar("T")
12
+ P = ParamSpec("P")
13
+
14
+ class TokenBucketRateLimiter:
15
+ def __init__(
16
+ self,
17
+ max_tokens: int,
18
+ refill_interval: float = 1.0,
19
+ concurrency_limit: Optional[int] = None,
20
+ logger: Optional[logging.Logger] = None
21
+ ) -> None:
22
+ self.max_tokens: int = max_tokens
23
+ self.refill_interval: float = refill_interval
24
+ self.bucket: asyncio.Queue[int] = asyncio.Queue(maxsize=max_tokens)
25
+ self.semaphore: Optional[asyncio.Semaphore] = asyncio.Semaphore(concurrency_limit) if concurrency_limit else None
26
+ self.started: bool = False
27
+ self.logger: logging.Logger = logger or logging.getLogger(__name__)
28
+
29
+ async def start(self) -> None:
30
+ if self.started:
31
+ return
32
+ self.started = True
33
+ for _ in range(self.max_tokens):
34
+ self.bucket.put_nowait(1)
35
+ asyncio.create_task(self._refill())
36
+ self.logger.debug("Rate limiter started with %d tokens", self.max_tokens)
37
+
38
+ async def _refill(self) -> None:
39
+ while True:
40
+ await asyncio.sleep(self.refill_interval)
41
+ for _ in range(self.max_tokens - self.bucket.qsize()):
42
+ try:
43
+ self.bucket.put_nowait(1)
44
+ except asyncio.QueueFull:
45
+ break
46
+
47
+ async def acquire(self) -> None:
48
+ if not self.started:
49
+ raise RuntimeError("RateLimiter not started. Call await limiter.start() before use.")
50
+ await self.bucket.get()
51
+ if self.semaphore:
52
+ await self.semaphore.acquire()
53
+
54
+ def release_token(self) -> None:
55
+ """Refund a token manually (not normally used)."""
56
+ try:
57
+ self.bucket.put_nowait(1)
58
+ except asyncio.QueueFull:
59
+ pass
60
+
61
+ def release_semaphore(self) -> None:
62
+ """Manually release the semaphore if acquired outside context."""
63
+ if self.semaphore:
64
+ self.semaphore.release()
65
+
66
+ async def __aenter__(self) -> "TokenBucketRateLimiter":
67
+ await self.acquire()
68
+ return self
69
+
70
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> Optional[bool]:
71
+ self.release_semaphore()
72
+ return None
73
+
74
+ def _release(self) -> None:
75
+ """Internal helper for releasing either semaphore or bucket."""
76
+ self.release_semaphore()
77
+ self.release_token()
78
+
79
+ def limit(
80
+ self,
81
+ fn: Optional[Callable[..., Awaitable[T]]] = None,
82
+ *,
83
+ tokens: int = 1
84
+ ) -> Union[
85
+ Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]],
86
+ Callable[..., Awaitable[T]]
87
+ ]:
88
+
89
+ def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
90
+ @wraps(func)
91
+ async def wrapper(*args, **kwargs) -> T:
92
+ for _ in range(tokens):
93
+ await self.acquire()
94
+ try:
95
+ return await func(*args, **kwargs)
96
+ finally:
97
+ for _ in range(tokens):
98
+ self._release()
99
+ return wrapper
100
+
101
+ if fn is None:
102
+ return decorator
103
+ else:
104
+ return decorator(fn)
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: throttlekit
3
+ Version: 0.1.0
4
+ Summary: Fast asyncio-compatible token bucket rate limiter
5
+ Author: Roudrasekhar Majumder
6
+ Author-email: Roudrasekhar Majumder <roudra25@gmail.com>
7
+ License-Expression: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Dist: typing-extensions>=4.0.0 ; python_full_version < '3.10'
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+
21
+ # ๐Ÿ”„ throttlekit
22
+
23
+ A lightweight, asyncio-based rate limiting library for Python that provides flexible and efficient rate limiting solutions.
24
+
25
+ ## ๐Ÿ“‹ Overview
26
+
27
+ **throttlekit** offers two proven rate limiting algorithms:
28
+
29
+ - **โšก TokenBucketRateLimiter**: Allows controlled bursts of activity
30
+ - **๐Ÿ’ง LeakyBucketRateLimiter**: Enforces a strict, steady rate
31
+
32
+ Perfect for API throttling, web scrapers, background jobs, and queue management.
33
+
34
+ ## ๐Ÿš€ Features
35
+
36
+ - โœ… **Two proven algorithms**: Token Bucket (burst-tolerant) and Leaky Bucket (evenly-paced)
37
+ - โœ… **Multiple usage patterns**: `@decorator`, `async with`, and manual `.acquire()`
38
+ - โœ… **Concurrency control**: Optional `concurrency_limit` parameter
39
+ - โœ… **High performance**: Low-overhead design optimized for async workloads
40
+ - โœ… **asyncio integration**: Works seamlessly with `asyncio.gather()` and `TaskGroup`
41
+
42
+ ## โš™๏ธ Installation
43
+
44
+ ### Using uv (recommended)
45
+
46
+ ```bash
47
+ uv add throttlekit
48
+ ```
49
+
50
+ ### Using pip
51
+
52
+ ```bash
53
+ pip install throttlekit
54
+ ```
55
+
56
+ ## โœจ Quick Start
57
+
58
+ ```python
59
+ import asyncio
60
+ from throttlekit import TokenBucketRateLimiter
61
+
62
+ # Create a rate limiter (5 tokens, refill every second)
63
+ limiter = TokenBucketRateLimiter(max_tokens=5, refill_interval=1.0)
64
+
65
+ @limiter.limit
66
+ async def call_api(i):
67
+ await asyncio.sleep(0.2)
68
+ return f"Request {i} completed"
69
+
70
+ async def main():
71
+ await limiter.start()
72
+
73
+ # Process 10 requests with rate limiting
74
+ results = await asyncio.gather(*(call_api(i) for i in range(10)))
75
+ print(results)
76
+
77
+ # Run the example
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ## ๐Ÿง  Which Limiter Should I Use?
82
+
83
+ | Use Case | Recommended Limiter | Why? |
84
+ |----------|-------------------|------|
85
+ | Allow short bursts (e.g., 5 calls at once) | **Token Bucket** | Accumulates tokens for burst capacity |
86
+ | Require steady pacing (e.g., 1 call/sec max) | **Leaky Bucket** | Maintains consistent rate |
87
+ | Queue smoothing, task draining | **Leaky Bucket** | FIFO processing at fixed rate |
88
+ | Per-user or per-key API quotas | **Token Bucket** | Flexible burst handling |
89
+
90
+ ## Rate Limiters
91
+
92
+ ### TokenBucketRateLimiter
93
+
94
+ Allows bursts up to `max_tokens`, then refills at a steady rate.
95
+
96
+ ```python
97
+ from throttlekit import TokenBucketRateLimiter
98
+
99
+ limiter = TokenBucketRateLimiter(
100
+ max_tokens=10, # Maximum burst size
101
+ refill_interval=1.0, # Refill every second
102
+ concurrency_limit=5 # Optional: limit concurrent operations
103
+ )
104
+ ```
105
+
106
+ **Supports:**
107
+
108
+ - `@limiter.limit` decorator
109
+ - `async with limiter` context manager
110
+ - `await limiter.acquire()` manual usage
111
+
112
+ ### ๐Ÿ’ง LeakyBucketRateLimiter
113
+
114
+ Processes requests at a fixed rate, queuing excess requests.
115
+
116
+ ```python
117
+ from throttlekit import LeakyBucketRateLimiter
118
+
119
+ limiter = LeakyBucketRateLimiter(
120
+ rate=2.0, # 2 requests per second
121
+ max_queue_size=100 # Maximum queued requests
122
+ )
123
+ ```
124
+
125
+ **Behavior:**
126
+
127
+ - Drains 1 request every `1/rate` seconds in FIFO order
128
+ - Queued requests are processed at a fixed rate
129
+ - Bursts are automatically queued (up to `max_queue_size`)
130
+
131
+ ## ๐Ÿ“˜ Usage Examples
132
+
133
+ ### 1๏ธโƒฃ Decorator Pattern
134
+
135
+ ```python
136
+ @limiter.limit
137
+ async def fetch_data(url):
138
+ async with aiohttp.ClientSession() as session:
139
+ async with session.get(url) as response:
140
+ return await response.json()
141
+ ```
142
+
143
+ ### 2๏ธโƒฃ Context Manager
144
+
145
+ ```python
146
+ async with limiter:
147
+ result = await expensive_operation()
148
+ return result
149
+ ```
150
+
151
+ ### 3๏ธโƒฃ Manual Control
152
+
153
+ ```python
154
+ await limiter.acquire()
155
+ try:
156
+ result = await do_work()
157
+ finally:
158
+ limiter.release_token() # For TokenBucket
159
+ limiter.release_semaphore() # If using concurrency_limit
160
+ ```
161
+
162
+ ## ๐Ÿงช Testing
163
+
164
+ Install test dependencies:
165
+
166
+ ```bash
167
+ uv pip install pytest pytest-asyncio
168
+ ```
169
+
170
+ Run tests with coverage:
171
+
172
+ ```bash
173
+ pytest --cov=src/throttlekit --cov-report=term-missing
174
+ ```
175
+
176
+ ## ๐Ÿ“ Project Structure
177
+
178
+ ```tree
179
+ throttlekit/
180
+ โ”œโ”€โ”€ src/
181
+ โ”‚ โ””โ”€โ”€ throttlekit/
182
+ โ”‚ โ”œโ”€โ”€ __init__.py
183
+ โ”‚ โ”œโ”€โ”€ limiter.py # TokenBucketRateLimiter
184
+ โ”‚ โ””โ”€โ”€ leaky_limiter.py # LeakyBucketRateLimiter
185
+ โ”œโ”€โ”€ tests/
186
+ โ”‚ โ”œโ”€โ”€ test_token_bucket_limiter.py
187
+ โ”‚ โ””โ”€โ”€ test_leaky_bucket_limiter.py
188
+ โ”œโ”€โ”€ pyproject.toml
189
+ โ”œโ”€โ”€ README.md
190
+ โ””โ”€โ”€ LICENSE
191
+ ```
192
+
193
+ ## ๐Ÿ“œ License
194
+
195
+ MIT License ยฉ [Roudrasekhar Majumder](https://github.com/rowds)
196
+
197
+ ## ๐Ÿ™‹ Contributing
198
+
199
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed setup instructions and guidelines.
200
+
201
+ ---
202
+
203
+ **โญ Star this repo if you find it useful!**
204
+
205
+ [Report Bug](https://github.com/rowds/throttlekit/issues) โ€ข [Request Feature](https://github.com/rowds/throttlekit/issues) โ€ข [Documentation](https://github.com/rowds/throttlekit)
@@ -0,0 +1,6 @@
1
+ throttlekit/__init__.py,sha256=2313ef64e3548b7eb0bda268ebe765f06b3ea0f553388d0c773bbe71013cb69f,93
2
+ throttlekit/leaky_limiter.py,sha256=d356307f138e88fe2ca9289912ad237ad0ef56e26a3703b379dba7b9d3a3b6b0,2445
3
+ throttlekit/limiter.py,sha256=94a02abfad2f23fd0a1af5fc1cfa2f95f9579c28f98bb5fae30b8dd21a74b7ab,3427
4
+ throttlekit-0.1.0.dist-info/WHEEL,sha256=b70116f4076fa664af162441d2ba3754dbb4ec63e09d563bdc1e9ab023cce400,78
5
+ throttlekit-0.1.0.dist-info/METADATA,sha256=259b904e3388fbfdbb8c4813c96ab3be996a59af8f71fbdfcb308150dfd52a5c,5531
6
+ throttlekit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any