- Notifications
You must be signed in to change notification settings - Fork3
⏲️ Easy rate limiting for Python using a token bucket algorithm, with async and thread-safe decorators and context managers
License
alexdelorenzo/limiter
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
limiter
makes it easy to addrate limiting to Python projects, usingatoken bucket algorithm.limiter
can provide Python projects andscripts with:
- Rate limiting thread-safedecorators
- Rate limiting async decorators
- Rate limiting thread-safecontext managers
- Ratelimitingasync context managers
Here are some features and benefits of usinglimiter
:
- Easily control burst and average request rates
- Itisthread-safe, with no need for a timer thread
- It addsjitter to help with contention
- It has a simple API that takes advantage of Python's features, idiomsandtype hinting
Here'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()
You can define limiters and use them dynamically across your project.
Note: If you're using Python version3.9.x
or below, checkoutthe documentation for version0.2.0
oflimiter
here.
Limiter
instances takerate
,capacity
andconsume
arguments.
rate
is the token replenishment rate per second. Tokens are automatically added every second.consume
is the amount of tokens consumed from the token bucket upon successfully taking tokens from the bucket.capacity
is the total amount of tokens the token bucket can hold. Token replenishment stops when this capacity isreached.
limiter
can rate limit all Python callables, and limiters can be used as context managers.
You can define a limiter with a set refreshrate
and total tokencapacity
. You can set the amount of tokens toconsume dynamically withconsume
, and thebucket
parameter 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, bothlimiter
andlimit_msgs
share the same limiter. The only difference is thatlimit_msgs
will take tokens from theMSG_BUCKET
bucket by default.
assertlimiter.limiterislimit_msgs.limiterassertlimiter.bucket!=limit_msgs.bucketassertlimiter!=limit_msgs
You can reuse existing limiters in your code, and you can create new limiters from the parameters of an existing limiterusing 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.attrs
The only things that are equivalent between the three new limiters above are the limiters' attributes, liketherate
,capacity
, andconsume
attributes.
You don't have to assignLimiter
objects to variables. Anonymous limiters don't share a token bucket like namedlimiters can. They work well when you don't have a reason to share a limiter between two or more blocks of code, andwhen a limiter has a single or independent purpose.
limiter
, after versionv0.3.0
, ships with alimit
type 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: ...
Bothlimit
andLimiter
are the same object:
assertlimitisLimiter
ALimiter
'sjitter
argument 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 between0
and50
milliseconds.- A number, to add a fixed amount of jitter.
- A
range
object, to add a random amount of jitter within the range. - A
tuple
of two numbers,start
andstop
, to add a random amount of jitter between the two numbers. - A
tuple
of three numbers:start
,stop
andstep
, to add jitter like you would withrange
.
For example, if you want to use a random amount of jitter between0
and100
milliseconds:
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 forjitter
when 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 ofjitter
in aLimiter
instance.
To add a small amount of random jitter, supplyTrue
as 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 aLimiter
configured with jitter, you can supplyFalse
as 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)
units
is a number representing the amount of units in one second. The default value is1000
for 1,000 milliseconds in one second.
Similar tojitter
,units
can be supplied at all the same call sites and constructors thatjitter
is accepted.
If you want to use a different unit than milliseconds, supply a different value forunits
.
- Python 3.10+ for versions
0.3.0
and up - Python 3.7+ for versions below
0.3.0
$ python3 -m pip install limiter
SeeLICENSE
. If you'd like to use this project with a different license, please get in touch.
About
⏲️ Easy rate limiting for Python using a token bucket algorithm, with async and thread-safe decorators and context managers