
如何快速實(shí)現(xiàn)REST API集成以優(yōu)化業(yè)務(wù)流程
訪問起源碼可以看見調(diào)到self.add_api_route傳了一個(gè)func,這個(gè)func在上面的代碼中就是index這個(gè)函數(shù) / endpoint,到這一層就夠了
通過上面得知每個(gè)router.xx后面都調(diào)用了add_api_route,那要實(shí)現(xiàn)類視圖,需要步驟如下:1. 創(chuàng)建APIRouter示例,2. 將類中的某些方法add_api_route加到APIRouter實(shí)例中即可
這里簡化了參數(shù)描述,會缺少代碼提示,實(shí)際和APIRouter的參數(shù)定義一致就行
class Controller:
def __init__(self, **kwargs):
"""kwargs 等同于APIRouter 實(shí)例化入?yún)?""
self.kwargs = kwargs
def __call__(self, cls):
# 創(chuàng)建router實(shí)例
router: APIRouter = APIRouter(
**self.kwargs
)
# 返回被裝飾類的所有方法和屬性名稱
for attr_name in dir(cls):
# 通過反射拿到對應(yīng)屬性的值 或方法對象本身
attr = getattr(cls, attr_name)
# 簡單處理,如果函數(shù)返回的是個(gè)字典并且endpoint在這里面,就添加到router上
if isinstance(attr, dict) and "endpoint" in attr:
router.add_api_route(**attr)
# 然后把router 給被裝飾的類上,再返回被裝飾類
cls.router = router
return cls
被裝飾方法返回帶
endpoint
的字典,讓Controller
能夠掃描到
class RequestMapping:
"""請求"""
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, func):
# 這里這個(gè)endpoint 對應(yīng)的value 就是被裝飾的函數(shù)
# 返回的內(nèi)容其實(shí)是符合self.api_add_route的入?yún)⒁?br />
return {"endpoint": func, **self.kwargs}
from fastapi import APIRouter
class Controller:
def __init__(self, **kwargs):
"""kwargs 等同于APIRouter 實(shí)例化入?yún)?""
self.kwargs = kwargs
def __call__(self, cls):
# 創(chuàng)建router實(shí)例
router: APIRouter = APIRouter(
**self.kwargs
)
# 返回被裝飾類的所有方法和屬性名稱
for attr_name in dir(cls):
# 通過反射拿到對應(yīng)屬性的值 或方法對象本身
attr = getattr(cls, attr_name)
# 簡單處理,如果函數(shù)返回的是個(gè)字典并且endpoint在這里面,就添加到router上
if isinstance(attr, dict) and "endpoint" in attr:
router.add_api_route(**attr)
# 然后把router 給被裝飾的類上,再返回被裝飾類
cls.router = router
return cls
class RequestMapping:
"""請求"""
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, func):
# 這里這個(gè)endpoint 對應(yīng)的value 就是被裝飾的函數(shù)
# 返回的內(nèi)容其實(shí)是符合self.api_add_route的入?yún)⒁?br />
return {"endpoint": func, **self.kwargs}
def get_db():
print("模擬db session")
return "模擬db session"
@Controller(prefix="/demo", tags=["demo"])
class Demo:
name: str = "ggbond"
db_session: str = Depends(get_db)
@RequestMapping(path="")
def get_list(self):
return [self.name, self.db_session]
from fastapi import FastAPI
app = FastAPI()
app.include_router(Demo.router)
if __name__ == '__main__':
import uvicorn
uvicorn.run(app)
遇到的這些問題,解決方法其實(shí)之前大佬都寫好了 https://github.com/dmontagu/fastapi-utils/blob/master/fastapi_utils/cbv.py
將
self
這個(gè)方法簽名修改成Depends(cls)
– 實(shí)例化當(dāng)前類對象,并依賴注入(依賴注入的參數(shù),無需用戶傳遞哦)。
def update_endpoint_signature(endpoint, cls_obj):
"""
source:https://github.com/dmontagu/fastapi-utils/blob/master/fastapi_utils/cbv.py#L92
修改端點(diǎn)簽名,將self參數(shù)改成關(guān)鍵字參數(shù) self = Depends(cls)
:param endpoint: 端點(diǎn)
:param cls_obj: 被裝飾的類 本身
:return:
"""
# 獲取原始端點(diǎn)函數(shù)的簽名
old_signature = inspect.signature(endpoint)
# 獲取參數(shù)列表
old_parameters = list(old_signature.parameters.values())
# 獲取原始端點(diǎn)函數(shù)的第一個(gè)參數(shù), self
old_first_parameter = old_parameters[0]
# 將第一個(gè)參數(shù)替換為依賴注入 當(dāng)前類 的參數(shù)
#
new_first_parameter = old_first_parameter.replace(default=Depends(cls_obj))
new_parameters = [new_first_parameter] + [
parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY)
for parameter in old_parameters[1:]
] # 替換其他參數(shù)的類型為 KEYWORD_ONLY,以便正確執(zhí)行依賴注入
new_signature = old_signature.replace(parameters=new_parameters) # 創(chuàng)建新的簽名
setattr(endpoint, "__signature__", new_signature) # 替換端點(diǎn)函數(shù)的簽名
需要修改類的簽名、和init方法,并在init方法時(shí)調(diào)用依賴注入的函數(shù)這里的實(shí)例
get_db
,并把結(jié)果更新到db_session
中去
CBV_CLASS_KEY = "__cbv_class__"
def _init_cbv(cls: type[Any]) -> None:
"""
Idempotently modifies the provided cls
, performing the following modifications:
* The __init__
function is updated to set any class-annotated dependencies as instance attributes
* The __signature__
attribute is updated to indicate to FastAPI what arguments should be passed to the initializer
"""
# 實(shí)例化之后就不用實(shí)例化了
if getattr(cls, CBV_CLASS_KEY, False): # pragma: no cover
return # Already initialized
# 保留原本的 init
old_init: Callable[..., Any] = cls.__init__
# 獲取原本的 實(shí)例簽名
old_signature = inspect.signature(old_init)
# 刪除self
old_parameters = list(old_signature.parameters.values())[1:] # drop self
parameter
# 改成關(guān)鍵字參數(shù)
new_parameters = [
x for x in old_parameters if x.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
]
# 依賴注入列表
dependency_names: list[str] = []
# 返回類 cls 中的所有類型標(biāo)注,
for name, hint in get_type_hints(cls).items():
if is_classvar(hint):
continue
parameter_kwargs = {"default": getattr(cls, name, Ellipsis)}
dependency_names.append(name)
# 加入到關(guān)鍵字參數(shù)中
new_parameters.append(
inspect.Parameter(name=name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=hint, **parameter_kwargs)
)
# 替換簽名參數(shù)
new_signature = old_signature.replace(parameters=new_parameters)
# 實(shí)例化時(shí)候的處理
def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
for dep_name in dependency_names:
# pop 會拿到依賴注入的返回值
dep_value = kwargs.pop(dep_name)
# 設(shè)置成屬性
setattr(self, dep_name, dep_value)
# 再調(diào)用原來的實(shí)例化方法
old_init(self, *args, **kwargs)
setattr(cls, "__signature__", new_signature)
setattr(cls, "__init__", new_init)
"""
Source: https://github.com/dmontagu/fastapi-utils/blob/master/fastapi_utils/cbv.py
"""
from __future__ import annotations
import inspect
from collections.abc import Callable
from typing import Any, TypeVar, get_type_hints
from fastapi import APIRouter, Depends
from pydantic.v1.typing import is_classvar
T = TypeVar("T")
CBV_CLASS_KEY = "__cbv_class__"
def _init_cbv(cls: type[Any]) -> None:
"""
Idempotently modifies the provided cls
, performing the following modifications:
* The __init__
function is updated to set any class-annotated dependencies as instance attributes
* The __signature__
attribute is updated to indicate to FastAPI what arguments should be passed to the initializer
"""
if getattr(cls, CBV_CLASS_KEY, False): # pragma: no cover
return # Already initialized
old_init: Callable[..., Any] = cls.__init__
old_signature = inspect.signature(old_init)
old_parameters = list(old_signature.parameters.values())[1:] # drop self
parameter
new_parameters = [
x for x in old_parameters if x.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
]
dependency_names: list[str] = []
for name, hint in get_type_hints(cls).items():
if is_classvar(hint):
continue
parameter_kwargs = {"default": getattr(cls, name, Ellipsis)}
dependency_names.append(name)
new_parameters.append(
inspect.Parameter(name=name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=hint, **parameter_kwargs)
)
new_signature = old_signature.replace(parameters=new_parameters)
def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
for dep_name in dependency_names:
dep_value = kwargs.pop(dep_name)
setattr(self, dep_name, dep_value)
old_init(self, *args, **kwargs)
setattr(cls, "__signature__", new_signature)
setattr(cls, "__init__", new_init)
setattr(cls, CBV_CLASS_KEY, True)
def _update_cbv_route_endpoint_signature(cls: type[Any], endpoint: Callable[..., Any]) -> None:
"""
Fixes the endpoint signature for a cbv route to ensure FastAPI performs dependency injection properly.
"""
old_signature = inspect.signature(endpoint)
old_parameters: list[inspect.Parameter] = list(old_signature.parameters.values())
old_first_parameter = old_parameters[0]
new_first_parameter = old_first_parameter.replace(default=Depends(cls))
new_parameters = [new_first_parameter] + [
parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY) for parameter in old_parameters[1:]
]
new_signature = old_signature.replace(parameters=new_parameters)
setattr(endpoint, "__signature__", new_signature)
class Controller:
def __init__(self, **kwargs):
"""kwargs 等同于APIRouter 實(shí)例化入?yún)?""
self.kwargs = kwargs
def __call__(self, cls):
_init_cbv(cls)
# 創(chuàng)建router實(shí)例
router: APIRouter = APIRouter(
**self.kwargs
)
# 返回被裝飾類的所有方法和屬性名稱
for attr_name in dir(cls):
# 通過反射拿到對應(yīng)屬性的值 或方法對象本身
attr = getattr(cls, attr_name)
# 添加到router上
if isinstance(attr, BaseRoute) and hasattr(attr, "kwargs"):
_update_cbv_route_endpoint_signature(cls, attr.kwargs["endpoint"])
if isinstance(attr, RequestMapping):
router.add_api_route(**attr.kwargs)
elif isinstance(attr, WebSocket):
router.add_websocket_route(**attr.kwargs)
else:
assert False, "Cls Type is RequestMapping or WebSocket"
cls.router = router
return cls
class BaseRoute:
pass
class RequestMapping(BaseRoute):
"""請求"""
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, func):
# 這里這個(gè)endpoint 對應(yīng)的value 就是被裝飾的函數(shù)
# 返回的內(nèi)容其實(shí)是符合self.api_add_route的入?yún)⒁? self.kwargs["endpoint"] = func
return self
class WebSocket(BaseRoute):
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, func):
# 這里這個(gè)endpoint 對應(yīng)的value 就是被裝飾的函數(shù)
# 返回的內(nèi)容其實(shí)是符合self.api_add_route的入?yún)⒁? self.kwargs["endpoint"] = func
return self
from fastapi import Depends
from boot import Controller, RequestMapping, WebSocket
def get_db():
print("模擬db session")
return "模擬db session"
@Controller(prefix="/demo", tags=["demo"])
class Demo:
name = "ggbond"
# 帶類型標(biāo)注類屬性會被加入到方法中如router.get(db_session: str = Depends(get_db))
db_session: str = Depends(get_db)
@RequestMapping(path="")
def get_list(self, age: int):
return [self.name, self.db_session]
@WebSocket(path="/ws")
async def ws(self):
pass
from fastapi import FastAPI
app = FastAPI()
app.include_router(Demo.router)
if __name__ == '__main__':
import uvicorn
uvicorn.run(app)
文章轉(zhuǎn)自微信公眾號@7y記