Package: fastapi-injector
FastAPI InjectorIntegratesinjectorwithFastAPI.Github:https://github.com/matyasrichter/fastapi-injectorPyPI:https://pypi.org/project/fastapi-injector/Installationpipinstallfastapi-injectorUsageWhen creating your FastAPI app, attach the injector to it:# app.pyfromfastapiimportFastAPIfrominjectorimportInjectorfromfastapi_injectorimportattach_injectordefcreate_app(injector:Injector)->FastAPI:app=FastAPI()app.include_router(...)...attach_injector(app,injector)returnappThen, useInjectedin your routes. Under the hood,InjectedisDepends, so you can use it anywhereDependscan be used. In the following example,InterfaceTypeis
something you've bound an implementation to in your injector instance.fromfastapiimportAPIRouterfromfastapi_injectorimportInjectedrouter=APIRouter()@router.get("/")asyncdefget_root(integer:int=Injected(InterfaceType)):returnintegerA more complete example could look like this (your FastAPI code only depends onInterfaceType,
its implementation only depends on a domain layer port etc.):# ------------------------# interface.pyimportabcfromabcimportabstractmethodclassSomeInterface(abc.ABC):@abstractmethodasyncdefcreate_some_entity(self)->None:"""Creates and saves an entity."""# ------------------------# service.pyimportabcfrom.interfaceimportSomeInterfaceclassSomeSavePort(abc.ABC):@abc.abstractmethodasyncdefsave_something(self,something:Entity)->None:"""Saves an entity."""classSomeService(SomeInterface):def__init__(self,save_port:Inject[SomeSavePort]):self.save_port=save_portasyncdefcreate_some_entity(self)->None:entity=Entity(attr1=1,attr2=2)awaitself.save_port.save_something(entity)# ------------------------# repository.pyfrom.serviceimportSomeSavePortclassSomeRepository(SomeSavePort):asyncdefsave_something(self,something:Entity)->None:# code that saves the entity to the DBRequest scopeA common requirement is to have a dependency resolved to the same instance multiple times in the same request,
but to create new instances for other requests. An example usecase for this behaviour
is managing per-request DB connections.This library provides aRequestScopethat fulfills this requirement.
Under the hood, it usesContext Variablesintroduced in Python 3.7, generates a UUID4 for each request, and caches dependencies in a dictionary
with this uuid as the key.frominjectorimportInjectorfromfastapiimportFastAPIfromfastapi_injectorimportInjectorMiddleware,request_scope,attach_injectorfromfoo.barimportInterface,Implementationinj=Injector()# Use request_scope when binding the dependencyinj.binder.bind(Interface,to=Implementation,scope=request_scope)app=FastAPI()# Add the injector middleware to the app instanceapp.add_middleware(InjectorMiddleware,injector=inj)attach_injector(app,inj)Your dependencies will then be cached within a request's resolution tree.
Caching works both for top-level and nested dependencies
(e.g. when you inject a DB connection to multiple repository classes).@app.get("/")defget_root(foo:Interface=Injected(Interface),bar:Interface=Injected(Interface),):# the following assert will pass because both are the same instance.assertfooisbarOutside of FastAPI routes, you can useRequestScopeFactory.create_scope, which returns a context manager that substitutesInjectorMiddleware.
This is useful in celery tasks, cron jobs, CLI commands and other parts of your application outside of the request-response cycle.classMessageHandler:def__init__(self,request_scope_factory:Inject[RequestScopeFactory])->None:self.request_scope_factory=request_scope_factoryasyncdefhandle(self,message:Any)->None:withself.request_scope_factory.create_scope():# process messageSyncInjectedThe dependency constructed byInjectedis asynchronous. This causes it to run on the main thread. Should your usecase require a synchronous dependency, there's also an alternative -SyncInjected. Synchronous dependencies created bySyncInjectedwill be run on a separate thread from the threadpool. See theFastAPI docs on this behaviour.Dependency cleanupIn some cases a request-scoped dependency may represent a resource that you wish to clean up in a deterministic manner at the end of the request scope. This might be because the resource is leased from a finite pool of such resources (e.g. a DB connection), and failure to release the resource in a timely manner could cause resource exhaustion. Typically, these resources will implement theContextManager protocolso that they can be used with Python'swithstatement (or the async equivalent,contextlib.AbstractAsyncContextManager, which is used with theasync withstatement).This library provides an option to perform cleanup on any dependency that implements one of theContextManagerprotocols (either sync or async). To enable cleanup, setenable_cleanup=Truewhen you attach the injector, as in the following example:frominjectorimportInjectorfromfastapiimportFastAPIfromfastapi_injectorimportInjectorMiddleware,request_scope,attach_injectorfromtypingimportTextIOclassResourceFile:def__init__(self)->None:self.file=Nonedef__enter__(self)->TextIO:self.file=open('resource.txt','r')returnfiledef__exit__(self,*_args)->None:self.file.close()inj=Injector()inj.binder.bind(ResourceFile,scope=request_scope)app=FastAPI()app.add_middleware(InjectorMiddleware,injector=inj)options=RequestScopeOptions(enable_cleanup=True)attach_injector(app,inj,options)By settingenable_cleanup=Truein theRequestScopeOptions, the library ensures that theResourceFile.__exit__()function is called at the end of the request, meaning that the file resource is released.Testing with fastapi-injectorTo use your app in tests with overridden dependencies, modify the injector before each test:# ------------------------# app entrypointimportpytestfrominjectorimportInjectorapp=create_app(inj)if__name__=="__main__":uvicorn.run("app",...)# ------------------------# composition rootdefcreate_injector()->Injector:inj=Injector()# note that this still gets executed,# so if you need to get rid of a DB connection, for example,# you would need to use a callable provider.inj.binder.bind(int,1)returninj# ------------------------# testsfromfastapiimportFastAPIfromfastapi.testclientimportTestClientfrompath.to.app.factoryimportcreate_app@pytest.fixturedefapp()->FastAPI:inj=Injector()inj.binder.bind(int,2)returncreate_app(inj)defsome_test(app:FastAPI):# use test client with the new appclient=TestClient(app)ContributingAll contributions are welcome. Please raise an issue and/or open a pull request if you'd like to help to makefastapi-injectorbetter.Usepoetryto install dependenciesUsepre-committo run linters and formatters before committing and pushingWrite tests for your code