limiter


Namelimiter JSON
Version 0.5.0 PyPI version JSON
download
home_pagehttps://github.com/alexdelorenzo/limiter
Summary⏲️ Easy rate limiting for Python. Rate limiting async and thread-safe decorators and context managers that use a token bucket algorithm.
upload_time2024-02-15 01:31:56
maintainer
docs_urlNone
authorAlex DeLorenzo
requires_python>=3.10
licenseLGPL-3.0
keywords rate-limit rate limit token bucket token-bucket token_bucket tokenbucket decorator contextmanager asynchronous threadsafe synchronous
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # ⏲️ 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).

### `Limiter` instances

`Limiter` instances take `rate`, `capacity` and `consume` 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 is
  reached.

### 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.

### Creating anonymous, or single-use, limiters

You don't have to assign `Limiter` objects 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 version `v0.3.0`, ships with a `limit` type alias for `Limiter`:

```python3
from limiter import limit


@limit(capacity=2, consume=2)
async def send_message():
  ...


async def upload_image():
  async with limit(capacity=3) as limiter:
    ...
```

The above is equivalent to the below:

```python3
from limiter import Limiter


@Limiter(capacity=2, consume=2)
async def send_message():
  ...


async def upload_image():
  async with Limiter(capacity=3) as limiter:
    ...
```

Both `limit` and `Limiter` are the same object:

```python3
assert limit is Limiter
```

### Jitter

A `Limiter`'s `jitter` argument adds jitter to help with contention.

The value is in `units`, 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.
- A `range` object, to add a random amount of jitter within the range.
- A `tuple` of two numbers, `start` and `stop`, to add a random amount of jitter between the two numbers.
- A `tuple` of three numbers: `start`, `stop` and `step`, to add jitter like you would with `range`.

For example, if you want to use a random amount of jitter between `0` and `100` milliseconds:

```python3
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 for `jitter` when using decorators or context-managers:

```python3
limiter = Limiter(rate=2, capacity=5, consume=2)


@limiter(jitter=range(0, 100))
def download_page(url: str) -> bytes:
  ...


async def download_page(url: str) -> bytes:
    async with limiter(jitter=(0, 100)):
        ...
```

You can use the above to override default values of `jitter` in a `Limiter` instance.


To add a small amount of random jitter, supply `True` as the value:
```python3
limiter = Limiter(rate=2, capacity=5, consume=2, jitter=True)

# or

@limiter(jitter=True)
def download_page(url: str) -> bytes:
  ...
```

To turn off jitter in a `Limiter` configured with jitter, you can supply `False` as the value:

```python3
limiter = Limiter(rate=2, capacity=5, consume=2, jitter=range(10))


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


async def download_page(url: str) -> bytes:
    async with limiter(jitter=False):
        ...
```

Or create a new limiter with jitter turned off:

```python3
limiter: Limiter = limiter.new(jitter=False)
```

### Units

`units` is a number representing the amount of units in one second. The default value is `1000` for 1,000 milliseconds in one second.

Similar to `jitter`, `units` can be supplied at all the same call sites and constructors that `jitter` is accepted.

If you want to use a different unit than milliseconds, supply a different value for `units`.

## 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.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/alexdelorenzo/limiter",
    "name": "limiter",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": "",
    "keywords": "rate-limit,rate,limit,token,bucket,token-bucket,token_bucket,tokenbucket,decorator,contextmanager,asynchronous,threadsafe,synchronous",
    "author": "Alex DeLorenzo",
    "author_email": "",
    "download_url": "https://files.pythonhosted.org/packages/5f/76/3fcdddbac1a64a65630a0fc54b7b372d61b67da1fadf55649f8ca3d7d61e/limiter-0.5.0.tar.gz",
    "platform": null,
    "description": "# \u23f2\ufe0f Easy rate limiting for Python\n\n`limiter` makes it easy to add [rate limiting](https://en.wikipedia.org/wiki/Rate_limiting) to Python projects, using\na [token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm. `limiter` can provide Python projects and\nscripts with:\n\n- Rate limiting thread-safe [decorators](https://www.python.org/dev/peps/pep-0318/)\n- Rate limiting async decorators\n- Rate limiting thread-safe [context managers](https://www.python.org/dev/peps/pep-0343/)\n- Rate\n  limiting [async context managers](https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with)\n\nHere are some features and benefits of using `limiter`:\n\n- Easily control burst and average request rates\n- It\n  is [thread-safe, with no need for a timer thread](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm#Comparison_with_the_token_bucket)\n- It adds [jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) to help with contention\n- It has a simple API that takes advantage of Python's features, idioms\n  and [type hinting](https://www.python.org/dev/peps/pep-0483/)\n\n## Example\n\nHere's an example of using a limiter as a decorator and context manager:\n\n```python\nfrom aiohttp import ClientSession\nfrom limiter import Limiter\n\n\nlimit_downloads = Limiter(rate=2, capacity=5, consume=2)\n\n\n@limit_downloads\nasync def download_image(url: str) -> bytes:\n  async with ClientSession() as session, session.get(url) as response:\n    return await response.read()\n\n\nasync def download_page(url: str) -> str:\n  async with (\n    ClientSession() as session,\n    limit_downloads,\n    session.get(url) as response\n  ):\n    return await response.text()\n```\n\n## Usage\n\nYou can define limiters and use them dynamically across your project.\n\n**Note**: If you're using Python version `3.9.x` or below, check\nout [the documentation for version `0.2.0` of `limiter` here](https://github.com/alexdelorenzo/limiter/blob/master/README-0.2.0.md).\n\n### `Limiter` instances\n\n`Limiter` instances take `rate`, `capacity` and `consume` arguments.\n\n- `rate` is the token replenishment rate per second. Tokens are automatically added every second.\n- `consume` is the amount of tokens consumed from the token bucket upon successfully taking tokens from the bucket.\n- `capacity` is the total amount of tokens the token bucket can hold. Token replenishment stops when this capacity is\n  reached.\n\n### Limiting blocks of code\n\n`limiter` can rate limit all Python callables, and limiters can be used as context managers.\n\nYou can define a limiter with a set refresh `rate` and total token `capacity`. You can set the amount of tokens to\nconsume dynamically with `consume`, and the `bucket` parameter sets the bucket to consume tokens from:\n\n```python3\nfrom limiter import Limiter\n\n\nREFRESH_RATE: int = 2\nBURST_RATE: int = 3\nMSG_BUCKET: str = 'messages'\n\nlimiter: Limiter = Limiter(rate=REFRESH_RATE, capacity=BURST_RATE)\nlimit_msgs: Limiter = limiter(bucket=MSG_BUCKET)\n\n\n@limiter\ndef download_page(url: str) -> bytes:\n  ...\n\n\n@limiter(consume=2)\nasync def download_page(url: str) -> bytes:\n  ...\n\n\ndef send_page(page: bytes):\n  with limiter(consume=1.5, bucket=MSG_BUCKET):\n    ...\n\n\nasync def send_page(page: bytes):\n  async with limit_msgs:\n    ...\n\n\n@limit_msgs(consume=3)\ndef send_email(to: str):\n  ...\n\n\nasync def send_email(to: str):\n  async with limiter(bucket=MSG_BUCKET):\n    ...\n```\n\nIn the example above, both `limiter` and `limit_msgs` share the same limiter. The only difference is that `limit_msgs`\nwill take tokens from the `MSG_BUCKET` bucket by default.\n\n```python3\nassert limiter.limiter is limit_msgs.limiter\nassert limiter.bucket != limit_msgs.bucket\nassert limiter != limit_msgs\n```\n\n### Creating new limiters\n\nYou can reuse existing limiters in your code, and you can create new limiters from the parameters of an existing limiter\nusing the `new()` method.\n\nOr, you can define a new limiter entirely:\n\n```python\n# you can reuse existing limiters\nlimit_downloads: Limiter = limiter(consume=2)\n\n# you can use the settings from an existing limiter in a new limiter\nlimit_downloads: Limiter = limiter.new(consume=2)\n\n# or you can simply define a new limiter\nlimit_downloads: Limiter = Limiter(REFRESH_RATE, BURST_RATE, consume=2)\n\n\n@limit_downloads\ndef download_page(url: str) -> bytes:\n  ...\n\n\n@limit_downloads\nasync def download_page(url: str) -> bytes:\n  ...\n\n\ndef download_image(url: str) -> bytes:\n  with limit_downloads:\n    ...\n\n\nasync def download_image(url: str) -> bytes:\n  async with limit_downloads:\n    ...\n```\n\nLet's look at the difference between reusing an existing limiter, and creating new limiters with the `new()` method:\n\n```python3\nlimiter_a: Limiter = limiter(consume=2)\nlimiter_b: Limiter = limiter.new(consume=2)\nlimiter_c: Limiter = Limiter(REFRESH_RATE, BURST_RATE, consume=2)\n\nassert limiter_a != limiter\nassert limiter_a != limiter_b != limiter_c\n\nassert limiter_a != limiter_b\nassert limiter_a.limiter is limiter.limiter\nassert limiter_a.limiter is not limiter_b.limiter\n\nassert limiter_a.attrs == limiter_b.attrs == limiter_c.attrs\n```\n\nThe only things that are equivalent between the three new limiters above are the limiters' attributes, like\nthe `rate`, `capacity`, and `consume` attributes.\n\n### Creating anonymous, or single-use, limiters\n\nYou don't have to assign `Limiter` objects to variables. Anonymous limiters don't share a token bucket like named\nlimiters can. They work well when you don't have a reason to share a limiter between two or more blocks of code, and\nwhen a limiter has a single or independent purpose.\n\n`limiter`, after version `v0.3.0`, ships with a `limit` type alias for `Limiter`:\n\n```python3\nfrom limiter import limit\n\n\n@limit(capacity=2, consume=2)\nasync def send_message():\n  ...\n\n\nasync def upload_image():\n  async with limit(capacity=3) as limiter:\n    ...\n```\n\nThe above is equivalent to the below:\n\n```python3\nfrom limiter import Limiter\n\n\n@Limiter(capacity=2, consume=2)\nasync def send_message():\n  ...\n\n\nasync def upload_image():\n  async with Limiter(capacity=3) as limiter:\n    ...\n```\n\nBoth `limit` and `Limiter` are the same object:\n\n```python3\nassert limit is Limiter\n```\n\n### Jitter\n\nA `Limiter`'s `jitter` argument adds jitter to help with contention.\n\nThe value is in `units`, which is milliseconds by default, and can be any of these:\n\n- `False`, to add no jitter. This is the default.\n- `True`, to add a random amount of jitter.\n- A number, to add a fixed amount of jitter.\n- A `range` object, to add a random amount of jitter within the range.\n- A `tuple` of two numbers, `start` and `stop`, to add a random amount of jitter between the two numbers.\n- A `tuple` of three numbers: `start`, `stop` and `step`, to add jitter like you would with `range`.\n\nFor example, if you want to use a random amount of jitter between `0` and `100` milliseconds:\n\n```python3\nlimiter = Limiter(rate=2, capacity=5, consume=2, jitter=(0, 100))\nlimiter = Limiter(rate=2, capacity=5, consume=2, jitter=(0, 100, 1))\nlimiter = Limiter(rate=2, capacity=5, consume=2, jitter=range(0, 100))\nlimiter = Limiter(rate=2, capacity=5, consume=2, jitter=range(0, 100, 1))\n```\n\nAll of the above are equivalent to each other in function.\n\nYou can also supply values for `jitter` when using decorators or context-managers:\n\n```python3\nlimiter = Limiter(rate=2, capacity=5, consume=2)\n\n\n@limiter(jitter=range(0, 100))\ndef download_page(url: str) -> bytes:\n  ...\n\n\nasync def download_page(url: str) -> bytes:\n    async with limiter(jitter=(0, 100)):\n        ...\n```\n\nYou can use the above to override default values of `jitter` in a `Limiter` instance.\n\n\nTo add a small amount of random jitter, supply `True` as the value:\n```python3\nlimiter = Limiter(rate=2, capacity=5, consume=2, jitter=True)\n\n# or\n\n@limiter(jitter=True)\ndef download_page(url: str) -> bytes:\n  ...\n```\n\nTo turn off jitter in a `Limiter` configured with jitter, you can supply `False` as the value:\n\n```python3\nlimiter = Limiter(rate=2, capacity=5, consume=2, jitter=range(10))\n\n\n@limiter(jitter=False)\ndef download_page(url: str) -> bytes:\n  ...\n\n\nasync def download_page(url: str) -> bytes:\n    async with limiter(jitter=False):\n        ...\n```\n\nOr create a new limiter with jitter turned off:\n\n```python3\nlimiter: Limiter = limiter.new(jitter=False)\n```\n\n### Units\n\n`units` is a number representing the amount of units in one second. The default value is `1000` for 1,000 milliseconds in one second.\n\nSimilar to `jitter`, `units` can be supplied at all the same call sites and constructors that `jitter` is accepted.\n\nIf you want to use a different unit than milliseconds, supply a different value for `units`.\n\n## Installation\n\n### Requirements\n\n- Python 3.10+ for versions `0.3.0` and up\n- [Python 3.7+ for versions below `0.3.0`](https://github.com/alexdelorenzo/limiter/blob/master/README-0.2.0.md)\n\n### Install via PyPI\n\n```bash\n$ python3 -m pip install limiter\n```\n\n## License\n\nSee [`LICENSE`](/LICENSE). If you'd like to use this project with a different license, please get in touch.\n",
    "bugtrack_url": null,
    "license": "LGPL-3.0",
    "summary": "\u23f2\ufe0f Easy rate limiting for Python. Rate limiting async and thread-safe decorators and context managers that use a token bucket algorithm.",
    "version": "0.5.0",
    "project_urls": {
        "Homepage": "https://github.com/alexdelorenzo/limiter"
    },
    "split_keywords": [
        "rate-limit",
        "rate",
        "limit",
        "token",
        "bucket",
        "token-bucket",
        "token_bucket",
        "tokenbucket",
        "decorator",
        "contextmanager",
        "asynchronous",
        "threadsafe",
        "synchronous"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "113a6a9b416113254840870d8db98fb7018957b1324898e65874cb1844f69dea",
                "md5": "071e0f19c150883032652febbf20ded2",
                "sha256": "920ee7587596b6421690ee2009a755a9970b743001567ae1005310d9b654985c"
            },
            "downloads": -1,
            "filename": "limiter-0.5.0-py2.py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "071e0f19c150883032652febbf20ded2",
            "packagetype": "bdist_wheel",
            "python_version": "py2.py3",
            "requires_python": ">=3.10",
            "size": 10120,
            "upload_time": "2024-02-15T01:31:53",
            "upload_time_iso_8601": "2024-02-15T01:31:53.449704Z",
            "url": "https://files.pythonhosted.org/packages/11/3a/6a9b416113254840870d8db98fb7018957b1324898e65874cb1844f69dea/limiter-0.5.0-py2.py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "5f763fcdddbac1a64a65630a0fc54b7b372d61b67da1fadf55649f8ca3d7d61e",
                "md5": "0f6fe5812be1f90cfb937ca79213fe0d",
                "sha256": "71b9e972e04f1dcf3fa9b541ca3a16dcc1be6ce0d6a12407b25a0888a57e612f"
            },
            "downloads": -1,
            "filename": "limiter-0.5.0.tar.gz",
            "has_sig": false,
            "md5_digest": "0f6fe5812be1f90cfb937ca79213fe0d",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 10212,
            "upload_time": "2024-02-15T01:31:56",
            "upload_time_iso_8601": "2024-02-15T01:31:56.908633Z",
            "url": "https://files.pythonhosted.org/packages/5f/76/3fcdddbac1a64a65630a0fc54b7b372d61b67da1fadf55649f8ca3d7d61e/limiter-0.5.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-02-15 01:31:56",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "alexdelorenzo",
    "github_project": "limiter",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "requirements": [],
    "lcname": "limiter"
}
        
Elapsed time: 0.18589s