Package: pyrate-limiter-concord
PyrateLimiterThe request rate limiter using Leaky-bucket algorithm.Full project documentation can be found atpyratelimiter.readthedocs.io.ContentsPyrateLimiterContentsFeaturesInstallationBasic usageDefining rate limitsApplying rate limitsIdentitiesHandling exceeded limitsBucket analogyRate limit exceptionsRate limit delaysAdditional usage optionsDecoratorContextmanagerAsync decorator/contextmanagerBackendsMemorySQLiteRedisCustom backendsAdditional featuresTime sourcesExamplesFeaturesTracks any number of rate limits and intervals you want to defineIndependently tracks rate limits for multiple services or resourcesHandles exceeded rate limits by either raising errors or adding delaysSeveral usage options including a normal function call, a decorator, or a contextmanagerAsync supportIncludes optional SQLite and Redis backends, which can be used to persist limit tracking across
multiple threads, processes, or application restartsInstallationInstall using pip:pip install pyrate-limiterOr using conda:conda install --channel conda-forge pyrate-limiterBasic usageDefining rate limitsConsider some public API (like LinkedIn, GitHub, etc.) that has rate limits like the following:- 500 requests per hour
- 1000 requests per day
- 10000 requests per monthYou can define these rates using theRequestRateclass, and add them to aLimiter:frompyrate_limiterimportBucketFullException,Duration,RequestRate,Limiterhourly_rate=RequestRate(500,Duration.HOUR)# 500 requests per hourdaily_rate=RequestRate(1000,Duration.DAY)# 1000 requests per daymonthly_rate=RequestRate(10000,Duration.MONTH)# 10000 requests per monthlimiter=Limiter(hourly_rate,daily_rate,monthly_rate)Note that these rates need to be ordered by interval length; in other words, an hourly rate must
come before a daily rate, etc.Applying rate limitsThen, useLimiter.try_acquire()wherever you are making requests (or other rate-limited operations).
This will raise an exception if the rate limit is exceeded.importrequestsdefrequest_function():limiter.try_acquire('identity')requests.get('https://example.com')whileTrue:request_function()Alternatively, you can useLimiter.ratelimit()as a function decorator:@limiter.ratelimit('identity')defrequest_function():requests.get('https://example.com')SeeAdditional usage optionsbelow for more details.IdentitiesNote that bothtry_acquire()andratelimit()take one or moreidentityarguments. Typically this is
the name of the service or resource that is being rate-limited. This allows you to track rate limits
for these resources independently. For example, if you have a service that is rate-limited by user:defrequest_function(user_ids):limiter.try_acquire(*user_ids)foruser_idinuser_ids:requests.get(f'https://example.com?user_id={user_id}')Handling exceeded limitsWhen a rate limit is exceeded, you have two options: raise an exception, or add delays.Bucket analogyAt this point it's useful to introduce the analogy of "buckets" used for rate-limiting. Here is a
quick summary:This library implements theLeaky Bucket algorithm.It is named after the idea of representing some kind of fixed capacity -- like a network or service -- as a bucket.The bucket "leaks" at a constant rate. For web services, this represents theideal or permitted request rate.The bucket is "filled" at an intermittent, unpredicatble rate, representing theactual rate of requests.When the bucket is "full", it will overflow, representingcanceled or delayed requests.Rate limit exceptionsBy default, aBucketFullExceptionwill be raised when a rate limit is exceeded.
The error contains ameta_infoattribute with the following information:identity: The identity it receivedrate: The specific rate that has been exceededremaining_time: The remaining time until the next request can be sentHere's an example that will raise an exception on the 4th request:limiter=Limiter(RequestRate(3,Duration.SECOND))for_inrange(4):try:limiter.try_acquire('vutran')exceptBucketFullExceptionaserr:print(err)# Output: Bucket for vutran with Rate 3/5 is already fullprint(err.meta_info)# Output: {'identity': 'vutran', 'rate': '5/5', 'remaining_time': 2.9,#          'error': 'Bucket for vutran with Rate 3/5 is already full'}Rate limit delaysYou may want to simply slow down your requests to stay within the rate limits instead of canceling
them. In that case you can use thedelayargument. Note that this is only available forLimiter.ratelimit():@limiter.ratelimit('identity',delay=True)defmy_function():do_stuff()If you exceed a rate limit with a long interval (daily, monthly, etc.), you may not want to delay
that long. In this case, you can set amax_delay(in seconds) that you are willing to wait in
between calls:@limiter.ratelimit('identity',delay=True,max_delay=360)defmy_function():do_stuff()In this case, calls may be delayed by at most 360 seconds to stay within the rate limits; any longer
than that, and aBucketFullExceptionwill be raised instead. Without specifyingmax_delay, calls
will be delayed as long as necessary.Additional usage optionsBesidesLimiter.try_acquire(), some additional usage options are available usingLimiter.ratelimit():DecoratorLimiter.ratelimit()can be used as a decorator:@limiter.ratelimit('identity')defmy_function():do_stuff()As withLimiter.try_acquire(), if calls to the wrapped function exceed the rate limits you
defined, aBucketFullExceptionwill be raised.ContextmanagerLimiter.ratelimit()also works as a contextmanager:defmy_function():withlimiter.ratelimit('identity',delay=True):do_stuff()Async decorator/contextmanagerLimiter.ratelimit()also support async functions, either as a decorator or contextmanager:@limiter.ratelimit('identity',delay=True)asyncdefmy_function():awaitdo_stuff()asyncdefmy_function():asyncwithlimiter.ratelimit('identity'):awaitdo_stuff()When delays are enabled for an async function,asyncio.sleep()will be used instead oftime.sleep().BackendsA few different bucket backends are available, which can be selected using thebucket_classargument forLimiter. Any additional backend-specific arguments can be passed
viabucket_kwargs.MemoryThe default bucket is stored in memory, backed by aqueue.Queue. A list implementation is also available:frompyrate_limiterimportLimiter,MemoryListBucketlimiter=Limiter(bucket_class=MemoryListBucket)SQLiteIf you need to persist the bucket state, a SQLite backend is available.By default it will store the state in the system temp directory, and you can use
thepathargument to use a different location:frompyrate_limiterimportLimiter,SQLiteBucketlimiter=Limiter(bucket_class=SQLiteBucket)By default, the database will be stored in the system temp directory. You can specify a different
path viabucket_kwargs:limiter=Limiter(bucket_class=SQLiteBucket,bucket_kwargs={'path':'/path/to/db.sqlite'},)ConcurrencyThis backend is thread-safe, and may also be used with multiple child processes that share the sameLimiterobject, e.g. if created withProcessPoolExecutorormultiprocessing.Process.If you want to use SQLite with multiple processes with no shared state, for example if created by
running multiple scripts or by an external process, some additional protections are needed. For
these cases, a separateFileLockSQLiteBucketclass is available. This requires installing thepy-filelocklibrary.limiter=Limiter(bucket_class=FileLockSQLiteBucket)RedisIf you have a larger, distributed application, Redis is an ideal backend. This
option requiresredis-py.Note that this backend requires abucket_nameargument, which will be used as a kprefix for the
Redis keys created. This can be used to disambiguate between multiple services using the same Redis
instance with pyrate-limiter.frompyrate_limiterimportLimiter,RedisBucketlimiter=Limiter(bucket_class=RedisBucket,bucket_kwargs={'bucket_name':'my_service'})Connection settingsIf you need to pass additional connection settings, you can use theredis_poolbucket argument:fromredisimportConnectionPoolredis_pool=ConnectionPool(host='localhost',port=6379,db=0)limiter=Limiter(bucket_class=RedisBucket,bucket_kwargs={'redis_pool':redis_pool,'bucket_name':'my_service'},)Redis clustersRedis clusters are also supported, which requiresredis-py-cluster:frompyrate_limiterimportLimiter,RedisClusterBucketlimiter=Limiter(bucket_class=RedisClusterBucket)Custom backendsIf these don't suit your needs, you can also create your own bucket backend by extendingpyrate_limiter.bucket.AbstractBucket.Additional featuresTime sourcesBy default, monotonic time is used, to ensure requests are always logged in the correct order.You can specify a custom time source with thetime_functionargument. For example, you may want to
use the current UTC time for consistency across a distributed application using a Redis backend.fromdatetimeimportdatetimefrompyrate_limiterimportDuration,Limiter,RequestRatefromtimeimporttimerate=RequestRate(5,Duration.SECOND)limiter_datetime=Limiter(rate,time_function=lambda:datetime.utcnow().timestamp())Or simply use the basictime.time()function:fromtimeimporttimerate=RequestRate(5,Duration.SECOND)limiter_time=Limiter(rate,time_function=time)ExamplesTo prove that pyrate-limiter is working as expected, here is a complete example to demonstrate
rate-limiting with delays:fromtimeimportperf_counterastimefrompyrate_limiterimportDuration,Limiter,RequestRatelimiter=Limiter(RequestRate(5,Duration.SECOND))n_requests=27@limiter.ratelimit("test",delay=True)deflimited_function(start_time):print(f"t +{(time()-start_time):.5f}")start_time=time()for_inrange(n_requests):limited_function(start_time)print(f"Ran{n_requests}requests in{time()-start_time:.5f}seconds")And an equivalent example for async usage:importasynciofromtimeimportperf_counterastimefrompyrate_limiterimportDuration,Limiter,RequestRatelimiter=Limiter(RequestRate(5,Duration.SECOND))n_requests=27@limiter.ratelimit("test",delay=True)asyncdeflimited_function(start_time):print(f"t +{(time()-start_time):.5f}")asyncdeftest_ratelimit():start_time=time()tasks=[limited_function(start_time)for_inrange(n_requests)]awaitasyncio.gather(*tasks)print(f"Ran{n_requests}requests in{time()-start_time:.5f}seconds")asyncio.run(test_ratelimit())