Package: aiometer
aiometeraiometeris a concurrency scheduling library compatible withasyncioandtrioand inspired byTrimeter. It makes it easier to execute lots of tasks concurrently while controlling concurrency limits (i.e. applyingbackpressure) and collecting results in a predictable manner.ContentExampleFeaturesInstallationUsageFlow controlRunning tasksHow ToAPI ReferenceContributingLicenseExampleLet's useHTTPXto make web requests concurrently...Try this code interactively usingIPython.>>>importasyncio>>>importfunctools>>>importrandom>>>importaiometer>>>importhttpx>>>>>>client=httpx.AsyncClient()>>>>>>asyncdeffetch(client,request):...response=awaitclient.send(request)...# Simulate extra processing......awaitasyncio.sleep(2*random.random())...returnresponse.json()["json"]...>>>requests=[...httpx.Request("POST","https://httpbin.org/anything",json={"index":index})...forindexinrange(100)...]...>>># Send requests, and process responses as they're made available:>>>asyncwithaiometer.amap(...functools.partial(fetch,client),...requests,...max_at_once=10,# Limit maximum number of concurrently running tasks....max_per_second=5,# Limit request rate to not overload the server....)asresults:...asyncfordatainresults:...print(data)...{'index':3}{'index':4}{'index':1}{'index':2}{'index':0}...>>># Alternatively, fetch and aggregate responses into an (ordered) list...>>>jobs=[functools.partial(fetch,client,request)forrequestinrequests]>>>results=awaitaiometer.run_all(jobs,max_at_once=10,max_per_second=5)>>>results[{'index':0},{'index':1},{'index':2},{'index':3},{'index':4},...]InstallationThis project is in beta and maturing. Be sure to pin any dependencies to the latest minor.pipinstall"aiometer==0.5.*"FeaturesConcurrency management and throttling helpers.asyncioandtriosupport.Fully type annotated.100% test coverage.UsageFlow controlThe key highlight ofaiometeris allowing you to apply flow control strategies in order to limit the degree of concurrency of your programs.There are two knobs you can play with to fine-tune concurrency:max_at_once: this is used to limit the maximum number of concurrently running tasks at any given time. (If you have 100 tasks and setmax_at_once=10, thenaiometerwill ensure that no more than 10 run at the same time.)max_per_second: this option limits the number of tasks spawned per second. This is useful to not overload I/O resources, such as servers that may have a rate limiting policy in place.Example usage:>>>importasyncio>>>importaiometer>>>asyncdefmake_query(query):...awaitasyncio.sleep(0.05)# Simulate a database request....>>>queries=['SELECT * from authors']*1000>>># Allow at most 5 queries to run concurrently at any given time:>>>awaitaiometer.run_on_each(make_query,queries,max_at_once=5)...>>># Make at most 10 queries per second:>>>awaitaiometer.run_on_each(make_query,queries,max_per_second=10)...>>># Run at most 10 concurrent jobs, spawning new ones at least every 5 seconds:>>>asyncdefjob(id):...awaitasyncio.sleep(10)# A very long task....>>>awaitaiometer.run_on_each(job,range(100),max_at_once=10,max_per_second=0.2)Running tasksaiometerprovides 4 different ways to run tasks concurrently in the form of 4 different run functions. Each function accepts all the options documented inFlow control, and runs tasks in a slightly different way, allowing to address a variety of use cases. Here's a handy table for reference (see also theAPI Reference):EntrypointUse caserun_on_each()Execute async callbacks in any order.run_all()Return results as an ordered list.amap()Iterate over results as they become available.run_any()Return result of first completed function.To illustrate the behavior of each run function, let's first setup a hello world async program:>>>importasyncio>>>importrandom>>>fromfunctoolsimportpartial>>>importaiometer>>>>>>asyncdefget_greeting(name):...awaitasyncio.sleep(random.random())# Simulate I/O...returnf"Hello,{name}"...>>>asyncdefgreet(name):...greeting=awaitget_greeting(name)...print(greeting)...>>>names=["Robert","Carmen","Lucas"]Let's start withrun_on_each(). It executes an async function once for each item in a list passed as argument:>>>awaitaiometer.run_on_each(greet,names)'Hello, Robert!''Hello, Lucas!''Hello, Carmen!'If we'd like to get the list of greetings in the same order asnames, in a fashion similar toPromise.all(), we can userun_all():>>>awaitaiometer.run_all([partial(get_greeting,name)fornameinnames])['Hello, Robert','Hello, Carmen!','Hello, Lucas!']amap()allows us to process each greeting as it becomes available (which means maintaining order is not guaranteed):>>>asyncwithaiometer.amap(get_greeting,names)asgreetings:...asyncforgreetingingreetings:...print(greeting)'Hello, Lucas!''Hello, Robert!''Hello, Carmen!'Lastly,run_any()can be used to run async functions until the first one completes, similarly toPromise.any():>>>awaitaiometer.run_any([partial(get_greeting,name)fornameinnames])'Hello, Carmen!'As a last fun example, let's useamap()to implement a no-threads async version ofsleep sort:>>>importasyncio>>>fromfunctoolsimportpartial>>>importaiometer>>>numbers=[0.3,0.1,0.6,0.2,0.7,0.5,0.5,0.2]>>>asyncdefprocess(n):...awaitasyncio.sleep(n)...returnn...>>>asyncwithaiometer.amap(process,numbers)asresults:...sorted_numbers=[nasyncforninresults]...>>>sorted_numbers[0.1,0.2,0.2,0.3,0.5,0.5,0.6,0.7]How ToMultiple parametrized values inrun_on_eachandamaprun_on_eachandamaponly accept functions that accept a single positional argument (i.e.(Any) -> Awaitable).So if you have a function that is parametrized by multiple values, you should refactor it to match this form.This can generally be achieved like this:Build a proxy container type (eg. anamedtuple), egT.Refactor your function so that its signature is now(T) -> Awaitable.Build a list of these proxy containers, and pass it toaiometer.For example, assuming you have a function that processes X/Y coordinates...asyncdefprocess(x:float,y:float)->None:passxs=list(range(100))ys=list(range(100))forx,yinzip(xs,ys):awaitprocess(x,y)You could use it withamapby refactoring it like this:fromtypingimportNamedTuple# Proxy container type:classPoint(NamedTuple):x:floaty:float# Rewrite to accept a proxy as a single positional argument:asyncdefprocess(point:Point)->None:x=point.xy=point.y...xs=list(range(100))ys=list(range(100))# Build a list of proxy containers:points=[Point(x,y)forx,yinzip(x,y)]# Use it:asyncwithaiometer.amap(process,points)asresults:...API ReferenceCommon optionsmax_at_once(Optional,int): the maximum number of concurrently running tasks at any given time.max_per_second(Optional,int): the maximum number of tasks spawned per second.aiometer.run_on_each()Signature:asyncaiometer.run_on_each(async_fn,args, *,max_at_once=None,max_per_second=None) ->NoneConcurrently run the equivalent ofasync_fn(arg) for arg in args. Does not return any value. To get return values back, useaiometer.run_all().aiometer.run_all()Signature:asyncaiometer.run_all(async_fns,max_at_once=None,max_per_second=None) ->listConcurrently run theasync_fnsfunctions, and return the list of results in the same order.aiometer.amap()Signature:asyncaiometer.amap(async_fn,args,max_at_once=None,max_per_second=None) ->async iteratorConcurrently run the equivalent ofasync_fn(arg) for arg in args, and return an async iterator that yields results as they become available.aiometer.run_any()Signature:asyncaiometer.run_any(async_fns,max_at_once=None,max_per_second=None) ->AnyConcurrently run theasync_fnsfunctions, and return the first available result.ContributingSeeCONTRIBUTING.md.LicenseMITChangelogAll notable changes to this project will be documented in this file.The format is based onKeep a Changelog.0.5.0 - 2023-12-11RemovedDrop support for Python 3.7, as it has reached EOL. (Pull #44)AddedAdd official support for Python 3.12. (Pull #44)Add support for anyio 4. This allows catching exception groups using the native ExceptionGroup. On anyio 3.2+, anyio would throw its own ExceptionGroup type. Compatibility with anyio 3.2+ is retained. (Pull #43)0.4.0 - 2023-01-18RemovedDrop support for Python 3.6, which has reached EOL. (Pull #38)AddedAdd official support for Python 3.10 and 3.11. (Pull #38)FixedRelax version requirements fortyping_extensionsand addressmypy>=0.981strict optional changes. (Pull #38)0.3.0 - 2021-07-06ChangedUpdateanyiodependency to v3 (previously v1). (Pull #25)NB: no API change, but dependency mismatches may occur. Be sure to port your codebase to anyio v3 before upgradingaiometer.AddedAdd support for Python 3.6 (installs thecontextlib2backport library there). (Pull #26)Officialize support for Python 3.9. (Pull #26)0.2.1 - 2020-03-26FixedImprove robustness of themax_per_secondimplementation by using the generic cell rate algorithm (GCRA) instead of leaky bucket. (Pull #5)0.2.0 - 2020-03-22AddedAdd support for Python 3.7. (Pull #3)0.1.0 - 2020-03-21AddedAddrun_on_each(),run_all(),amap()andrun_any(), withmax_at_onceandmax_per_secondoptions. (Pull #1)