Package: classy-fastapi
OverviewThis project contains classes and decorators to use FastAPI with "class based routing". In particular this allows you to
construct aninstanceof a class and have methods of that instance be route handlers. For example:fromdaoimportDao# Some fictional daofromclassy_fastapiimportRoutable,get,deletedefparse_arg()->argparse.Namespace:"""parse command line arguments."""...classUserRoutes(Routable):"""Inherits from Routable."""# Note injection here by simply passing values to the constructor. Other injection frameworks also# supported as there's nothing special about this __init__ method.def__init__(self,dao:Dao)->None:"""Constructor. The Dao is injected here."""super().__init__()self.__dao=Dao@get('/user/{name}')defget_user_by_name(self,name:str)->User:# Use our injected DAO instance.returnself.__dao.get_user_by_name(name)@delete('/user/{name}')defdelete_user(self,name:str)->None:self.__dao.delete(name)defmain():args=parse_args()# Configure the DAO per command line argumentsdao=Dao(args.url,args.user,args.password)# Simple intuitive injectionuser_routes=UserRoutes(dao)app=FastAPI()# router member inherited from cr.Routable and configured per the annotations.app.include_router(user_routes.router)Note that there are no global variables and dependency injection is accomplished by simply passing arguments to the
constructor.WhyFastAPI generally has one define routes like:app=FastAPI()@app.get('/echo/{x}')defecho(x:int)->int:returnxNote thatappis a global. Furthermore,FastAPI's suggested way of doing dependency
injectionis handy for things like pulling
values out of header in the HTTP request. However, they don't work well for more standard dependency injection scenarios
where we'd like to do something like inject a DAO or database connection. For that, FastAPI suggeststheir
parameterized dependencieswhich might look something
like:app=FastAPI()classValueToInject:def__init__(self,y:int)->None:self.y=ydef__call__(self)->int:returnself.yto_add=ValueToInject(2)@app.get('/add/{x}')defadd(x:int,y:Depends(to_add))->int:returnx+yThis works but there's a few issues:TheDependencymust be a callable which requires an unfortunate amount of boilerplate.If we want to use the same dependency on several routes, as we would with something like a database connection, we
have to repeat theDependency(to_add)bit on each endpoint. Note that FastAPI lets you group endpoints your we caninclude the dependency on all of thembut then there's
no way to access the dependency from the router code so this really only works for things like authentication where
the dependency can do some route handling (e.g. return a 402 if an auth header is missing).to_addis a global variable which is limiting.Let's consider an expanded, more realistic example where we have a group of routes that operate on users to add them,
delete them, change the password, etc. Those routes will need to access a database so we have a DAO that helps set that
up. We're going to take the database URL, password, etc. via command line arguments and then set up our routes.
Furthermore, we'll split up our application into a few separate files. Doing this without class routing looks like the
following:# main.pyimport.userfrom.depsimportdaodefparse_arg()->argparse.Namespace:"""parse command line arguments."""...defmain():args=parse_args()globaldaodao=Dao(args.url,args.user,args.password)app=FastAPI()app.include_router(user.router)##### dao.pyfromdaoimportDao# DAO for injection. We don't know the command line arguments yet but we need to make this global as we need to be able# to access it in user.py below so it's None here and gets set in main()dao:Optional[Dao]=None###### user.pyfrom.depsimportdaofromdaoimportDaofromfastapi.routingimportAPIRouter@router.get('/user/{name}')defget_user_by_name(name:str,dao:Dao=Depends(dao))->User:returndao.get_user_by_name(name)@router.delete('/user/{name}')defdelete_user(name:str,dao:Dao=Depends(dao))->None:dao.delete(name)# ... additional user methods ...That works but it's a bit verbose. Additionally, as noted above, it has some limitations. For example, suppose we've
updated our API in a breaking way so we've added a/v2set of routes. However, theusers.pyroutes haven't changed
at all except that we've changed how we store users (e.g. a new password hashing algorithm) so/v2user routes need to
use a different DAO. Ideally you'd callapp.include_routertwice with different prefixes but that won't work because
the dependency on the DAO is toa specific DAO instanceinuser.py. You can adddependency
overridesbut it feels awkward.By contrast the class based routing in this package does not have any global variables at all and injection can be
performed by simply passing values to a constructor or via any other dependency injection framework.AlternativesFastAPI-utilshas a class based views
implementation but the routes are on the class itself rather than oninstancesof the class.There's demand for this feature so a number of alternatives have been proposedin an open
bugandon
StackOverflowbut all seem to require global injection or hacks like
defining all the routes inside the constructor.Older Versions of PythonUnfortunately this does not work withasyncroutes with Python versions less than 3.8due to bugs ininspect.iscoroutinefunction. Specifically with older versions of Pythoniscoroutinefunctionincorrectly returns false soasyncroutes aren'tawait'd. We therefore only support Python
versions >= 3.8