2 changed files with 215 additions and 0 deletions
@ -0,0 +1,120 @@ |
|||
import inspect |
|||
import logging |
|||
from collections.abc import Callable |
|||
from functools import partial |
|||
from typing import Any |
|||
|
|||
from fastapi import APIRouter |
|||
from typing_extensions import Self |
|||
|
|||
|
|||
class CBV: |
|||
def __init__( |
|||
self, |
|||
cls: type[Any], |
|||
router: APIRouter, |
|||
prefix: str = "", |
|||
): |
|||
self.logger = logging.getLogger(self.__class__.__name__) |
|||
self.router = router |
|||
self.prefix = prefix |
|||
self.cls = cls |
|||
|
|||
def __call__(self, *args: Any, **kwargs: Any) -> Self: |
|||
self.instance = self.cls(*args, **kwargs) |
|||
for name in [ |
|||
"head", |
|||
"get", |
|||
"post", |
|||
"put", |
|||
"delete", |
|||
"patch", |
|||
"options", |
|||
"trace", |
|||
"connect", |
|||
]: |
|||
if hasattr(self.instance, name): |
|||
method = getattr(self.instance, name) |
|||
self.router.add_api_route( |
|||
path=self.prefix, |
|||
endpoint=method, |
|||
methods=[name.upper()], |
|||
) |
|||
|
|||
return self |
|||
|
|||
|
|||
class CBR: |
|||
def __init__( |
|||
self, |
|||
cls: type[Any], |
|||
router: APIRouter, |
|||
prefix: str = "", |
|||
): |
|||
self.logger = logging.getLogger(self.__class__.__name__) |
|||
self.router = router |
|||
self.prefix = prefix |
|||
self.cls = cls |
|||
|
|||
def __call__(self, *args: Any, **kwargs: Any) -> Self: |
|||
self.instance = self.cls(*args, **kwargs) |
|||
|
|||
for _name, endpoint in inspect.getmembers( |
|||
self.instance, |
|||
lambda x: inspect.ismethod(x) or inspect.isfunction(x) |
|||
): |
|||
if cbx_router := endpoint.__annotations__.get("cbx_router"): |
|||
self.router.add_api_route( |
|||
path=f"{self.prefix}{cbx_router['path']}", |
|||
endpoint=endpoint, |
|||
methods=[cbx_router["method"]], |
|||
) |
|||
return self |
|||
|
|||
|
|||
class cbv: |
|||
def __init__(self, router: APIRouter, prefix: str = ""): |
|||
self.router = router |
|||
self.prefix = prefix |
|||
|
|||
def __call__(self, cls: type[Any]) -> CBV: |
|||
return CBV(cls, self.router, self.prefix) |
|||
|
|||
|
|||
class cbr: |
|||
|
|||
class method: |
|||
def __init__(self, method: str, path: str, *args: Any, **kwargs: Any): |
|||
self.method = method |
|||
self.path = path |
|||
self.args = args |
|||
self.kwargs = kwargs |
|||
|
|||
def __call__(self, endpoint: Callable[..., Any]) -> Callable[..., Any]: |
|||
endpoint.__annotations__.setdefault( |
|||
"cbx_router", |
|||
{ |
|||
"method": self.method, |
|||
"path": self.path, |
|||
"args": self.args, |
|||
"kwargs": self.kwargs, |
|||
}, |
|||
) |
|||
return endpoint |
|||
|
|||
head = partial(method, "HEAD") |
|||
get = partial(method, "GET") |
|||
post = partial(method, "POST") |
|||
put = partial(method, "PUT") |
|||
delete = partial(method, "DELETE") |
|||
patch = partial(method, "PATCH") |
|||
options = partial(method, "OPTIONS") |
|||
trace = partial(method, "TRACE") |
|||
connect = partial(method, "CONNECT") |
|||
|
|||
def __init__(self, router: APIRouter, prefix: str = ""): |
|||
self.router = router |
|||
self.prefix = prefix |
|||
|
|||
def __call__(self, cls: type[Any]) -> CBR: |
|||
return CBR(cls, self.router, self.prefix) |
|||
@ -0,0 +1,95 @@ |
|||
from fastapi import APIRouter, FastAPI |
|||
from fastapi.cbx import cbr, cbv |
|||
from fastapi.testclient import TestClient |
|||
from pydantic import BaseModel |
|||
|
|||
|
|||
# ============================== |
|||
# 请求体模型(用于 POST 传参) |
|||
# ============================== |
|||
class UserCreate(BaseModel): |
|||
username: str |
|||
age: int |
|||
|
|||
|
|||
# ============================== |
|||
# 1. CBV 测试:自动路由 + 接口传参 |
|||
# ============================== |
|||
@cbv(prefix="/cbv", router=APIRouter()) |
|||
class CBVTest: |
|||
def __init__(self, message: str): |
|||
self.message = message |
|||
|
|||
# GET 传参:查询参数 |
|||
def get(self, user_id: int, name: str = None): |
|||
return {"hello": self.message, "user_id": user_id, "name": name} |
|||
|
|||
# POST 传参:请求体 |
|||
def post(self, user: UserCreate): |
|||
return { |
|||
"method": "POST", |
|||
"message": self.message, |
|||
"user": user.model_dump(), # 🔥 修复在这里 |
|||
} |
|||
|
|||
|
|||
# ============================== |
|||
# 2. CBR 测试:装饰器路由 + 接口传参 |
|||
# ============================== |
|||
@cbr(prefix="/cbr", router=APIRouter()) |
|||
class CBRTest: |
|||
def __init__(self, service_name: str): |
|||
self.service_name = service_name |
|||
|
|||
@cbr.get("/info") |
|||
def get_info(self, id: int): |
|||
return {"service": self.service_name, "id": id} |
|||
|
|||
@cbr.post("/create") |
|||
def create_user(self, user: UserCreate): |
|||
return { |
|||
"service": self.service_name, |
|||
"user": user.model_dump(), # 🔥 修复在这里 |
|||
} |
|||
|
|||
|
|||
# ============================== |
|||
# ✅ 优雅初始化(传参)+ 挂载路由 |
|||
# ============================== |
|||
cbv_instance = CBVTest(message="cbv_success") |
|||
cbr_instance = CBRTest(service_name="cbr_service") |
|||
|
|||
app = FastAPI() |
|||
app.include_router(cbv_instance.router) |
|||
app.include_router(cbr_instance.router) |
|||
client = TestClient(app) |
|||
|
|||
|
|||
# ============================== |
|||
# 🟢 CBV 测试用例(带真实传参) |
|||
# ============================== |
|||
def test_cbv_get_with_params(): |
|||
resp = client.get("/cbv?user_id=100&name=test") |
|||
assert resp.status_code == 200 |
|||
assert resp.json()["hello"] == "cbv_success" |
|||
|
|||
|
|||
def test_cbv_post_with_body(): |
|||
resp = client.post("/cbv", json={"username": "test", "age": 20}) |
|||
assert resp.status_code == 200 |
|||
assert resp.json()["user"]["username"] == "test" |
|||
|
|||
|
|||
# ============================== |
|||
# 🟢 CBR 测试用例(带真实传参) |
|||
# ============================== |
|||
def test_cbr_get_with_query(): |
|||
resp = client.get("/cbr/info?id=999") |
|||
assert resp.status_code == 200 |
|||
assert resp.json()["service"] == "cbr_service" |
|||
|
|||
|
|||
def test_cbr_post_with_body(): |
|||
resp = client.post("/cbr/create", json={"username": "cbr_user", "age": 25}) |
|||
assert resp.status_code == 200 |
|||
assert resp.json()["user"]["age"] == 25 |
|||
Loading…
Reference in new issue