# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['pyrate_limiter']

package_data = \
{'': ['*']}

extras_require = \
{'all': ['filelock>=3.0', 'redis>=3.3,<4.0', 'redis-py-cluster>=2.1.3,<3.0.0']}

setup_kwargs = {
    'name': 'pyrate-limiter',
    'version': '2.7.0',
    'description': 'Python Rate-Limiter using Leaky-Bucket Algorimth Family',
    'long_description': '<img align="left" width="95" height="120" src="https://raw.githubusercontent.com/vutran1710/PyrateLimiter/master/img/log.png">\n\n# PyrateLimiter\nThe request rate limiter using Leaky-bucket algorithm\n\n[![PyPI version](https://badge.fury.io/py/pyrate-limiter.svg)](https://badge.fury.io/py/pyrate-limiter)\n[![PyPI - Python Versions](https://img.shields.io/pypi/pyversions/pyrate-limiter)](https://pypi.org/project/pyrate-limiter)\n[![codecov](https://codecov.io/gh/vutran1710/PyrateLimiter/branch/master/graph/badge.svg?token=E0Q0YBSINS)](https://codecov.io/gh/vutran1710/PyrateLimiter)\n[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/vutran1710/PyrateLimiter/graphs/commit-activity)\n[![PyPI license](https://img.shields.io/pypi/l/ansicolortags.svg)](https://pypi.python.org/pypi/pyrate-limiter/)\n\n<br>\n\n- [PyrateLimiter](#pyratelimiter)\n  * [Introduction](#introduction)\n  * [Available modules](#available-modules)\n  * [Bucket backends](#bucket-backends)\n    + [Memory](#memory)\n    + [SQLite](#sqlite)\n    + [Redis](#redis)\n    + [Custom backends](#custom-backends)\n  * [Strategies](#strategies)\n    + [Subscription strategies](#subscription-strategies)\n    + [BucketFullException](#bucketfullexception)\n    + [Decorator](#decorator)\n    + [Rate-limiting delays](#rate-limiting-delays)\n    + [Contextmanager](#contextmanager)\n    + [Async decorator/contextmanager](#async-decorator-contextmanager)\n    + [Examples](#examples)\n    + [Spam-protection strategies](#spam-protection-strategies)\n    + [Throttling handling](#throttling-handling)\n    + [More complex scenario](#more-complex-scenario)\n  * [Development](#development)\n  * [Notes](#notes)\n\n## Introduction\nThis module can be used to apply rate-limit for API request. User defines window duration and the limit of function calls within such interval.\n\n## Available modules\n```python\nfrom pyrate_limiter import (\n    BucketFullException,\n    Duration,\n    RequestRate,\n    Limiter,\n    MemoryListBucket,\n    MemoryQueueBucket,\n    SQLiteBucket,\n    RedisBucket,\n    RedisClusterBucket,\n)\n```\n\n## Bucket backends\nA few different bucket backends are available, which can be selected using the `bucket_class`\nargument for `Limiter`. Any additional backend-specific arguments can be passed\nvia `bucket_kwargs`.\n\n### Memory\nThe default bucket is stored in memory, backed by a `queue.Queue`. A list implementation is also available:\n```python\nfrom pyrate_limiter import Limiter, MemoryListBucket\n\nlimiter = Limiter(bucket_class=MemoryListBucket)\n```\n\n### SQLite\nIf you need to persist the bucket state, a SQLite backend is available.\n\nBy default it will store the state in the system temp directory, and you can use\nthe `path` argument to use a different location:\n```python\nfrom pyrate_limiter import Limiter, SQLiteBucket\n\nlimiter = Limiter(bucket_class=SQLiteBucket)\n```\n\nBy default, the database will be stored in the system temp directory. You can specify a different\npath via `bucket_kwargs`:\n```python\nlimiter = Limiter(\n    bucket_class=SQLiteBucket,\n    bucket_kwargs={\'path\': \'/path/to/db.sqlite\'},\n)\n```\n\n#### Concurrency\nThis backend is thread-safe, and may also be used with multiple child processes that share the same\n`Limiter` object, e.g. if created with `ProcessPoolExecutor` or `multiprocessing.Process`.\n\nIf you want to use SQLite with multiple processes with no shared state, for example if created by\nrunning multiple scripts or by an external process, some additional protections are needed. For\nthese cases, a separate `FileLockSQLiteBucket` class is available. This requires installing the\n[py-filelock](https://py-filelock.readthedocs.io) library.\n```python\nlimiter = Limiter(bucket_class=FileLockSQLiteBucket)\n```\n\n### Redis\nIf you have a larger, distributed application, Redis is an ideal backend. This\noption requires [redis-py](https://github.com/andymccurdy/redis-py).\n\nYou can use the `redis_pool` argument to pass any connection settings:\n```python\nfrom pyrate_limiter import Limiter, RedisBucket\nfrom redis import ConnectionPool\n\nredis_pool = ConnectionPool(host=\'localhost\', port=6379, db=0)\nlimiter = Limiter(\n    bucket_class=RedisBucket,\n    bucket_kwargs={\'redis_pool\': redis_pool},\n)\n```\n\nRedis clusters are also supported, which requires\n[redis-py-cluster](https://github.com/Grokzen/redis-py-cluster):\n```python\nfrom pyrate_limiter import Limiter, RedisClusterBucket\n\nlimiter = Limiter(bucket_class=RedisClusterBucket)\n```\n\n### Custom backends\nIf these don\'t suit your needs, you can also create your own bucket backend by extending `pyrate_limiter.bucket.AbstractBucket`.\n\n## Strategies\n\n### Subscription strategies\n\nConsidering API throttling logic for usual business models of Subscription, we usually see strategies somewhat similar to these.\n\n``` shell\nSome commercial/free API (Linkedin, Github etc)\n- 500 requests/hour, and\n- 1000 requests/day, and\n- maximum 10,000 requests/month\n```\n\n- [x] `RequestRate` class is designed to describe this strategies - eg for the above strategies we have a Rate-Limiter defined\nas following\n\n``` python\nhourly_rate = RequestRate(500, Duration.HOUR) # maximum 500 requests/hour\ndaily_rate = RequestRate(1000, Duration.DAY) # maximum 1000 requests/day\nmonthly_rate = RequestRate(10000, Duration.MONTH) # and so on\n\nlimiter = Limiter(hourly_rate, daily_rate, monthly_rate, *other_rates, bucket_class=MemoryListBucket) # default is MemoryQueueBucket\n\n# usage\nidentity = user_id # or ip-address, or maybe both\nlimiter.try_acquire(identity)\n```\n\nAs the logic is pretty self-explainatory, note that the superior rate-limit must come after the inferiors, ie\n1000 req/day must be declared after an hourly-rate-limit, and the daily-limit must be larger than hourly-limit.\n\n- [x] `bucket_class` is the type of bucket that holds request. It could be an in-memory data structure like Python List (`MemoryListBucket`), or Queue `MemoryQueueBucket`.\n\n\n- [x] For microservices or decentralized platform, multiple rate-Limiter may share a single store for storing\n      request-rate history, ie `Redis`. This lib provides ready-to-use `RedisBucket` (`redis-py` is required), and `RedisClusterBucket` (with `redis-py-cluster` being required). The usage difference is when using Redis, a naming `prefix` must be provide so the keys can be distinct for each item\'s identity.\n\n``` python\nfrom pyrate_limiter.bucket import RedisBucket, RedisClusterBucket\nfrom redis import ConnectionPool\n\nredis_pool = ConnectionPool.from_url(\'redis://localhost:6379\')\n\nrate = RequestRate(3, 5 * Duration.SECOND)\n\nbucket_kwargs = {\n    "redis_pool": redis_pool,\n    "bucket_name": "my-ultimate-bucket-prefix"\n}\n\n# so each item buckets will have a key name as\n# my-ultimate-bucket-prefix__item-identity\n\nlimiter = Limiter(rate, bucket_class=RedisBucket, bucket_kwargs=bucket_kwargs)\n# or RedisClusterBucket when used with a redis cluster\n# limiter = Limiter(rate, bucket_class=RedisClusterBucket, bucket_kwargs=bucket_kwargs)\nitem = \'vutran_item\'\nlimiter.try_acquire(item)\n```\n\n### BucketFullException\nIf the Bucket is full, an exception `BucketFullException` will be raised, with meta-info about the identity it received, the rate that has raised, and the remaining time until the next request can be processed.\n\n```python\nrate = RequestRate(3, 5 * Duration.SECOND)\nlimiter = Limiter(rate)\nitem = \'vutran\'\n\nhas_raised = False\ntry:\n    for _ in range(4):\n        limiter.try_acquire(item)\n        sleep(1)\nexcept BucketFullException as err:\n    has_raised = True\n    assert str(err)\n    # Bucket for vutran with Rate 3/5 is already full\n    assert isinstance(err.meta_info, dict)\n    # {\'error\': \'Bucket for vutran with Rate 3/5 is already full\', \'identity\': \'tranvu\', \'rate\': \'5/5\', \'remaining_time\': 2}\n```\n\n- [ ] *RequestRate may be required to `reset` on a fixed schedule, eg: every first-day of a month\n\n### Decorator\nRate-limiting is also available in decorator form, using `Limiter.ratelimit`. Example:\n```python\n@limiter.ratelimit(item)\ndef my_function():\n    do_stuff()\n```\n\nAs with `Limiter.try_acquire`, if calls to the wrapped function exceed the rate limits you\ndefined, a `BucketFullException` will be raised.\n\n### Rate-limiting delays\nIn some cases, you may want to simply slow down your calls to stay within the rate limits instead of\ncanceling them. In that case you can use the `delay` flag, optionally with a `max_delay`\n(in seconds) that you are willing to wait in between calls.\n\nExample:\n```python\n@limiter.ratelimit(item, delay=True, max_delay=10)\ndef my_function():\n    do_stuff()\n```\n\nIn this case, calls may be delayed by at most 10 seconds to stay within the rate limits; any longer\nthan that, and a `BucketFullException` will be raised instead. Without specifying `max_delay`, calls\nwill be delayed as long as necessary.\n\n### Contextmanager\n`Limiter.ratelimit` also works as a contextmanager:\n\n```python\ndef my_function():\n    with limiter.ratelimit(item, delay=True):\n        do_stuff()\n```\n\n### Async decorator/contextmanager\nAll the above features of `Limiter.ratelimit` also work on async functions:\n```python\n@limiter.ratelimit(item, delay=True)\nasync def my_function():\n    await do_stuff()\n\nasync def my_function():\n    async with limiter.ratelimit(item):\n        await do_stuff()\n```\n\nWhen delays are enabled, `asyncio.sleep` will be used instead of `time.sleep`.\n\n### Examples\nTo prove that pyrate-limiter is working as expected, here is a complete example to demonstrate\nrate-limiting with delays:\n```python\nfrom time import perf_counter as time\nfrom pyrate_limiter import Duration, Limiter, RequestRate\n\nlimiter = Limiter(RequestRate(5, Duration.SECOND))\nn_requests = 27\n\n@limiter.ratelimit("test", delay=True)\ndef limited_function(start_time):\n    print(f"t + {(time() - start_time):.5f}")\n\nstart_time = time()\nfor _ in range(n_requests):\n    limited_function(start_time)\nprint(f"Ran {n_requests} requests in {time() - start_time:.5f} seconds")\n```\n\nAnd an equivalent example for async usage:\n```python\nimport asyncio\nfrom time import perf_counter as time\nfrom pyrate_limiter import Duration, Limiter, RequestRate\n\nlimiter = Limiter(RequestRate(5, Duration.SECOND))\nn_requests = 27\n\n@limiter.ratelimit("test", delay=True)\nasync def limited_function(start_time):\n    print(f"t + {(time() - start_time):.5f}")\n\nasync def test_ratelimit():\n    start_time = time()\n    tasks = [limited_function(start_time) for _ in range(n_requests)]\n    await asyncio.gather(*tasks)\n    print(f"Ran {n_requests} requests in {time() - start_time:.5f} seconds")\n\nasyncio.run(test_ratelimit())\n```\n\nLimiter can be used with any custom time source. For example user may want to use\nRedis time to use same time in distributed installation. By default `time.monotonic` is\nused. To adjust time source use `time_function` parameter with any no arguments function.\n```python\nfrom datetime import datetime\nfrom pyrate_limiter import Duration, Limiter, RequestRate\nfrom time import time\n\nlimiter_datetime = Limiter(RequestRate(5, Duration.SECOND), time_function=lambda: datetime.utcnow().timestamp())\nlimiter_time = Limiter(RequestRate(5, Duration.SECOND), time_function=time)\n```\n\n### Spam-protection strategies\n- [x] Sometimes, we need a rate-limiter to protect our API from spamming/ddos attack. Some usual strategies for this could be as\nfollowing\n\n``` shell\n1. No more than 100 requests/minute, or\n2. 100 request per minute, and no more than 300 request per hour\n```\n\n### Throttling handling\nWhen the number of incoming requets go beyond the limit, we can either do..\n\n``` shell\n1. Raise a 429 Http Error, or\n2. Keep the incoming requests, wait then slowly process them one by one.\n```\n\n### More complex scenario\nhttps://www.keycdn.com/support/rate-limiting#types-of-rate-limits\n\n- [ ] *Sometimes, we may need to apply specific rate-limiting strategies based on schedules/region or some other metrics. It\nrequires the capability to `switch` the strategies instantly without re-deploying the whole service.\n\n## Development\n\n### Setup & Commands\n- To setup local development,  *Poetry* and *Python 3.6* is required. Python can be installed using *Pyenv* or normal installation from binary source. To install *poetry*, follow the official guideline (https://python-poetry.org/docs/#installation).\n\nThen, in the repository directory, run the following to install all optional backend dependencies and dev dependencies:\n```shell\n$ poetry install -E all\n```\n\nSome shortcuts are included for some common development tasks, using [nox](https://nox.thea.codes):\n- Run tests with: `nox -e test`\n- To run tests with coverage: `nox -e cover`\n- Format & check for lint error: `nox -e lint`\n- To run linting for every commit, run: `pre-commit install`\n\n### Guideline & Notes\nWe have GitHub Action CICD to do the checking, testing and publishing work. So, there are few small notes when making Pull Request:\n- All existing tests must pass (Of course!)\n- Reduction in *Coverage* shall result in failure. (below 98% is not accepted)\n- When you are making bug fixes, or adding more features, remember to bump the version number in **pyproject.toml**. The number should follow *semantic-versioning* rules\n\n## Notes\nTodo-items marked with (*) are planned for v3 release.\n',
    'author': 'vutr',
    'author_email': 'me@vutr.io',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/vutran1710/PyrateLimiter',
    'packages': packages,
    'package_data': package_data,
    'extras_require': extras_require,
    'python_requires': '>=3.6.2,<4.0.0',
}


setup(**setup_kwargs)
