fastapi-concurrency-limiter 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.
- fastapi_concurrency_limiter/__init__.py +4 -0
- fastapi_concurrency_limiter/limiter.py +56 -0
- fastapi_concurrency_limiter/py.typed +0 -0
- fastapi_concurrency_limiter/resource.py +9 -0
- fastapi_concurrency_limiter-0.1.0.dist-info/METADATA +109 -0
- fastapi_concurrency_limiter-0.1.0.dist-info/RECORD +8 -0
- fastapi_concurrency_limiter-0.1.0.dist-info/WHEEL +4 -0
- fastapi_concurrency_limiter-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from fastapi import HTTPException
|
|
8
|
+
|
|
9
|
+
from .resource import Resource
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Limiter:
|
|
13
|
+
def __init__(self, timeout: float, resources: list[Resource]) -> None:
|
|
14
|
+
self.timeout = timeout
|
|
15
|
+
self._resources = resources
|
|
16
|
+
|
|
17
|
+
def resources(self, resources: list[Resource]) -> Callable:
|
|
18
|
+
for r in resources:
|
|
19
|
+
if r not in self._resources:
|
|
20
|
+
raise ValueError(f"Resource {r!r} was not registered with this Limiter")
|
|
21
|
+
|
|
22
|
+
positions = [self._resources.index(r) for r in resources]
|
|
23
|
+
if positions != sorted(positions):
|
|
24
|
+
raise ValueError(
|
|
25
|
+
"Resources must be passed in registration order to prevent deadlocks"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def decorator(func: Callable) -> Callable:
|
|
29
|
+
@functools.wraps(func)
|
|
30
|
+
async def wrapper(*args, **kwargs):
|
|
31
|
+
async with self._acquire_all(resources):
|
|
32
|
+
return await func(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
wrapper.__signature__ = inspect.signature(func)
|
|
35
|
+
return wrapper
|
|
36
|
+
|
|
37
|
+
return decorator
|
|
38
|
+
|
|
39
|
+
@asynccontextmanager
|
|
40
|
+
async def _acquire_all(self, resources: list[Resource]):
|
|
41
|
+
acquired: list[Resource] = []
|
|
42
|
+
try:
|
|
43
|
+
try:
|
|
44
|
+
async with asyncio.timeout(self.timeout):
|
|
45
|
+
for resource in resources:
|
|
46
|
+
await resource._semaphore.acquire()
|
|
47
|
+
acquired.append(resource)
|
|
48
|
+
except TimeoutError:
|
|
49
|
+
raise HTTPException(
|
|
50
|
+
status_code=503,
|
|
51
|
+
detail="Server busy, please retry later",
|
|
52
|
+
)
|
|
53
|
+
yield
|
|
54
|
+
finally:
|
|
55
|
+
for resource in reversed(acquired):
|
|
56
|
+
resource._semaphore.release()
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-concurrency-limiter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Semaphore-based concurrency limiter for FastAPI
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: fastapi,concurrency,semaphore,rate-limiting,async
|
|
8
|
+
Author: David Zilahi
|
|
9
|
+
Author-email: zilahia@gmail.com
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Framework :: FastAPI
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: fastapi (>=0.100.0)
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# fastapi-concurrency-limiter
|
|
24
|
+
|
|
25
|
+
Semaphore-based concurrency limiter for [FastAPI](https://fastapi.tiangolo.com/). Define named resources with a maximum concurrency, apply them to routes with a decorator, and get automatic 503 responses when the server is too busy.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install fastapi-concurrency-limiter
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from fastapi import FastAPI
|
|
37
|
+
from fastapi_concurrency_limiter import Limiter, Resource
|
|
38
|
+
|
|
39
|
+
app = FastAPI()
|
|
40
|
+
|
|
41
|
+
database_resource = Resource(5) # max 5 concurrent DB operations
|
|
42
|
+
file_resource = Resource(10) # max 10 concurrent file operations
|
|
43
|
+
|
|
44
|
+
# Register resources with the limiter in the order they should be acquired.
|
|
45
|
+
# Always use this same order in @limiter.resources() to prevent deadlocks.
|
|
46
|
+
limiter = Limiter(timeout=5, resources=[database_resource, file_resource])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.get("/messages")
|
|
50
|
+
@limiter.resources([database_resource])
|
|
51
|
+
async def read_messages():
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.get("/file")
|
|
56
|
+
@limiter.resources([file_resource])
|
|
57
|
+
async def read_file():
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.get("/messages-and-file")
|
|
62
|
+
@limiter.resources([database_resource, file_resource]) # must match registration order
|
|
63
|
+
async def read_messages_and_file():
|
|
64
|
+
...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
When a request cannot acquire a semaphore within `timeout` seconds it receives:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{"detail": "Server busy, please retry later"}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
with HTTP status **503**.
|
|
74
|
+
|
|
75
|
+
## Resource ordering and deadlocks
|
|
76
|
+
|
|
77
|
+
When an endpoint acquires multiple resources, always pass them to `@limiter.resources()` in the same order they were registered in `Limiter(resources=[...])`. Passing them in a different order raises a `ValueError` at startup — this is intentional: consistent acquisition order is the simplest way to prevent deadlocks.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
limiter = Limiter(timeout=5, resources=[db, fs])
|
|
81
|
+
|
|
82
|
+
@app.get("/ok")
|
|
83
|
+
@limiter.resources([db, fs]) # correct
|
|
84
|
+
async def ok(): ...
|
|
85
|
+
|
|
86
|
+
@app.get("/bad")
|
|
87
|
+
@limiter.resources([fs, db]) # raises ValueError at startup
|
|
88
|
+
async def bad(): ...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
### `Resource(capacity: int)`
|
|
94
|
+
|
|
95
|
+
A semaphore with the given concurrency limit.
|
|
96
|
+
|
|
97
|
+
### `Limiter(timeout: float, resources: list[Resource])`
|
|
98
|
+
|
|
99
|
+
- `timeout` — seconds to wait for semaphore acquisition before returning 503.
|
|
100
|
+
- `resources` — all resources managed by this limiter, in canonical acquisition order.
|
|
101
|
+
|
|
102
|
+
### `@limiter.resources(resources: list[Resource])`
|
|
103
|
+
|
|
104
|
+
Route decorator. Acquires the listed resources before the handler runs and releases them after, even on exception.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
109
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fastapi_concurrency_limiter/__init__.py,sha256=lGPvCTaiGmGtHlup-WbPKyEU2s7RunkTtxD7NJz0LWs,95
|
|
2
|
+
fastapi_concurrency_limiter/limiter.py,sha256=5heIOIRoOzFUTqvx3T4FV_PB2_429ctCpddbGQMKq4I,1854
|
|
3
|
+
fastapi_concurrency_limiter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
fastapi_concurrency_limiter/resource.py,sha256=HMMRRof-t81Cjbcek3b0RodD1yXT83-X9Uk_P1b7s9k,246
|
|
5
|
+
fastapi_concurrency_limiter-0.1.0.dist-info/METADATA,sha256=DPOfQ8w8nM5jqJj6i3AbOQPHQlfOOCAoV8BP-VDVfck,3232
|
|
6
|
+
fastapi_concurrency_limiter-0.1.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
7
|
+
fastapi_concurrency_limiter-0.1.0.dist-info/licenses/LICENSE,sha256=junx9xHpZUnKB22V3436-WwRSQhxs6lRDp2eaADhQ14,1069
|
|
8
|
+
fastapi_concurrency_limiter-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Zilahi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|