fastapi-cbv-router 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,19 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtualenvs / tooling
10
+ .venv/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
@@ -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.
@@ -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
+ [![CI](https://github.com/Chelovek760/fastapi-cbv/actions/workflows/ci.yml/badge.svg)](https://github.com/Chelovek760/fastapi-cbv/actions/workflows/ci.yml)
28
+ [![PyPI version](https://img.shields.io/pypi/v/fastapi-cbv-router.svg)](https://pypi.org/project/fastapi-cbv-router/)
29
+ [![Python versions](https://img.shields.io/pypi/pyversions/fastapi-cbv-router.svg)](https://pypi.org/project/fastapi-cbv-router/)
30
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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,155 @@
1
+ # fastapi-cbv-router
2
+
3
+ [![CI](https://github.com/Chelovek760/fastapi-cbv/actions/workflows/ci.yml/badge.svg)](https://github.com/Chelovek760/fastapi-cbv/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/fastapi-cbv-router.svg)](https://pypi.org/project/fastapi-cbv-router/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/fastapi-cbv-router.svg)](https://pypi.org/project/fastapi-cbv-router/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ Class-based views (CBV) for FastAPI routers — a small, maintained drop-in
9
+ replacement for the unmaintained `fastapi_utils.cbv`. Works on modern FastAPI
10
+ (tested from 0.115), and in particular survives FastAPI 0.137+, where
11
+ `fastapi_utils.cbv` breaks.
12
+
13
+ ## Why
14
+
15
+ `fastapi_utils.cbv` relied on `APIRouter.include_router` eagerly copying
16
+ `APIRoute` objects into `router.routes`. FastAPI 0.137 made `include_router`
17
+ lazy, which broke `fastapi_utils` whenever two `@cbv` decorators shared one
18
+ router.
19
+
20
+ `fastapi-cbv-router` rebuilds each endpoint through the router's public
21
+ `add_api_route` / `add_api_websocket_route` API, which stays eager and
22
+ delegates all route-state computation back to FastAPI. Route configuration is
23
+ copied by introspecting `add_api_route`'s own parameters, so the package adapts
24
+ to upstream FastAPI changes instead of hard-coding a kwarg list.
25
+
26
+ ## Features
27
+
28
+ - One small decorator, `@cbv(router)` — no base classes or metaclasses required.
29
+ - Share a single dependency instance (`self`) across every endpoint on a class.
30
+ - Inject dependencies either as **class-level annotated attributes** or through a
31
+ **custom `__init__`** — use whichever fits your style.
32
+ - Works with regular HTTP routes **and** WebSocket routes.
33
+ - Preserves all route configuration (`status_code`, `response_model`,
34
+ `dependencies`, tags, …) and the router `prefix`.
35
+ - Fully typed (`py.typed`), zero dependencies beyond FastAPI.
36
+
37
+ ## Requirements
38
+
39
+ - **Python:** 3.13+
40
+ - **FastAPI:** 0.115+ — continuously tested against the latest release, currently
41
+ **0.137.2**. The test suite specifically covers FastAPI 0.137+, where
42
+ `include_router` became lazy and broke `fastapi_utils.cbv`.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install fastapi-cbv-router
48
+ # or
49
+ uv add fastapi-cbv-router
50
+ ```
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ from fastapi import APIRouter, Depends, FastAPI
56
+ from fastapi_cbv_router import cbv
57
+
58
+ router = APIRouter(prefix="/items")
59
+
60
+
61
+ def get_db() -> str:
62
+ return "db-connection"
63
+
64
+
65
+ @cbv(router)
66
+ class ItemsView:
67
+ db: str = Depends(get_db)
68
+
69
+ @router.get("/")
70
+ def list_items(self) -> dict:
71
+ return {"db": self.db, "items": []}
72
+
73
+ @router.post("/")
74
+ def create_item(self, name: str) -> dict:
75
+ return {"db": self.db, "created": name}
76
+
77
+
78
+ app = FastAPI()
79
+ app.include_router(router)
80
+ ```
81
+
82
+ Class-level annotated attributes become keyword-only dependencies injected via
83
+ `Depends(ItemsView)` and are available as `self.<attr>` in every endpoint.
84
+ `ClassVar`-annotated attributes are treated as plain class constants and are not
85
+ injected.
86
+
87
+ ## Dependency injection via `__init__`
88
+
89
+ If you prefer constructor injection — for example to keep a clean abstract base
90
+ and wire dependencies explicitly — define a custom `__init__`. Its parameters
91
+ are resolved by FastAPI when the view is constructed, exactly like an endpoint's
92
+ parameters:
93
+
94
+ ```python
95
+ import abc
96
+ from http import HTTPStatus
97
+
98
+ from fastapi import APIRouter, Depends
99
+ from fastapi_cbv_router import cbv
100
+
101
+ router = APIRouter(prefix="/items", tags=["items"])
102
+
103
+
104
+ def get_service() -> "ItemService":
105
+ ...
106
+
107
+
108
+ class ItemApi(abc.ABC):
109
+ @abc.abstractmethod
110
+ async def get_item(self, item_id: int) -> dict: ...
111
+
112
+
113
+ @cbv(router)
114
+ class HttpItemApi(ItemApi):
115
+ def __init__(self, service: "ItemService" = Depends(get_service)) -> None:
116
+ self.service = service
117
+
118
+ @router.get("/{item_id}", status_code=HTTPStatus.OK)
119
+ async def get_item(self, item_id: int) -> dict:
120
+ return await self.service.fetch(item_id)
121
+ ```
122
+
123
+ This pairs cleanly with DI containers such as
124
+ [`dependency-injector`](https://python-dependency-injector.ets-labs.org/): use
125
+ `Depends(Provide[...])` defaults on the `@inject`-decorated `__init__`.
126
+
127
+ ## WebSocket routes
128
+
129
+ WebSocket endpoints are supported the same way as HTTP routes:
130
+
131
+ ```python
132
+ from fastapi import APIRouter, WebSocket
133
+ from fastapi_cbv_router import cbv
134
+
135
+ router = APIRouter()
136
+
137
+
138
+ @cbv(router)
139
+ class ChatView:
140
+ @router.websocket("/ws")
141
+ async def chat(self, websocket: WebSocket) -> None:
142
+ await websocket.accept()
143
+ await websocket.send_text("hello")
144
+ await websocket.close()
145
+ ```
146
+
147
+ ## Scope
148
+
149
+ Only the plain `@cbv(router)` form is supported (no `*urls` / `set_responses`
150
+ helpers). This is intentional — it covers the common case with the smallest,
151
+ most maintainable surface.
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,115 @@
1
+ [project]
2
+ name = "fastapi-cbv-router"
3
+ version = "0.1.0"
4
+ description = "Class-based views for FastAPI routers — a maintained, FastAPI 0.137+ compatible drop-in for fastapi_utils.cbv."
5
+ authors = [{ name = "Vladimir Klychnikov" }]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ requires-python = ">=3.13"
10
+ keywords = ["fastapi", "cbv", "class-based-views", "router", "dependency-injection"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Framework :: FastAPI",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "fastapi>=0.115",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/Chelovek760/fastapi-cbv"
28
+ Repository = "https://github.com/Chelovek760/fastapi-cbv"
29
+ Issues = "https://github.com/Chelovek760/fastapi-cbv/issues"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "pytest>=9.0.0",
34
+ "pytest-asyncio>=1.2.0",
35
+ "httpx>=0.27",
36
+ "ruff>=0.14",
37
+ "mypy>=2.0.0",
38
+ ]
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/fastapi_cbv_router"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = ["src/**/*.py", "src/**/py.typed"]
49
+
50
+ [tool.ruff]
51
+ line-length = 120
52
+ indent-width = 4
53
+ target-version = "py313"
54
+ output-format = "grouped"
55
+
56
+ lint.task-tags = ["TODO"]
57
+
58
+ lint.select = [
59
+ "E", "W", "F", "I", "D", "UP", "N", "YTT", "ANN", "S", "BLE", "FBT",
60
+ "B", "A", "C4", "T10", "EM", "ISC", "ICN", "T20", "PT", "Q", "RET",
61
+ "SIM", "TID", "DTZ", "ERA", "PD", "PGH",
62
+ "PLC", "PLE", "PLR", "PLW", "PIE", "RUF",
63
+ ]
64
+ lint.ignore = [
65
+ "PLR0913", "ERA001", "ANN204", "C408", "B008", "RUF012",
66
+ "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107",
67
+ "D200", "D212", "ANN401", "ICN001", "UP035", "BLE001", "S608",
68
+ "PLC1901", "S311", "S603", "S602", "S301", "A003", "D415",
69
+ ]
70
+
71
+ lint.fixable = ["ALL"]
72
+
73
+ [tool.ruff.lint.per-file-ignores]
74
+ "tests/**" = ["S101", "PLR2004"]
75
+
76
+ [tool.ruff.format]
77
+ quote-style = "double"
78
+ indent-style = "space"
79
+ skip-magic-trailing-comma = false
80
+ line-ending = "auto"
81
+ docstring-code-format = true
82
+ docstring-code-line-length = "dynamic"
83
+
84
+ [tool.ruff.lint.isort]
85
+ combine-as-imports = true
86
+ relative-imports-order = "closest-to-furthest"
87
+
88
+ [tool.ruff.lint.flake8-tidy-imports]
89
+ ban-relative-imports = "all"
90
+
91
+ [tool.ruff.lint.pep8-naming]
92
+ classmethod-decorators = ["classmethod"]
93
+
94
+ [tool.ruff.lint.pycodestyle]
95
+ max-doc-length = 100
96
+
97
+ [tool.ruff.lint.pydocstyle]
98
+ convention = "google"
99
+
100
+ [tool.ruff.lint.flake8-annotations]
101
+ suppress-dummy-args = true
102
+
103
+ [tool.mypy]
104
+ python_version = "3.13"
105
+ disallow_untyped_defs = true
106
+ ignore_missing_imports = true
107
+ show_error_codes = true
108
+ warn_unused_ignores = true
109
+ check_untyped_defs = true
110
+
111
+ [tool.pytest.ini_options]
112
+ pythonpath = ["src"]
113
+ asyncio_mode = "auto"
114
+ asyncio_default_fixture_loop_scope = "session"
115
+ asyncio_default_test_loop_scope = "session"
@@ -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]