FastAPI CBV的實(shí)現(xiàn)

FastAPI天生不支持CBV

Starlette層面上,還提供有CBV的支持,但是在FastAPI的實(shí)現(xiàn),都是默認(rèn)不考慮CBV的。
fastapi中實(shí)現(xiàn)原生cbv,需要涉及到源碼的大量修改。因?yàn)?strong>fastapi的路由對(duì)應(yīng)的app。包含了endpoint,同時(shí)也包含了對(duì)endpoint的依賴解析(dependant)。
當(dāng)APIRoute節(jié)點(diǎn)生成時(shí),會(huì)拿著傳進(jìn)來(lái)的endpoint,通過get_dependant()解析endpoint,并生成一份特定的依賴樹。這是FastAPI感知力的關(guān)鍵。有了這份依賴,Fastapi才能像靜態(tài)語(yǔ)言那樣,對(duì)數(shù)據(jù)進(jìn)行強(qiáng)制要求(因?yàn)樗智宄揺ndpoint需要哪些東西)。

為什么不能是cbv

我們上面說(shuō)到,一份endpoint有一份專屬的dependant,才能使fastapi的機(jī)能正常工作,所以我們?yōu)?strong>APIRoute提供的endpoint,一定要為function形式才行。因?yàn)?strong>class具有多個(gè)方法,如果直接將class作為app(這在starlette中是允許的),就算可以通過__call__解決一些問題,但是會(huì)導(dǎo)致諸如inspect.signature(call)的反射能力失效,因?yàn)?strong>class的endpoint是不明確的,fastapi不清楚應(yīng)該生成哪個(gè)方法的dependant。所以除非我們從源碼層面解決,生成新的專屬解決方案。否則,傳入APIRoute時(shí)就必須是function形式。

那么該怎么做?

雖然不能像Starlette那樣,將類作為app,傳入scope再進(jìn)行dispatch。但是我們可以再形式上接近c(diǎn)bv。即編寫時(shí)仍然是cbv形式。但實(shí)際邏輯是將cbv的方法拆出來(lái),當(dāng)做四個(gè)不同的endpoint生成路由。再使用層面上仍然可以達(dá)到近似效果。

下面提供一個(gè)簡(jiǎn)單的思路

class CbvMeta(type):

    def __new__(mcs, *args, **kwargs):
        cls = super().__new__(mcs, *args, **kwargs)
        if cls.__name__ != 'CbvTest':
            asgi = getattr(cls, 'app', None)
            path = getattr(cls, 'path', None)

            assert asgi is not None and isinstance(asgi, FastAPI), f"請(qǐng)為{cls.__name__}類配置正確的ASGI應(yīng)用"
            assert path is not None, f"請(qǐng)為{cls.__name__}類配置正確的路徑"

            for http_method in ['get', 'post', 'put', 'delete', 'options', 'head', 'patch', 'trace']:

                def parse(item):
                    item = getattr(cls, item)

                    return item[http_method] if isinstance(item, MethodDict) else None

                if method := getattr(cls, http_method, None):
                    getattr(asgi, http_method, None)(
                        path=path,
                        tags=parse('tags') or [cls.__name__],
                        summary=parse('summary') or f'{cls.__name__}.{http_method}',
                        operation_id=parse('operation_id') or f'cbv_{path[1:-2]}_{http_method}',
                        response_model=parse('response_model'),
                        status_code=parse('status_code'),
                        dependencies=parse('dependencies'),
                        description=parse('description'),
                        response_description=parse('response_description'),
                        responses=parse('responses'),
                        deprecated=parse('deprecated'),
                        response_model_include=parse('response_model_include'),
                        response_model_exclude=parse('response_model_exclude'),
                        response_model_by_alias=parse('response_model_by_alias'),
                        response_model_exclude_unset=parse('response_model_exclude_unset'),
                        response_model_exclude_defaults=parse('response_model_exclude_defaults'),
                        response_model_exclude_none=parse('response_model_exclude_none'),
                        include_in_schema=parse('include_in_schema'),
                        response_class=parse('response_class'),
                        name=parse('name'),
                        callbacks=parse('callbacks'),
                    )(method)
        return cls


class MethodDict:
    def __init__(self, get=None, post=None, put=None, delete=None, options=None, head=None, patch=None, trace=None,
                 default=None):
        self.get = get if get is not None else default
        self.post = post if post is not None else default
        self.put = put if put is not None else default
        self.delete = delete if delete is not None else default
        self.options = options if options is not None else default
        self.head = head if head is not None else default
        self.patch = patch if patch is not None else default
        self.trace = trace if trace is not None else default
        self.default = default

    def __getitem__(self, item):
        return getattr(self, item, self.default)


class CbvTest(metaclass=CbvMeta):
    app: FastAPI = None
    path: str = None

    tags: Optional[List[str]] = MethodDict(default=None)
    summary: Optional[str] = MethodDict(default=None)
    operation_id: Optional[str] = MethodDict(default=None)
    response_model: Optional[Type[Any]] = MethodDict(default=None)
    status_code: int = MethodDict(default=200)
    dependencies: Optional[Sequence[params.Depends]] = MethodDict(default=None)
    description: Optional[str] = MethodDict(default=None)
    response_description: str = MethodDict(default="Successful Response")
    responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = MethodDict(default=None)
    deprecated: Optional[bool] = MethodDict(default=None)
    response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = MethodDict(default=None)
    response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = MethodDict(default=None)
    response_model_by_alias: bool = MethodDict(default=True)
    response_model_exclude_unset: bool = MethodDict(default=False)
    response_model_exclude_defaults: bool = MethodDict(default=False)
    response_model_exclude_none: bool = MethodDict(default=False)
    include_in_schema: bool = MethodDict(default=True)
    response_class: Optional[Type[Response]] = MethodDict(default=None)
    name: Optional[str] = MethodDict(default=None)
    callbacks: Optional[List[APIRoute]] = MethodDict(default=None)

CBV的三個(gè)重點(diǎn),一是通過面向?qū)ο笫蛊涓奖?,二是方便同類不同方法的整合,三是可以方便不同方法共用資源。
本示例使用metaclass的方式,將方法拆出來(lái),手動(dòng)調(diào)用app的裝飾器。

from cbv import CbvTest, MethodDict
from fastapi import FastAPI

app = FastAPI()


class Test(CbvTest):
    app = app
    path = '/test1/'

    description = MethodDict(get='get方法', post='post方法', default=None)
    status_code = MethodDict(get=200, default=200)
    # ------------------------
    num = 1

    @classmethod
    def get(cls):
        return {'msg': cls.num}

    @classmethod
    def post(cls, item_id: int):
        return items[cls.num]


items = {
    1: {"name": "Foo", "price": 50.2},
    2: {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    3: {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

使用方法
可以使用類方法作為endpoint,這樣可以使用類的資源。
在類屬性中可以重寫MethodDict,這里沒有采用{‘get’:..., 'post':...}的字典形式,而是封裝成了類。

僅做示例用途,可能包含一些未知的bug,或者處理不妥之處,還請(qǐng)見諒

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容