Package: fastapi-magic-di
FastAPI Magic DIDependency Injector for FastAPI that makes your life easierWhat are the problems with FastAPI’s dependency injector?It forces you to use global variables.You need to write an endless number of fabrics with startup logicIt makes your project highly dependent on FastAPI’s injector by using “Depends” everywhere.To solve these problems, you can use this dead-simple Dependency Injector that will make development so much easier.Q: But why not to usepython-dependency-injectoror other libs?A: The goal of this Dependency Injector is toreduce the amount of code as much as possibleand get rid of enterprise code with millions of configs, containers, and fabrics. That’s why python-dependency-injector and similar libraries are overkill. The philosophy of this injector is that clients know how to configure themselves and perform all startup routines.InstallGetting StartedClients ConfigurationZero config clientsClients with ConfigUsing interfaces instead of implementationsTestingInstallpipinstallfastapi-magic-diGetting StartedfromfastapiimportFastAPIfromfastapi_diimportProvide,ClientProtocolfromfastapi_di.appimportinject_appapp=inject_app(FastAPI())classDatabase:connected:bool=Falsedef__connect__(self):self.connected=Truedef__disconnect__(self):self.connected=FalseclassService(ClientProtocol):def__init__(self,db:Database):self.db=dbdefis_connected(self):returnself.db.connected@app.get(path="/hello-world")defhello_world(service:Provide[Service])->dict:return{"is_connected":service.is_connected()}That's all!
his simple code will recursively inject all dependencies and connect them using the__connect__and__disconnect__magic methods.But what happened there?We created a new FastAPI app and injected it. Theinject_appfunction makes the injector connect all clients on app startup and disconnect them on shutdown. That’s how you can open and close all connections (e.g., session to DB).We defined new classes with__connect__and__disconnect__magic methods.That’s how the injector finds classes that need to be injected. The injector uses duck typing to check if some class has these methods. It means you don’t need to inherit fromClientProtocol(but you can to reduce the number of code lines).Wrapped theServicetype hint intoProvideso that FastAPI can use our DI.Please note: you need to useProvideonly in FastAPI endpoints, which makes your codebase independent from FastAPI and this Dependency Injector.PROFIT!As you can see, in this example, you don’t need to write special constructors to store your dependencies in global variables. All you need to do to complete the startup logic is to write it in the__connect__method.Clients ConfigurationThis dependency injector promotes the idea of ‘zero-config clients’, but you can still use configurations if you preferZero config clientsSimply fetch everything needed from the environment. There is no need for an additional configuration file. In this case, the library includes the env_fields module to simplify zero-client developmentfromdataclassesimportdataclass,fieldfromfastapi_di.env_fieldsimportfield_str,field_boolfromredis.asyncioimportRedisasRedisClient,from_url@dataclassclassRedis:url:str=field_str("REDIS_URL")decode_responses:bool=field_bool("REDIS_DECODE_RESPONSES")client:RedisClient=field(init=False)asyncdef__connect__(self):self.client=awaitfrom_url(self.url,decode_responses=self.decode_responses)awaitself.client.ping()asyncdef__disconnect__(self):awaitself.client.close()@propertydefdb(self)->RedisClient:returnself.clientJust use thefield_*functions in dataclasses to fetch variables from the environment and cast them to the required data type.Clients with ConfigInject config as dependency :)fromdataclassesimportdataclass,fieldfromfastapi_diimportClientProtocolfromredis.asyncioimportRedisasRedisClient@dataclassclassRedisConfig(ClientProtocol):url:str="SOME_URL"decode_responses:bool=TrueclassRedis:db:RedisClientdef__init__(self,config:RedisConfig):self.db=RedisClient(config.url,decode_responses=config.decode_responses)asyncdef__connect__(self):awaitself.db.ping()asyncdef__disconnect__(self):awaitself.db.close()Using interfaces instead of implementationsSometimes, you may not want to stick to a certain interface implementation everywhere. Therefore, you can use interfaces (protocols, abstract classes) with Dependency Injection (DI). With DI, you can effortlessly bind an implementation to an interface and subsequently update it if necessary.fromtypingimportProtocolfromfastapiimportFastAPIfromfastapi_diimportProvide,ClientProtocol,injectorfromfastapi_di.appimportinject_appclassMyInterface(Protocol):defdo_something(self)->bool:...classMyInterfaceImplementation(ClientProtocol):defdo_something(self)->bool:returnTrueapp=inject_app(FastAPI())injector.bind({MyInterface:MyInterfaceImplementation})@app.get(path="/hello-world")defhello_world(service:Provide[MyInterface])->dict:return{"result":service.do_something(),}Usinginjector.bind, you can bind implementations that will be injected everywhere the bound interface is used.TestingIf you need to mock a dependency in tests, you can easily do so by using theinjector.overridecontext manager and still use this dependency injector.To mock clients, you can useClientMockfrom thetestingmodule.fromfastapi_diimportinjectorfromfastapi_di.testingimportClientMockdeftest_http_handler(client):service_mock=ClientMock()withinjector.override({Service:service_mock.mock_cls}):resp=client.post('/hello-world')assertresp.status_code==200