5

I am trying to use class based views in my FastApi project to reduce redundancy of code. Basically I need CRUD functionality for all of my models and therefor would have to write the same routes over and over again. I created a small example project to display my progress so far, but I ran into some issues.

I know there is this Fastapi-utils but as far as I understand only reduces the number of Dependencies to call and is no longer maintained properly (last commit was March 2020).

I have some arbitrary pydantic Schema/Model. The SQLAlchemy models and DB connection are irrelevant for now.

from typing import Optional
from pydantic import BaseModel
class ObjBase(BaseModel):
 name: Optional[str]
class ObjCreate(ObjBase):
 pass
class ObjUpdate(ObjBase):
 pass
class Obj(ObjBase):
 id: int

A BaseService class is used to implement DB access. To simplify this there is no DB access right now and only get (by id) and list (all) is implemented.

from typing import Any, Generic, List, Optional, Type, TypeVar
from pydantic import BaseModel
SchemaType = TypeVar("SchemaType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class BaseService(Generic[SchemaType, CreateSchemaType, UpdateSchemaType]):
 def __init__(self, model: Type[SchemaType]):
 self.model = model
 def get(self, id: Any) -> Any:
 return {"id": id}
 def list(self, skip: int = 0, limit: int = 100) -> Any:
 return [
 {"id": 1},
 {"id": 2},
 ]

This BaseService can then be inherited by a ObjService class providing these base functions for the previously defined pydantic Obj Model.

from schemas.obj import Obj, ObjCreate, ObjUpdate
from .base import BaseService
class ObjService(BaseService[Obj, ObjCreate, ObjUpdate]):
 def __init__(self):
 super(ObjService, self).__init__(Obj)

In the init.py file in this directory a function is provided to get an ObjService instance.

from fastapi import Depends
from .obj import ObjService
def get_obj_service() -> ObjService:
 return ObjService()

So far everything is working. I can inject the Service Class into the relevant FastApi routes. But all routes need to be written for each model and CRUD function. Making it tedious when providing the same API endpoints for multiple models/schemas. Therefor my thought was to use something similar to the logic behind the BaseService by providing a BaseRouter which defines these routes and inherit from that class for each model.

The BaseRouter class:

from typing import Generic, Type, TypeVar
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from services.base import BaseService
SchemaType = TypeVar("SchemaType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class BaseRouter(Generic[SchemaType, CreateSchemaType, UpdateSchemaType]):
 def __init__(self, schema: Type[SchemaType], prefix: str, service: BaseService):
 self.schema = schema
 self.service = service
 
 self.router = APIRouter(
 prefix=prefix
 )
 self.router.add_api_route("/", self.list, methods=['GET'])
 self.router.add_api_route("/{id}", self.get, methods=['GET'])
 def get(self, id):
 return self.service.get(id)
 def list(self):
 return self.service.list()

The ObjRouter class:

from schemas.obj import Obj, ObjCreate, ObjUpdate
from .base import BaseRouter
from services.base import BaseService
class ObjRouter(BaseRouter[Obj, ObjCreate, ObjUpdate]):
 def __init__(self, prefix: str, service: BaseService):
 super(ObjRouter, self).__init__(Obj, prefix, service)

The init.py file in that directory

from fastapi import Depends
from services import get_obj_service
from services.obj import ObjService
from .obj import ObjRouter
def get_obj_router(service: ObjService = Depends(get_obj_service())) -> ObjRouter:
 return ObjRouter("/obj", service).router

In my main.py file this router is added to the FastApi App.

from fastapi import Depends, FastAPI
from routes import get_obj_router
app = FastAPI()
app.include_router(get_obj_router())

When starting the app the routes Get "/obj" and Get "/obj/id" show up in my Swagger Docs for the project. But when testing one of the endpoints I am getting an AttributeError: 'Depends' object has no attribute 'list'

As far as I understand Depends can only be used in FastApi functions or functions that are dependecies themselves. Therefor I tried altering the app.include_router line in my main.py by this

app.include_router(Depends(get_obj_router()))

But it again throws an AttributeError: 'Depends' object has no attribute 'routes'.

Long story short question: What am I doing wrong? Is this even possible in FastApi or do I need to stick to defining the same CRUD Api Endpoints over and over again?

The reason I want to use the Dependenvy Injection capabilities of FastApi is that later I will use the following function call in my Service classes to inject the DB session and automatically close it after the request:

def get_db():
 db = SessionLocal()
 try:
 yield db
 finally:
 db.close()

As far as I understand this is only possible when the highest call in the dependency hierachy (Route depends on Service depends on get_db) is done by a FastApi Route.

PS: This is my first question on StackOverflow, please be gentle.

asked Jan 26, 2023 at 16:42
2
  • Hey I am just trying to recreate your problem, but I believe you miscopied the ObjectRouter. It seems to be the ObjectService instead. Could you update your question? Commented Jan 26, 2023 at 17:10
  • I updated the code, it should now be correct. Commented Jan 27, 2023 at 9:09

1 Answer 1

6

since your question is understandably very long, I will post a full working example at the bottom of this answer.

Dependencies in FastAPI are callables that can modify an endpoints parameters and pass values down to them. In the api model they work in the endpoint level. To pass-on any dependency results you need to explicitly pass them to the controller function.

In the example below I have created a dummy Session class and a dummy session injection function (injecting_session). Then I have added this dependency to the BaseRouter functions get and list and passed the result on to the BaseObject class get and list functions.

As promised; A fully working example:

from typing import Optional, TypeVar, Type, Generic, Any, Union, Sequence
from fastapi import Depends, APIRouter, FastAPI
from pydantic import BaseModel
class ObjBase(BaseModel):
 name: Optional[str]
class ObjCreate(ObjBase):
 pass
class ObjUpdate(ObjBase):
 pass
class Obj(ObjBase):
 id: int
SchemaType = TypeVar("SchemaType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class Session:
 def __str__(self):
 return "I am a session!"
async def injecting_session():
 print("Creating Session")
 return Session()
class BaseService(Generic[SchemaType, CreateSchemaType, UpdateSchemaType]):
 def __init__(self, model: Type[SchemaType]):
 self.model = model
 def get(self, id: Any, session: Session) -> Any:
 print(session)
 return {"id": id}
 def list(self, session: Session) -> Any:
 print(session)
 return [
 {"id": 1},
 {"id": 2},
 ]
class ObjService(BaseService[Obj, ObjCreate, ObjUpdate]):
 def __init__(self):
 super(ObjService, self).__init__(Obj)
def get_obj_service() -> ObjService:
 return ObjService()
SchemaType2 = TypeVar("SchemaType2", bound=BaseModel)
CreateSchemaType2 = TypeVar("CreateSchemaType2", bound=BaseModel)
UpdateSchemaType2 = TypeVar("UpdateSchemaType2", bound=BaseModel)
class BaseRouter(Generic[SchemaType2, CreateSchemaType2, UpdateSchemaType2]):
 def __init__(self, schema: Type[SchemaType2], prefix: str, service: BaseService):
 self.schema = schema
 self.service = service
 self.router = APIRouter(
 prefix=prefix
 )
 self.router.add_api_route("/", self.list, methods=['GET'])
 self.router.add_api_route("/{id}", self.get, methods=['GET'])
 def get(self, id, session=Depends(injecting_session)):
 return self.service.get(id, session)
 def list(self, session=Depends(injecting_session)):
 return self.service.list(session)
class ObjRouter(BaseRouter[Obj, ObjCreate, ObjUpdate]):
 def __init__(self, path, service):
 super(ObjRouter, self).__init__(Obj, path, service)
def get_obj_router(service=get_obj_service()) -> APIRouter: # returns API router now
 return ObjRouter("/obj", service).router
app = FastAPI()
app.include_router(get_obj_router())

By adding parameters to injecting_session() you can add parameters to all endpoints that use the dependency.

answered Jan 26, 2023 at 17:26
Sign up to request clarification or add additional context in comments.

10 Comments

Thanks for your answer. I updated my question above. I would like to use the Dependency Injection functionality of FastApi because this then makes it easier using the DB session etc. Therfore dropping the Depends is not an option.
Hey, I just updated my answer based on your comment. You want to add a dependency when you add the api route. Check BaseRouter in my updated code. In this example I am just injecting some parameters like the example on the fastapi website, but you can do whatever you want from here.
How can these parameters be accessed in the get() function for example?
I honestly still don't exactly understand what you are trying to do, but I just updated my answer again. This time showing how you could use a dependency injection to get a session to the controller function. If you are only looking to get a session to the controllers it might be more useful to use a decorator that passes a session to your function instead.
@atlasloewenherz I mainly followed this tutorial: patrick-muehlbauer.com/articles/fastapi-with-sqlalchemy. Maybe you can take a look
|

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.