fastapi-concurrency-limiter 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,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.
@@ -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,86 @@
1
+ # fastapi-concurrency-limiter
2
+
3
+ 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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install fastapi-concurrency-limiter
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from fastapi import FastAPI
15
+ from fastapi_concurrency_limiter import Limiter, Resource
16
+
17
+ app = FastAPI()
18
+
19
+ database_resource = Resource(5) # max 5 concurrent DB operations
20
+ file_resource = Resource(10) # max 10 concurrent file operations
21
+
22
+ # Register resources with the limiter in the order they should be acquired.
23
+ # Always use this same order in @limiter.resources() to prevent deadlocks.
24
+ limiter = Limiter(timeout=5, resources=[database_resource, file_resource])
25
+
26
+
27
+ @app.get("/messages")
28
+ @limiter.resources([database_resource])
29
+ async def read_messages():
30
+ ...
31
+
32
+
33
+ @app.get("/file")
34
+ @limiter.resources([file_resource])
35
+ async def read_file():
36
+ ...
37
+
38
+
39
+ @app.get("/messages-and-file")
40
+ @limiter.resources([database_resource, file_resource]) # must match registration order
41
+ async def read_messages_and_file():
42
+ ...
43
+ ```
44
+
45
+ When a request cannot acquire a semaphore within `timeout` seconds it receives:
46
+
47
+ ```json
48
+ {"detail": "Server busy, please retry later"}
49
+ ```
50
+
51
+ with HTTP status **503**.
52
+
53
+ ## Resource ordering and deadlocks
54
+
55
+ 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.
56
+
57
+ ```python
58
+ limiter = Limiter(timeout=5, resources=[db, fs])
59
+
60
+ @app.get("/ok")
61
+ @limiter.resources([db, fs]) # correct
62
+ async def ok(): ...
63
+
64
+ @app.get("/bad")
65
+ @limiter.resources([fs, db]) # raises ValueError at startup
66
+ async def bad(): ...
67
+ ```
68
+
69
+ ## API
70
+
71
+ ### `Resource(capacity: int)`
72
+
73
+ A semaphore with the given concurrency limit.
74
+
75
+ ### `Limiter(timeout: float, resources: list[Resource])`
76
+
77
+ - `timeout` — seconds to wait for semaphore acquisition before returning 503.
78
+ - `resources` — all resources managed by this limiter, in canonical acquisition order.
79
+
80
+ ### `@limiter.resources(resources: list[Resource])`
81
+
82
+ Route decorator. Acquires the listed resources before the handler runs and releases them after, even on exception.
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "fastapi-concurrency-limiter"
3
+ version = "0.1.0"
4
+ description = "Semaphore-based concurrency limiter for FastAPI"
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ authors = [{name = "David Zilahi", email = "zilahia@gmail.com"}]
8
+ keywords = ["fastapi", "concurrency", "semaphore", "rate-limiting", "async"]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "fastapi>=0.100.0",
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Framework :: FastAPI",
21
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ [tool.poetry]
26
+ packages = [{include = "fastapi_concurrency_limiter", from = "src"}]
27
+
28
+ [tool.poetry.group.dev.dependencies]
29
+ pytest = "^7.4"
30
+ pytest-asyncio = "^0.23"
31
+ httpx = "^0.26"
32
+ uvicorn = "^0.29"
33
+
34
+ [tool.pytest.ini_options]
35
+ asyncio_mode = "auto"
36
+
37
+ [build-system]
38
+ requires = ["poetry-core"]
39
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,4 @@
1
+ from .limiter import Limiter
2
+ from .resource import Resource
3
+
4
+ __all__ = ["Limiter", "Resource"]
@@ -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()
@@ -0,0 +1,9 @@
1
+ import asyncio
2
+
3
+
4
+ class Resource:
5
+ def __init__(self, capacity: int) -> None:
6
+ if capacity < 1:
7
+ raise ValueError("capacity must be >= 1")
8
+ self.capacity = capacity
9
+ self._semaphore = asyncio.Semaphore(capacity)