Package: asyncinject
asyncinjectRun async workflows using pytest-fixtures-style dependency injectionInstallationInstall this library usingpip:$ pip install asyncinjectUsageThis library is inspired bypytest fixtures.The idea is to simplify executing parallelasynciooperations by allowing them to be defined using a collection of functions, where the function arguments represent dependent functions that need to be executed first.The library can then create and execute a plan for executing the required functions in parallel in the most efficient sequence possible.Here's an example, using thehttpxHTTP library.fromasyncinjectimportRegistryimporthttpxasyncdefget(url):asyncwithhttpx.AsyncClient()asclient:return(awaitclient.get(url)).textasyncdefexample():returnawaitget("http://www.example.com/")asyncdefsimonwillison():returnawaitget("https://simonwillison.net/search/?tag=empty")asyncdefboth(example,simonwillison):returnexample+"\n\n"+simonwillisonregistry=Registry(example,simonwillison,both)combined=awaitregistry.resolve(both)print(combined)If you run this inipythonorpython -m asyncio(to enable top-level await in the console) you will see output that combines HTML from both of those pages.The HTTP requests towww.example.comandsimonwillison.netwill be performed in parallel.The library notices thatboth()takes two arguments which are the names of other registeredasync deffunctions, and will construct an execution plan that executes those two functions in parallel, then passes their results to theboth()method.Registry.from_dict()Passing a list of functions to theRegistryconstructor will register each function under their introspected function name, usingfn.__name__.You can set explicit names instead using a dictionary:registry=Registry.from_dict({"example":example,"simonwillison":simonwillison,"both":both})Those string names will be used to match parameters, so each function will need to accept parameters named after the keys used in that dictionary.Registering additional functionsFunctions that are registered can be regular functions orasync deffunctions.In addition to registering functions by passing them to the constructor, you can also add them to a registry using the.register()method:asyncdefanother():return"another"registry.register(another)To register them with a name other than the name of the function, pass thename=argument:asyncdefanother():return"another 2"registry.register(another,name="another_2")Resolving an unregistered functionYou don't need to register the final function that you pass to.resolve()- if you pass an unregistered function, the library will introspect the function's parameters and resolve them directly.This works with both regular and async functions:asyncdefone():return1asyncdeftwo():return2registry=Registry(one,two)# async def works here too:defthree(one,two):returnone+twoprint(awaitregistry.resolve(three))# Prints 3Parameters are passed throughYour dependent functions can require keyword arguments which have been passed to the.resolve()call:asyncdefget_param_1(param1):returnawaitget(param1)asyncdefget_param_2(param2):returnawaitget(param2)asyncdefboth(get_param_1,get_param_2):returnget_param_1+"\n\n"+get_param_2combined=awaitRegistry(get_param_1,get_param_2,both).resolve(both,param1="http://www.example.com/",param2="https://simonwillison.net/search/?tag=empty")print(combined)Parameters with default values are ignoredYou can opt a parameter out of the dependency injection mechanism by assigning it a default value:asyncdefgo(calc1,x=5):returncalc1+xasyncdefcalc1():return5print(awaitRegistry(calc1,go).resolve(go))# Prints 10Tracking with a timerYou can pass atimer=callable to theRegistryconstructor to gather timing information about executed tasks..  Your function should take three positional arguments:name- the name of the function that is being timedstart- the time that it started executing, usingtime.perf_counter()(perf_counter() docs)end- the time that it finished executingYou can useprinthere too:combined=awaitRegistry(get_param_1,get_param_2,both,timer=print).resolve(both,param1="http://www.example.com/",param2="https://simonwillison.net/search/?tag=empty")This will output:get_param_1 436633.584580685 436633.797921747
get_param_2 436633.641832699 436634.196364347
both 436634.196570217 436634.196575639Turning off parallel executionBy default, functions that can run in parallel according to the execution plan will run in parallel usingasyncio.gather().You can disable this parallel exection by passingparallel=Falseto theRegistryconstructor, or by settingregistry.parallel = Falseafter the registry object has been created.This is mainly useful for benchmarking the difference between parallel and serial execution for your project.DevelopmentTo contribute to this library, first checkout the code. Then create a new virtual environment:cd asyncinject
python -m venv venv
source venv/bin/activateNow install the dependencies and test dependencies:pip install -e '.[test]'To run the tests:pytest