Package: limiter
⏲️ Easy rate limiting for Pythonlimitermakes it easy to addrate limitingto Python projects, using
atoken bucketalgorithm.limitercan provide Python projects and
scripts with:Rate limiting thread-safedecoratorsRate limiting async decoratorsRate limiting thread-safecontext managersRate
limitingasync context managersHere are some features and benefits of usinglimiter:Easily control burst and average request ratesIt
isthread-safe, with no need for a timer threadIt addsjitterto help with contentionIt has a simple API that takes advantage of Python's features, idioms
andtype hintingExampleHere's an example of using a limiter as a decorator and context manager:fromaiohttpimportClientSessionfromlimiterimportLimiterlimit_downloads=Limiter(rate=2,capacity=5,consume=2)@limit_downloadsasyncdefdownload_image(url:str)->bytes:asyncwithClientSession()assession,session.get(url)asresponse:returnawaitresponse.read()asyncdefdownload_page(url:str)->str:asyncwith(ClientSession()assession,limit_downloads,session.get(url)asresponse):returnawaitresponse.text()UsageYou can define limiters and use them dynamically across your project.Note: If you're using Python version3.9.xor below, check
outthe documentation for version0.2.0oflimiterhere.LimiterinstancesLimiterinstances takerate,capacityandconsumearguments.rateis the token replenishment rate per second. Tokens are automatically added every second.consumeis the amount of tokens consumed from the token bucket upon successfully taking tokens from the bucket.capacityis the total amount of tokens the token bucket can hold. Token replenishment stops when this capacity is
reached.Limiting blocks of codelimitercan rate limit all Python callables, and limiters can be used as context managers.You can define a limiter with a set refreshrateand total tokencapacity. You can set the amount of tokens to
consume dynamically withconsume, and thebucketparameter sets the bucket to consume tokens from:fromlimiterimportLimiterREFRESH_RATE:int=2BURST_RATE:int=3MSG_BUCKET:str='messages'limiter:Limiter=Limiter(rate=REFRESH_RATE,capacity=BURST_RATE)limit_msgs:Limiter=limiter(bucket=MSG_BUCKET)@limiterdefdownload_page(url:str)->bytes:...@limiter(consume=2)asyncdefdownload_page(url:str)->bytes:...defsend_page(page:bytes):withlimiter(consume=1.5,bucket=MSG_BUCKET):...asyncdefsend_page(page:bytes):asyncwithlimit_msgs:...@limit_msgs(consume=3)defsend_email(to:str):...asyncdefsend_email(to:str):asyncwithlimiter(bucket=MSG_BUCKET):...In the example above, bothlimiterandlimit_msgsshare the same limiter. The only difference is thatlimit_msgswill take tokens from theMSG_BUCKETbucket by default.assertlimiter.limiterislimit_msgs.limiterassertlimiter.bucket!=limit_msgs.bucketassertlimiter!=limit_msgsCreating new limitersYou can reuse existing limiters in your code, and you can create new limiters from the parameters of an existing limiter
using thenew()method.Or, you can define a new limiter entirely:# you can reuse existing limiterslimit_downloads:Limiter=limiter(consume=2)# you can use the settings from an existing limiter in a new limiterlimit_downloads:Limiter=limiter.new(consume=2)# or you can simply define a new limiterlimit_downloads:Limiter=Limiter(REFRESH_RATE,BURST_RATE,consume=2)@limit_downloadsdefdownload_page(url:str)->bytes:...@limit_downloadsasyncdefdownload_page(url:str)->bytes:...defdownload_image(url:str)->bytes:withlimit_downloads:...asyncdefdownload_image(url:str)->bytes:asyncwithlimit_downloads:...Let's look at the difference between reusing an existing limiter, and creating new limiters with thenew()method:limiter_a:Limiter=limiter(consume=2)limiter_b:Limiter=limiter.new(consume=2)limiter_c:Limiter=Limiter(REFRESH_RATE,BURST_RATE,consume=2)assertlimiter_a!=limiterassertlimiter_a!=limiter_b!=limiter_cassertlimiter_a!=limiter_bassertlimiter_a.limiterislimiter.limiterassertlimiter_a.limiterisnotlimiter_b.limiterassertlimiter_a.attrs==limiter_b.attrs==limiter_c.attrsThe only things that are equivalent between the three new limiters above are the limiters' attributes, like
therate,capacity, andconsumeattributes.Creating anonymous, or single-use, limitersYou don't have to assignLimiterobjects to variables. Anonymous limiters don't share a token bucket like named
limiters can. They work well when you don't have a reason to share a limiter between two or more blocks of code, and
when a limiter has a single or independent purpose.limiter, after versionv0.3.0, ships with alimittype alias forLimiter:fromlimiterimportlimit@limit(capacity=2,consume=2)asyncdefsend_message():...asyncdefupload_image():asyncwithlimit(capacity=3)aslimiter:...The above is equivalent to the below:fromlimiterimportLimiter@Limiter(capacity=2,consume=2)asyncdefsend_message():...asyncdefupload_image():asyncwithLimiter(capacity=3)aslimiter:...BothlimitandLimiterare the same object:assertlimitisLimiterJitterALimiter'sjitterargument adds jitter to help with contention.The value is inunits, which is milliseconds by default, and can be any of these:False, to add no jitter. This is the default.True, to add a random amount of jitter.A number, to add a fixed amount of jitter.Arangeobject, to add a random amount of jitter within the range.Atupleof two numbers,startandstop, to add a random amount of jitter between the two numbers.Atupleof three numbers:start,stopandstep, to add jitter like you would withrange.For example, if you want to use a random amount of jitter between0and100milliseconds:limiter=Limiter(rate=2,capacity=5,consume=2,jitter=(0,100))limiter=Limiter(rate=2,capacity=5,consume=2,jitter=(0,100,1))limiter=Limiter(rate=2,capacity=5,consume=2,jitter=range(0,100))limiter=Limiter(rate=2,capacity=5,consume=2,jitter=range(0,100,1))All of the above are equivalent to each other in function.You can also supply values forjitterwhen using decorators or context-managers:limiter=Limiter(rate=2,capacity=5,consume=2)@limiter(jitter=range(0,100))defdownload_page(url:str)->bytes:...asyncdefdownload_page(url:str)->bytes:asyncwithlimiter(jitter=(0,100)):...You can use the above to override default values ofjitterin aLimiterinstance.To add a small amount of random jitter, supplyTrueas the value:limiter=Limiter(rate=2,capacity=5,consume=2,jitter=True)# or@limiter(jitter=True)defdownload_page(url:str)->bytes:...To turn off jitter in aLimiterconfigured with jitter, you can supplyFalseas the value:limiter=Limiter(rate=2,capacity=5,consume=2,jitter=range(10))@limiter(jitter=False)defdownload_page(url:str)->bytes:...asyncdefdownload_page(url:str)->bytes:asyncwithlimiter(jitter=False):...Or create a new limiter with jitter turned off:limiter:Limiter=limiter.new(jitter=False)Unitsunitsis a number representing the amount of units in one second. The default value is1000for 1,000 milliseconds in one second.Similar tojitter,unitscan be supplied at all the same call sites and constructors thatjitteris accepted.If you want to use a different unit than milliseconds, supply a different value forunits.InstallationRequirementsPython 3.10+ for versions0.3.0and upPython 3.7+ for versions below0.3.0Install via PyPI$python3-mpipinstalllimiterLicenseSeeLICENSE. If you'd like to use this project with a different license, please get in touch.