fastapi-cbv-router 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_cbv_router/__init__.py +9 -0
- fastapi_cbv_router/cbv.py +140 -0
- fastapi_cbv_router/py.typed +0 -0
- fastapi_cbv_router-0.1.0.dist-info/METADATA +179 -0
- fastapi_cbv_router-0.1.0.dist-info/RECORD +7 -0
- fastapi_cbv_router-0.1.0.dist-info/WHEEL +4 -0
- fastapi_cbv_router-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Class-based views for FastAPI routers.
|
|
2
|
+
|
|
3
|
+
A maintained, FastAPI 0.137+ compatible drop-in replacement for
|
|
4
|
+
``fastapi_utils.cbv``. Only the plain ``@cbv(router)`` form is supported.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi_cbv_router.cbv import CBV_CLASS_KEY, cbv
|
|
8
|
+
|
|
9
|
+
__all__ = ["CBV_CLASS_KEY", "cbv"]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Class-based views for FastAPI routers.
|
|
2
|
+
|
|
3
|
+
Drop-in replacement for the unmaintained ``fastapi_utils.cbv``. The original
|
|
4
|
+
relied on ``APIRouter.include_router`` eagerly copying ``APIRoute`` objects into
|
|
5
|
+
``router.routes``; FastAPI 0.137 made ``include_router`` lazy (it appends an
|
|
6
|
+
internal ``_IncludedRouter`` placeholder), which broke ``fastapi_utils`` whenever
|
|
7
|
+
two ``@cbv`` decorators shared one router.
|
|
8
|
+
|
|
9
|
+
This implementation rebuilds each endpoint by re-adding it through the router's
|
|
10
|
+
public ``add_api_route`` / ``add_api_websocket_route`` API, which stays eager and
|
|
11
|
+
delegates all route-state computation back to FastAPI. Route configuration is
|
|
12
|
+
copied by introspecting ``add_api_route``'s own parameters, so the helper adapts
|
|
13
|
+
to upstream parameter changes instead of hard-coding a kwarg list.
|
|
14
|
+
|
|
15
|
+
Only the plain ``@cbv(router)`` form is supported (no ``*urls`` / ``set_responses``
|
|
16
|
+
helpers), which covers every call site in the codebase.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import inspect
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any, ClassVar, TypeVar, get_origin, get_type_hints
|
|
22
|
+
|
|
23
|
+
from fastapi import APIRouter, Depends
|
|
24
|
+
from fastapi.routing import APIRoute, APIWebSocketRoute
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
CBV_CLASS_KEY = "__cbv_class__"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cbv(router: APIRouter) -> Callable[[type[T]], type[T]]:
|
|
32
|
+
"""Convert the decorated class into a class-based view for ``router``.
|
|
33
|
+
|
|
34
|
+
Methods of the class registered as endpoints on ``router`` become router
|
|
35
|
+
endpoints whose first positional argument (``self``) is populated via
|
|
36
|
+
FastAPI dependency injection of an instance of the class.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
router: The router whose endpoints defined on the class should be bound.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A class decorator.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def decorator(cls: type[T]) -> type[T]:
|
|
46
|
+
_init_cbv(cls)
|
|
47
|
+
_register_endpoints(router, cls)
|
|
48
|
+
return cls
|
|
49
|
+
|
|
50
|
+
return decorator
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_classvar(annotation: Any) -> bool:
|
|
54
|
+
return annotation is ClassVar or get_origin(annotation) is ClassVar
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _init_cbv(cls: type[Any]) -> None:
|
|
58
|
+
"""Make class-annotated dependencies injectable.
|
|
59
|
+
|
|
60
|
+
Rewrites ``__init__`` so that class-level annotated attributes are accepted as
|
|
61
|
+
keyword-only dependencies and stored on the instance, and updates
|
|
62
|
+
``__signature__`` so FastAPI knows what to pass when resolving ``Depends(cls)``.
|
|
63
|
+
"""
|
|
64
|
+
if getattr(cls, CBV_CLASS_KEY, False):
|
|
65
|
+
return # Already initialized
|
|
66
|
+
old_init: Callable[..., Any] = cls.__init__
|
|
67
|
+
old_signature = inspect.signature(old_init)
|
|
68
|
+
old_parameters = list(old_signature.parameters.values())[1:] # drop `self`
|
|
69
|
+
new_parameters = [
|
|
70
|
+
p for p in old_parameters if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
dependency_names: list[str] = []
|
|
74
|
+
for name, hint in get_type_hints(cls).items():
|
|
75
|
+
if _is_classvar(hint):
|
|
76
|
+
continue
|
|
77
|
+
dependency_names.append(name)
|
|
78
|
+
new_parameters.append(
|
|
79
|
+
inspect.Parameter(
|
|
80
|
+
name=name,
|
|
81
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
82
|
+
annotation=hint,
|
|
83
|
+
default=getattr(cls, name, inspect.Parameter.empty),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
new_signature = old_signature.replace(parameters=new_parameters)
|
|
87
|
+
|
|
88
|
+
def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
|
|
89
|
+
for dep_name in dependency_names:
|
|
90
|
+
setattr(self, dep_name, kwargs.pop(dep_name))
|
|
91
|
+
old_init(self, *args, **kwargs)
|
|
92
|
+
|
|
93
|
+
cls.__signature__ = new_signature
|
|
94
|
+
cls.__init__ = new_init
|
|
95
|
+
cls.__cbv_class__ = True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _register_endpoints(router: APIRouter, cls: type[Any]) -> None:
|
|
99
|
+
functions = {func for _, func in inspect.getmembers(cls, inspect.isfunction)}
|
|
100
|
+
cbv_routes = [
|
|
101
|
+
route
|
|
102
|
+
for route in list(router.routes)
|
|
103
|
+
if isinstance(route, (APIRoute, APIWebSocketRoute)) and route.endpoint in functions
|
|
104
|
+
]
|
|
105
|
+
prefix_length = len(router.prefix)
|
|
106
|
+
for route in cbv_routes:
|
|
107
|
+
_update_endpoint_signature(cls, route)
|
|
108
|
+
router.routes.remove(route)
|
|
109
|
+
path = route.path[prefix_length:]
|
|
110
|
+
name = f"{cls.__name__}.{route.name}"
|
|
111
|
+
if isinstance(route, APIWebSocketRoute):
|
|
112
|
+
router.add_api_websocket_route(path, route.endpoint, name=name, dependencies=route.dependencies)
|
|
113
|
+
else:
|
|
114
|
+
router.add_api_route(path, route.endpoint, **_copy_route_kwargs(router, route, name))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _copy_route_kwargs(router: APIRouter, route: APIRoute, name: str) -> dict[str, Any]:
|
|
118
|
+
"""Reconstruct ``add_api_route`` kwargs from an existing route.
|
|
119
|
+
|
|
120
|
+
Pulls each parameter ``add_api_route`` accepts straight off the route object
|
|
121
|
+
(attribute names match parameter names), so new/removed FastAPI parameters are
|
|
122
|
+
handled automatically without editing this helper.
|
|
123
|
+
"""
|
|
124
|
+
skip = {"self", "path", "endpoint", "name", "methods", "route_class_override"}
|
|
125
|
+
sig = inspect.signature(router.add_api_route)
|
|
126
|
+
kwargs: dict[str, Any] = {pname: getattr(route, pname) for pname in sig.parameters if pname not in skip}
|
|
127
|
+
kwargs["name"] = name
|
|
128
|
+
kwargs["methods"] = list(route.methods or [])
|
|
129
|
+
if "route_class_override" in sig.parameters:
|
|
130
|
+
kwargs["route_class_override"] = type(route)
|
|
131
|
+
return kwargs
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _update_endpoint_signature(cls: type[Any], route: APIRoute | APIWebSocketRoute) -> None:
|
|
135
|
+
"""Inject ``Depends(cls)`` as the default for the endpoint's ``self`` parameter."""
|
|
136
|
+
old_signature = inspect.signature(route.endpoint)
|
|
137
|
+
old_parameters = list(old_signature.parameters.values())
|
|
138
|
+
new_first = old_parameters[0].replace(default=Depends(cls))
|
|
139
|
+
new_parameters = [new_first] + [p.replace(kind=inspect.Parameter.KEYWORD_ONLY) for p in old_parameters[1:]]
|
|
140
|
+
route.endpoint.__signature__ = old_signature.replace(parameters=new_parameters) # type: ignore[attr-defined]
|
|
File without changes
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-cbv-router
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Class-based views for FastAPI routers — a maintained, FastAPI 0.137+ compatible drop-in for fastapi_utils.cbv.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Chelovek760/fastapi-cbv
|
|
6
|
+
Project-URL: Repository, https://github.com/Chelovek760/fastapi-cbv
|
|
7
|
+
Project-URL: Issues, https://github.com/Chelovek760/fastapi-cbv/issues
|
|
8
|
+
Author: Vladimir Klychnikov
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cbv,class-based-views,dependency-injection,fastapi,router
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.13
|
|
22
|
+
Requires-Dist: fastapi>=0.115
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# fastapi-cbv-router
|
|
26
|
+
|
|
27
|
+
[](https://github.com/Chelovek760/fastapi-cbv/actions/workflows/ci.yml)
|
|
28
|
+
[](https://pypi.org/project/fastapi-cbv-router/)
|
|
29
|
+
[](https://pypi.org/project/fastapi-cbv-router/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
Class-based views (CBV) for FastAPI routers — a small, maintained drop-in
|
|
33
|
+
replacement for the unmaintained `fastapi_utils.cbv`. Works on modern FastAPI
|
|
34
|
+
(tested from 0.115), and in particular survives FastAPI 0.137+, where
|
|
35
|
+
`fastapi_utils.cbv` breaks.
|
|
36
|
+
|
|
37
|
+
## Why
|
|
38
|
+
|
|
39
|
+
`fastapi_utils.cbv` relied on `APIRouter.include_router` eagerly copying
|
|
40
|
+
`APIRoute` objects into `router.routes`. FastAPI 0.137 made `include_router`
|
|
41
|
+
lazy, which broke `fastapi_utils` whenever two `@cbv` decorators shared one
|
|
42
|
+
router.
|
|
43
|
+
|
|
44
|
+
`fastapi-cbv-router` rebuilds each endpoint through the router's public
|
|
45
|
+
`add_api_route` / `add_api_websocket_route` API, which stays eager and
|
|
46
|
+
delegates all route-state computation back to FastAPI. Route configuration is
|
|
47
|
+
copied by introspecting `add_api_route`'s own parameters, so the package adapts
|
|
48
|
+
to upstream FastAPI changes instead of hard-coding a kwarg list.
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- One small decorator, `@cbv(router)` — no base classes or metaclasses required.
|
|
53
|
+
- Share a single dependency instance (`self`) across every endpoint on a class.
|
|
54
|
+
- Inject dependencies either as **class-level annotated attributes** or through a
|
|
55
|
+
**custom `__init__`** — use whichever fits your style.
|
|
56
|
+
- Works with regular HTTP routes **and** WebSocket routes.
|
|
57
|
+
- Preserves all route configuration (`status_code`, `response_model`,
|
|
58
|
+
`dependencies`, tags, …) and the router `prefix`.
|
|
59
|
+
- Fully typed (`py.typed`), zero dependencies beyond FastAPI.
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
- **Python:** 3.13+
|
|
64
|
+
- **FastAPI:** 0.115+ — continuously tested against the latest release, currently
|
|
65
|
+
**0.137.2**. The test suite specifically covers FastAPI 0.137+, where
|
|
66
|
+
`include_router` became lazy and broke `fastapi_utils.cbv`.
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install fastapi-cbv-router
|
|
72
|
+
# or
|
|
73
|
+
uv add fastapi-cbv-router
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quickstart
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from fastapi import APIRouter, Depends, FastAPI
|
|
80
|
+
from fastapi_cbv_router import cbv
|
|
81
|
+
|
|
82
|
+
router = APIRouter(prefix="/items")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_db() -> str:
|
|
86
|
+
return "db-connection"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cbv(router)
|
|
90
|
+
class ItemsView:
|
|
91
|
+
db: str = Depends(get_db)
|
|
92
|
+
|
|
93
|
+
@router.get("/")
|
|
94
|
+
def list_items(self) -> dict:
|
|
95
|
+
return {"db": self.db, "items": []}
|
|
96
|
+
|
|
97
|
+
@router.post("/")
|
|
98
|
+
def create_item(self, name: str) -> dict:
|
|
99
|
+
return {"db": self.db, "created": name}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
app = FastAPI()
|
|
103
|
+
app.include_router(router)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Class-level annotated attributes become keyword-only dependencies injected via
|
|
107
|
+
`Depends(ItemsView)` and are available as `self.<attr>` in every endpoint.
|
|
108
|
+
`ClassVar`-annotated attributes are treated as plain class constants and are not
|
|
109
|
+
injected.
|
|
110
|
+
|
|
111
|
+
## Dependency injection via `__init__`
|
|
112
|
+
|
|
113
|
+
If you prefer constructor injection — for example to keep a clean abstract base
|
|
114
|
+
and wire dependencies explicitly — define a custom `__init__`. Its parameters
|
|
115
|
+
are resolved by FastAPI when the view is constructed, exactly like an endpoint's
|
|
116
|
+
parameters:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import abc
|
|
120
|
+
from http import HTTPStatus
|
|
121
|
+
|
|
122
|
+
from fastapi import APIRouter, Depends
|
|
123
|
+
from fastapi_cbv_router import cbv
|
|
124
|
+
|
|
125
|
+
router = APIRouter(prefix="/items", tags=["items"])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_service() -> "ItemService":
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ItemApi(abc.ABC):
|
|
133
|
+
@abc.abstractmethod
|
|
134
|
+
async def get_item(self, item_id: int) -> dict: ...
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@cbv(router)
|
|
138
|
+
class HttpItemApi(ItemApi):
|
|
139
|
+
def __init__(self, service: "ItemService" = Depends(get_service)) -> None:
|
|
140
|
+
self.service = service
|
|
141
|
+
|
|
142
|
+
@router.get("/{item_id}", status_code=HTTPStatus.OK)
|
|
143
|
+
async def get_item(self, item_id: int) -> dict:
|
|
144
|
+
return await self.service.fetch(item_id)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This pairs cleanly with DI containers such as
|
|
148
|
+
[`dependency-injector`](https://python-dependency-injector.ets-labs.org/): use
|
|
149
|
+
`Depends(Provide[...])` defaults on the `@inject`-decorated `__init__`.
|
|
150
|
+
|
|
151
|
+
## WebSocket routes
|
|
152
|
+
|
|
153
|
+
WebSocket endpoints are supported the same way as HTTP routes:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from fastapi import APIRouter, WebSocket
|
|
157
|
+
from fastapi_cbv_router import cbv
|
|
158
|
+
|
|
159
|
+
router = APIRouter()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@cbv(router)
|
|
163
|
+
class ChatView:
|
|
164
|
+
@router.websocket("/ws")
|
|
165
|
+
async def chat(self, websocket: WebSocket) -> None:
|
|
166
|
+
await websocket.accept()
|
|
167
|
+
await websocket.send_text("hello")
|
|
168
|
+
await websocket.close()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Scope
|
|
172
|
+
|
|
173
|
+
Only the plain `@cbv(router)` form is supported (no `*urls` / `set_responses`
|
|
174
|
+
helpers). This is intentional — it covers the common case with the smallest,
|
|
175
|
+
most maintainable surface.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
fastapi_cbv_router/__init__.py,sha256=95gZI4yuy8S8qa6pZTV3SeUW5jPnUqkllrzE-W3X1lA,276
|
|
2
|
+
fastapi_cbv_router/cbv.py,sha256=8CkevG_MSiovHNxVNJUB56q5e9WRtzSdqe4dVEGl5sk,5773
|
|
3
|
+
fastapi_cbv_router/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
fastapi_cbv_router-0.1.0.dist-info/METADATA,sha256=KodkvkDd5KcvzAQO26q7JbE5cGu8OlOSohtko41IXi4,5844
|
|
5
|
+
fastapi_cbv_router-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
fastapi_cbv_router-0.1.0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
|
|
7
|
+
fastapi_cbv_router-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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.
|