throttlekit 0.1.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,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,185 @@
1
+ # 🔄 throttlekit
2
+
3
+ A lightweight, asyncio-based rate limiting library for Python that provides flexible and efficient rate limiting solutions.
4
+
5
+ ## 📋 Overview
6
+
7
+ **throttlekit** offers two proven rate limiting algorithms:
8
+
9
+ - **⚡ TokenBucketRateLimiter**: Allows controlled bursts of activity
10
+ - **💧 LeakyBucketRateLimiter**: Enforces a strict, steady rate
11
+
12
+ Perfect for API throttling, web scrapers, background jobs, and queue management.
13
+
14
+ ## 🚀 Features
15
+
16
+ - ✅ **Two proven algorithms**: Token Bucket (burst-tolerant) and Leaky Bucket (evenly-paced)
17
+ - ✅ **Multiple usage patterns**: `@decorator`, `async with`, and manual `.acquire()`
18
+ - ✅ **Concurrency control**: Optional `concurrency_limit` parameter
19
+ - ✅ **High performance**: Low-overhead design optimized for async workloads
20
+ - ✅ **asyncio integration**: Works seamlessly with `asyncio.gather()` and `TaskGroup`
21
+
22
+ ## ⚙️ Installation
23
+
24
+ ### Using uv (recommended)
25
+
26
+ ```bash
27
+ uv add throttlekit
28
+ ```
29
+
30
+ ### Using pip
31
+
32
+ ```bash
33
+ pip install throttlekit
34
+ ```
35
+
36
+ ## ✨ Quick Start
37
+
38
+ ```python
39
+ import asyncio
40
+ from throttlekit import TokenBucketRateLimiter
41
+
42
+ # Create a rate limiter (5 tokens, refill every second)
43
+ limiter = TokenBucketRateLimiter(max_tokens=5, refill_interval=1.0)
44
+
45
+ @limiter.limit
46
+ async def call_api(i):
47
+ await asyncio.sleep(0.2)
48
+ return f"Request {i} completed"
49
+
50
+ async def main():
51
+ await limiter.start()
52
+
53
+ # Process 10 requests with rate limiting
54
+ results = await asyncio.gather(*(call_api(i) for i in range(10)))
55
+ print(results)
56
+
57
+ # Run the example
58
+ asyncio.run(main())
59
+ ```
60
+
61
+ ## 🧠 Which Limiter Should I Use?
62
+
63
+ | Use Case | Recommended Limiter | Why? |
64
+ |----------|-------------------|------|
65
+ | Allow short bursts (e.g., 5 calls at once) | **Token Bucket** | Accumulates tokens for burst capacity |
66
+ | Require steady pacing (e.g., 1 call/sec max) | **Leaky Bucket** | Maintains consistent rate |
67
+ | Queue smoothing, task draining | **Leaky Bucket** | FIFO processing at fixed rate |
68
+ | Per-user or per-key API quotas | **Token Bucket** | Flexible burst handling |
69
+
70
+ ## Rate Limiters
71
+
72
+ ### TokenBucketRateLimiter
73
+
74
+ Allows bursts up to `max_tokens`, then refills at a steady rate.
75
+
76
+ ```python
77
+ from throttlekit import TokenBucketRateLimiter
78
+
79
+ limiter = TokenBucketRateLimiter(
80
+ max_tokens=10, # Maximum burst size
81
+ refill_interval=1.0, # Refill every second
82
+ concurrency_limit=5 # Optional: limit concurrent operations
83
+ )
84
+ ```
85
+
86
+ **Supports:**
87
+
88
+ - `@limiter.limit` decorator
89
+ - `async with limiter` context manager
90
+ - `await limiter.acquire()` manual usage
91
+
92
+ ### 💧 LeakyBucketRateLimiter
93
+
94
+ Processes requests at a fixed rate, queuing excess requests.
95
+
96
+ ```python
97
+ from throttlekit import LeakyBucketRateLimiter
98
+
99
+ limiter = LeakyBucketRateLimiter(
100
+ rate=2.0, # 2 requests per second
101
+ max_queue_size=100 # Maximum queued requests
102
+ )
103
+ ```
104
+
105
+ **Behavior:**
106
+
107
+ - Drains 1 request every `1/rate` seconds in FIFO order
108
+ - Queued requests are processed at a fixed rate
109
+ - Bursts are automatically queued (up to `max_queue_size`)
110
+
111
+ ## 📘 Usage Examples
112
+
113
+ ### 1️⃣ Decorator Pattern
114
+
115
+ ```python
116
+ @limiter.limit
117
+ async def fetch_data(url):
118
+ async with aiohttp.ClientSession() as session:
119
+ async with session.get(url) as response:
120
+ return await response.json()
121
+ ```
122
+
123
+ ### 2️⃣ Context Manager
124
+
125
+ ```python
126
+ async with limiter:
127
+ result = await expensive_operation()
128
+ return result
129
+ ```
130
+
131
+ ### 3️⃣ Manual Control
132
+
133
+ ```python
134
+ await limiter.acquire()
135
+ try:
136
+ result = await do_work()
137
+ finally:
138
+ limiter.release_token() # For TokenBucket
139
+ limiter.release_semaphore() # If using concurrency_limit
140
+ ```
141
+
142
+ ## 🧪 Testing
143
+
144
+ Install test dependencies:
145
+
146
+ ```bash
147
+ uv pip install pytest pytest-asyncio
148
+ ```
149
+
150
+ Run tests with coverage:
151
+
152
+ ```bash
153
+ pytest --cov=src/throttlekit --cov-report=term-missing
154
+ ```
155
+
156
+ ## 📁 Project Structure
157
+
158
+ ```tree
159
+ throttlekit/
160
+ ├── src/
161
+ │ └── throttlekit/
162
+ │ ├── __init__.py
163
+ │ ├── limiter.py # TokenBucketRateLimiter
164
+ │ └── leaky_limiter.py # LeakyBucketRateLimiter
165
+ ├── tests/
166
+ │ ├── test_token_bucket_limiter.py
167
+ │ └── test_leaky_bucket_limiter.py
168
+ ├── pyproject.toml
169
+ ├── README.md
170
+ └── LICENSE
171
+ ```
172
+
173
+ ## 📜 License
174
+
175
+ MIT License © [Roudrasekhar Majumder](https://github.com/rowds)
176
+
177
+ ## 🙋 Contributing
178
+
179
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed setup instructions and guidelines.
180
+
181
+ ---
182
+
183
+ **⭐ Star this repo if you find it useful!**
184
+
185
+ [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,42 @@
1
+ [project]
2
+ name = "throttlekit"
3
+ version = "0.1.0"
4
+ description = "Fast asyncio-compatible token bucket rate limiter"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ authors = [{ name = "Roudrasekhar Majumder", email = "roudra25@gmail.com" }]
8
+ license = "MIT"
9
+ dependencies = [
10
+ "typing-extensions>=4.0.0; python_version<'3.10'"
11
+ ]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.9",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Intended Audience :: Developers",
21
+ "Topic :: Software Development :: Libraries"
22
+ ]
23
+
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.8.3"]
27
+ build-backend = "uv_build"
28
+
29
+ [tool.uv.build-backend]
30
+ name = "throttlekit"
31
+ path = "src"
32
+ packages = ["throttlekit"]
33
+
34
+ [tool.pytest.ini_options]
35
+ asyncio_mode = "auto"
36
+
37
+ [tool.uv]
38
+ dev-dependencies = [
39
+ "pytest>=8.4.1",
40
+ "pytest-asyncio>=1.1.0",
41
+ "pytest-cov>=6.2.1"
42
+ ]
@@ -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)
@@ -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)