Metadata-Version: 2.1
Name: limiter
Version: 0.3.0
Summary: ⏲️ Easy rate limiting for Python. Rate limiting async and thread-safe decorators and context managers that use a token bucket algorithm.
Home-page: https://github.com/alexdelorenzo/limiter
Author: Alex DeLorenzo
License: AGPL-3.0
Keywords: rate-limit,rate,limit,token,bucket,token-bucket,token_bucket,tokenbucket,decorator,contextmanager,asynchronous,threadsafe,synchronous
Platform: UNKNOWN
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE

# ⏲️ Easy rate limiting for Python

`limiter` makes it easy to add [rate limiting](https://en.wikipedia.org/wiki/Rate_limiting) to Python projects, using a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm. `limiter` can provide Python projects and scripts with:
  - Rate limiting thread-safe [decorators](https://www.python.org/dev/peps/pep-0318/)
  - Rate limiting async decorators
  - Rate limiting thread-safe [context managers](https://www.python.org/dev/peps/pep-0343/)
  - Rate limiting [async context managers](https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with)

Here are some features and benefits of using `limiter`:
 - Easily control burst and average request rates
 - It is [thread-safe, with no need for a timer thread](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm#Comparison_with_the_token_bucket)
 - It adds [jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) to help with contention
 - It has a simple API that takes advantage of Python's features, idioms and [type hinting](https://www.python.org/dev/peps/pep-0483/)

## Example
Here's an example of using a limiter as a decorator and context manager:
```python
from aiohttp import ClientSession
from limiter import Limiter


limit_downloads = Limiter(rate=2, capacity=5, consume=2)


@limit_downloads
async def download_image(url: str) -> bytes:
  async with ClientSession() as session, session.get(url) as response:
    return await response.read()


async def download_page(url: str) -> str:
  async with (
    ClientSession() as session,
    limit_downloads,
    session.get(url) as response
  ):
    return await response.text()
```

# Usage
You can define limiters and use them dynamically across your project.

**Note**: If you're using Python version `3.9.x` or below, check out [the documentation for version `0.2.0` of `limiter` here](https://github.com/alexdelorenzo/limiter/blob/master/README-0.2.0.md).

### Limiting blocks of code
`limiter` can rate limit all Python callables, and limiters can be used as context managers.

You can define a limiter with a set refresh `rate` and total token `capacity`. You can set the amount of tokens to consume dynamically with `consume`, and the `bucket` parameter sets the bucket to consume tokens from:
```python3
from limiter import Limiter


REFRESH_RATE: int = 2
BURST_RATE: int = 3
MSG_BUCKET: str = 'messages'


limiter: Limiter = Limiter(rate=REFRESH_RATE, capacity=BURST_RATE)
limit_msgs: Limiter = limiter(bucket=MSG_BUCKET)


@limiter
def download_page(url: str) -> bytes:
  ...


@limiter(consume=2)
async def download_page(url: str) -> bytes:
  ...


def send_page(page: bytes):
  with limiter(consume=1.5, bucket=MSG_BUCKET):
    ...


async def send_page(page: bytes):
  async with limit_msgs:
    ...


@limit_msgs(consume=3)
def send_email(to: str):
  ...


async def send_email(to: str):
  async with limiter(bucket=MSG_BUCKET):
    ...
```

In the example above, both `limiter` and `limit_msgs` share the same limiter. The only difference is that `limit_msgs` will take tokens from the `MSG_BUCKET` bucket by default.

```python3
assert limiter.limiter is limit_msgs.limiter
assert limiter.bucket != limit_msgs.bucket
assert limiter != limit_msgs
```

### Creating new limiters
You can reuse existing limiters in your code, and you can create new limiters from the parameters of an existing limiter using the `new()` method. 

Or, you can define a new limiter entirely:
```python
# you can reuse existing limiters
limit_downloads: Limiter = limiter(consume=2)

# you can use the settings from an existing limiter in a new limiter
limit_downloads: Limiter = limiter.new(consume=2)

# or you can simply define a new limiter
limit_downloads: Limiter = Limiter(REFRESH_RATE, BURST_RATE, consume=2)


@limit_downloads
def download_page(url: str) -> bytes:
  ...


@limit_downloads
async def download_page(url: str) -> bytes:
  ...


def download_image(url: str) -> bytes:
  with limit_downloads:
    ...


async def download_image(url: str) -> bytes:
  async with limit_downloads:
    ...
```

Let's look at the difference between reusing an existing limiter, and creating new limiters with the `new()` method:
```python3
limiter_a: Limiter = limiter(consume=2)
limiter_b: Limiter = limiter.new(consume=2)
limiter_c: Limiter = Limiter(REFRESH_RATE, BURST_RATE, consume=2)

assert limiter_a != limiter
assert limiter_a != limiter_b != limiter_c

assert limiter_a != limiter_b
assert limiter_a.limiter is limiter.limiter
assert limiter_a.limiter is not limiter_b.limiter

assert limiter_a.attrs == limiter_b.attrs == limiter_c.attrs
```

The only things that are equivalent between the three new limiters above are the limiters' attributes, like the `rate`, `capacity`, and `consume` attributes.

# Installation
## Requirements
 - Python 3.10+ for versions `0.3.0` and up
 - [Python 3.7+ for versions below `0.3.0`](https://github.com/alexdelorenzo/limiter/blob/master/README-0.2.0.md)

## Install via PyPI
```bash
$ python3 -m pip install limiter
```

# License
See [`LICENSE`](/LICENSE). If you'd like to use this project with a different license, please get in touch.


