dmontagu / fastapi-utils

Reusable utilities for FastAPI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[QUESTION] CBV Inheritance

bgorges opened this issue · comments

Description

How might I go about writing a CBV that can be inherited from? I want to make a generic class that just needs the class variables set to populate the default CRUD operations that are in the generic base class.

"First" attempt

cbv_router = APIRouter()

class GenericCBV:
    service: Optional[Service] = Depends(None)
    model: Optional[Vertex] = Depends(None)

    @property
    def model_id(self):
        return f'{self.model.vertex_label()}_id'

    @cbv_router.get('/')
    async def get_model_list(self):
        return self.service.get(self.model.vertex_label())
router = APIRouter()
router.include_router(cbv_router)

@cbv(router)
class InvalidationController(GenericCBV):
    service = Depends(Invalidations)
    model = Depends(Invalidation)

Args and Kwargs for required parameters

I am pretty sure I am either missing something or this isn't possible yet. Thanks!

Okay, it wasn't super straightforward, but I think I've mostly addressed this now in #3.

The main change is that I've added a decorator called generic_cbv which you use to decorate the generic CBV, and to which you pass the router used to decorate the generic endpoints.

I haven't tested it properly yet, but the following at least seems to generate the right OpenAPI schema:

from typing import Optional

from fastapi import APIRouter, Depends, FastAPI

from fastapi_utils.cbv import cbv, generic_cbv


def get_a(a: Optional[int]):
    return a


def get_b(b: Optional[int]):
    return b


router = APIRouter()


@generic_cbv(router)
class GenericCBV:
    service: Optional[int] = Depends(None)
    model: Optional[int] = Depends(None)

    @property
    def model_id(self):
        return f"{self.model.vertex_label()}_id"

    @router.get("/")
    async def get_model_list(self):
        return self.service.get(self.model.vertex_label())


router_a = APIRouter()
router_b = APIRouter()


@cbv(router_a)
class CBVA(GenericCBV):
    service: Optional[int] = Depends(get_a)
    model: Optional[int] = Depends(get_a)


@cbv(router_b)
class CBVB(GenericCBV):
    service: Optional[int] = Depends(get_b)
    model: Optional[int] = Depends(get_b)


app = FastAPI()
app.include_router(router_a, prefix="/a")
app.include_router(router_b, prefix="/b")

# It's ugly, but you can look inside here to confirm it worked as expected:
# * there is a "/a/" route that expects a query parameter with name "a", and
# * there is a "/b/" route that expects a query parameter with name "b"
print(app.openapi())

@bgorges does this seem to you like a good API for this?

@bgorges Okay, I've got the above working with python 3.6; you can see the added test case in #3 for reference.

I'll push a new release once I have a chance to write docs for this feature (hopefully in the next day or so).

Hmm, actually, I think I can get rid of the separate decorator, and just use @cbv(...) in both cases. It would basically be set up so that if you inherit from a class already decorated with @cbv(router) and decorate it with another router, any endpoints present in the previous decorating router are just "copied" into the new decorating router.

(I'll need to make sure this plays nicely with inheritance if you override the parent class methods, but I have some ideas.)

I've been thinking of it in terms of basic CRUD operations, maybe with the possibility of switching pydantic models within the signatures... Might be too much signature mangling at that point.

I see, that's useful context.

Given this, are you uninterested in the feature as implemented above? The Depends-replacement capability (as implemented above) seems like it could be interesting/useful even without the ability to change route signatures, but if you aren't interested in that I'll deprioritize quick release in favor of deeper thought into what's the best API here.

I think the biggest challenge with this now is that you can't change the response_model without re-declaring the decorator, so if you want to specify the response model of the endpoint you might lose the benefits.

I think this is a bigger issue with FastAPI that should be addressed there (it would be necessary to make use of the new starlette routing table approach anyway); I've been thinking about it for a while, maybe I'll make a pull request (though I suspect the change will be a little controversial at best).

I second giving it more thought. It would be very useful to make CRUD operations code fore 15-ish models much easier to maintain, but if it becomes complicated then there is no real benefit. ;)

I might have to look at starlette routing and see what that looks like. That said I am very happy to have found fastapi and your contributions, I was struggling to make marshmallow dance.

@dmontagu Hi. Can you help me please: when will this PR (#3) merge into master?

Hi,

First of all, big props to @dmontagu for providing these very useful fastapi utils.

I would really like this feature also. It's been a while since the last comment, any update on when this might make it? The PR has some conflicts, so I'm guessing there are some issues to resolve.

For context my problems are around use of Depends and Request in a base class as __init__ params, such that can be overridden e.g.

class BaseCBV:
    def __init__(
        self,
        request: Request, 
        some_dependency: SomeClass  = Depends(get_some_dependency),
    ):
        self._some_depedency = some_depedency
        self._request = request

@cbv(router)
class ChildCBV(BaseCBV):
    def __init__(self, *args, **kwargs): 
        super().__init__(*args, **kwargs)

As is, this complains about the request:
TypeError: __init__() missing 1 required positional argument: 'request'

If I move request from the BaseCBV.__init__ to the ChildCBV.__init__, I then find that the some_dependency is not resolved, resulting in somethign like this when I access it:
AttributeError: 'Depends' object has no attribute 'id'

If I simply move the BaseCBV.__init__ to the ChildCBV this all works as expected.

Thanks

Okay, it wasn't super straightforward, but I think I've mostly addressed this now in #3.

The main change is that I've added a decorator called generic_cbv which you use to decorate the generic CBV, and to which you pass the router used to decorate the generic endpoints.

I haven't tested it properly yet, but the following at least seems to generate the right OpenAPI schema:

from typing import Optional

from fastapi import APIRouter, Depends, FastAPI

from fastapi_utils.cbv import cbv, generic_cbv


def get_a(a: Optional[int]):
    return a


def get_b(b: Optional[int]):
    return b


router = APIRouter()


@generic_cbv(router)
class GenericCBV:
    service: Optional[int] = Depends(None)
    model: Optional[int] = Depends(None)

    @property
    def model_id(self):
        return f"{self.model.vertex_label()}_id"

    @router.get("/")
    async def get_model_list(self):
        return self.service.get(self.model.vertex_label())


router_a = APIRouter()
router_b = APIRouter()


@cbv(router_a)
class CBVA(GenericCBV):
    service: Optional[int] = Depends(get_a)
    model: Optional[int] = Depends(get_a)


@cbv(router_b)
class CBVB(GenericCBV):
    service: Optional[int] = Depends(get_b)
    model: Optional[int] = Depends(get_b)


app = FastAPI()
app.include_router(router_a, prefix="/a")
app.include_router(router_b, prefix="/b")

# It's ugly, but you can look inside here to confirm it worked as expected:
# * there is a "/a/" route that expects a query parameter with name "a", and
# * there is a "/b/" route that expects a query parameter with name "b"
print(app.openapi())

@bgorges does this seem to you like a good API for this?

ImportError: cannot import name 'generic_cbv' from 'fastapi_utils.cbv' (/usr/local/lib/python3.10/site-packages/fastapi_utils/cbv.py)

Hi,

First of all, big props to @dmontagu for providing these very useful fastapi utils.

I would really like this feature also. It's been a while since the last comment, any update on when this might make it? The PR has some conflicts, so I'm guessing there are some issues to resolve.

For context my problems are around use of Depends and Request in a base class as __init__ params, such that can be overridden e.g.

class BaseCBV:
    def __init__(
        self,
        request: Request, 
        some_dependency: SomeClass  = Depends(get_some_dependency),
    ):
        self._some_depedency = some_depedency
        self._request = request

@cbv(router)
class ChildCBV(BaseCBV):
    def __init__(self, *args, **kwargs): 
        super().__init__(*args, **kwargs)

As is, this complains about the request: TypeError: __init__() missing 1 required positional argument: 'request'

If I move request from the BaseCBV.__init__ to the ChildCBV.__init__, I then find that the some_dependency is not resolved, resulting in somethign like this when I access it: AttributeError: 'Depends' object has no attribute 'id'

If I simply move the BaseCBV.__init__ to the ChildCBV this all works as expected.

Thanks

router is not inhertable of class cbv only wraps class properities to dependancies
the class itself does not have [GET, POST, PUT, PATCH, HEAD, DELETE, ..., GENERIC / DEFAULT] methods
so inherting is not an option, as we don't register the class itself but registering the router

even refactoring the cbv to have those methods and give them to the router is not enough
we need to register the class instead of the router so it has the inheritance abilaty

so the solution we are looking for is a registerable class