pax_global_header00006660000000000000000000000064143372163740014524gustar00rootroot0000000000000052 comment=912895b1231968412801ba883d49cc62174e8e89 throttler-1.2.2/000077500000000000000000000000001433721637400135555ustar00rootroot00000000000000throttler-1.2.2/.github/000077500000000000000000000000001433721637400151155ustar00rootroot00000000000000throttler-1.2.2/.github/workflows/000077500000000000000000000000001433721637400171525ustar00rootroot00000000000000throttler-1.2.2/.github/workflows/pythonpublish.yml000066400000000000000000000016441433721637400226120ustar00rootroot00000000000000# These workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [ created ] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install --upgrade setuptools wheel twine readme_renderer[md] - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* throttler-1.2.2/.github/workflows/tests.yml000066400000000000000000000021161433721637400210370ustar00rootroot00000000000000name: Python Tests on: [push] jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.6, 3.7, 3.8, 3.9, '3.10'] env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt - name: Lint with flake8 run: | # Stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - name: Test with pytest run: | pytest -vl --cov=./ --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: env_vars: OS,PYTHON flags: unittests throttler-1.2.2/.gitignore000066400000000000000000000034251433721637400155510ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # IDE .idea/ throttler-1.2.2/LICENSE000066400000000000000000000020621433721637400145620ustar00rootroot00000000000000MIT License Copyright (c) 2020 Ramzan Bekbulatov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. throttler-1.2.2/MANIFEST.in000066400000000000000000000000571433721637400153150ustar00rootroot00000000000000include readme.md include requirements-dev.txt throttler-1.2.2/codecov.yml000066400000000000000000000000621433721637400157200ustar00rootroot00000000000000ignore: - 'examples' - 'setup.py' - 'tests' throttler-1.2.2/examples/000077500000000000000000000000001433721637400153735ustar00rootroot00000000000000throttler-1.2.2/examples/__init__.py000066400000000000000000000000001433721637400174720ustar00rootroot00000000000000throttler-1.2.2/examples/example_execution_timer.py000066400000000000000000000016621433721637400226700ustar00rootroot00000000000000import asyncio import time from throttler import ExecutionTimer def example(): """ Output will be like: Thu Mar 26 00:56:17 2020 | 1585173377.1203406 Thu Mar 26 00:57:00 2020 | 1585173420.0006166 Thu Mar 26 00:58:00 2020 | 1585173480.002517 Thu Mar 26 00:59:00 2020 | 1585173540.001494 """ et = ExecutionTimer(60, align_sleep=True) while True: with et: print(time.asctime(), '|', time.time()) def example_async(): """ Output will be like: Thu Mar 26 00:56:17 2020 | 1585173377.1203406 Thu Mar 26 00:57:00 2020 | 1585173420.0006166 Thu Mar 26 00:58:00 2020 | 1585173480.002517 Thu Mar 26 00:59:00 2020 | 1585173540.001494 """ et = ExecutionTimer(60, align_sleep=True) async def coro(): while True: async with et: print(time.asctime(), '|', time.time()) asyncio.run(coro()) throttler-1.2.2/examples/example_throttlers.py000066400000000000000000000012611433721637400216720ustar00rootroot00000000000000import asyncio import time from throttler import Throttler, ThrottlerSimultaneous async def task(throttler): async with throttler: return await asyncio.sleep(0.1) async def many_tasks(throttler, count: int): coros = [task(throttler) for _ in range(count)] for coro in asyncio.as_completed(coros): _result = await coro print(f'Timestamp: {time.time()}') def example(): async def run(): t = Throttler(rate_limit=10, period=3.0) await many_tasks(t, 100) asyncio.run(run()) def example_simultaneous(): async def run(): t = ThrottlerSimultaneous(count=5) await many_tasks(t, 100) asyncio.run(run()) throttler-1.2.2/examples/example_throttlers_aiohttp.py000066400000000000000000000020471433721637400234250ustar00rootroot00000000000000import asyncio import time import aiohttp from throttler import Throttler, ThrottlerSimultaneous class SomeAPI: api_url = 'https://example.com' def __init__(self, throttler): self.throttler = throttler async def request(self, session: aiohttp.ClientSession): async with self.throttler: async with session.get(self.api_url) as resp: return resp async def many_requests(self, count: int): async with aiohttp.ClientSession() as session: coros = [self.request(session) for _ in range(count)] for coro in asyncio.as_completed(coros): response = await coro print(f'{int(time.time())} | Result: {response.status}') def example(): async def run(): api = SomeAPI(Throttler(rate_limit=3, period=1.0)) await api.many_requests(100) asyncio.run(run()) def example_simultaneous(): async def run(): api = SomeAPI(ThrottlerSimultaneous(count=5)) await api.many_requests(100) asyncio.run(run()) throttler-1.2.2/examples/example_timer.py000066400000000000000000000017441433721637400206060ustar00rootroot00000000000000import random import time from throttler import Timer def example(): """ Output will be like: My Timer | elapsed: 0.85 sec My Timer | elapsed: 0.93 sec My Timer | elapsed: 0.31 sec """ timer = Timer('My Timer') for _ in range(3): with timer: time.sleep(random.random()) def example_verbose(): """ Output will be like: #1 | My Timer | begin: 2020-03-26 01:46:07.648661 #1 | My Timer | end: 2020-03-26 01:46:08.382135, elapsed: 0.73 sec, average: 0.73 sec #2 | My Timer | begin: 2020-03-26 01:46:08.382135 #2 | My Timer | end: 2020-03-26 01:46:08.599919, elapsed: 0.22 sec, average: 0.48 sec #3 | My Timer | begin: 2020-03-26 01:46:08.599919 #3 | My Timer | end: 2020-03-26 01:46:09.083370, elapsed: 0.48 sec, average: 0.48 sec """ timer = Timer('My Timer', verbose=True) for _ in range(3): with timer: time.sleep(random.random()) throttler-1.2.2/readme.md000066400000000000000000000140661433721637400153430ustar00rootroot00000000000000# Throttler [![Python](https://img.shields.io/badge/Python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue.svg?longCache=true)]() [![PyPI](https://img.shields.io/pypi/v/throttler.svg)](https://pypi.python.org/pypi/throttler) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/uburuntu/throttler/blob/master/LICENSE) [![Python Tests](https://github.com/uburuntu/throttler/actions/workflows/tests.yml/badge.svg)](https://github.com/uburuntu/throttler/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/uburuntu/throttler/branch/master/graph/badge.svg)](https://codecov.io/gh/uburuntu/throttler) Zero-dependency Python package for easy throttling with asyncio support. > ![Demo](https://i.imgur.com/MyAALZt.gif) ## 📝 Table of Contents - 🎒 [Install](#-install) - 🛠 [Usage Examples](#-usage-examples) - [Throttler and ThrottlerSimultaneous](#throttler-and-throttlersimultaneous) - [Simple Example](#simple-example) - [API Example](#api-example) - [ExecutionTimer](#executiontimer) - [Timer](#timer) - 👨🏻‍💻 [Author](#-author) - 💬 [Contributing](#-contributing) - 📝 [License](#-license) ## 🎒 Install Just ```sh pip install throttler ``` ## 🛠 Usage Examples All run-ready examples are [here](examples). ### Throttler and ThrottlerSimultaneous **Throttler**: > Context manager for limiting rate of accessing to context block. ```python from throttler import Throttler # Limit to three calls per second t = Throttler(rate_limit=3, period=1.0) async with t: pass ``` Or ```python import asyncio from throttler import throttle # Limit to three calls per second @throttle(rate_limit=3, period=1.0) async def task(): return await asyncio.sleep(0.1) ``` **ThrottlerSimultaneous**: > Context manager for limiting simultaneous count of accessing to context block. ```python from throttler import ThrottlerSimultaneous # Limit to five simultaneous calls t = ThrottlerSimultaneous(count=5) async with t: pass ``` Or ```python import asyncio from throttler import throttle_simultaneous # Limit to five simultaneous calls @throttle_simultaneous(count=5) async def task(): return await asyncio.sleep(0.1) ``` #### Simple Example ```python import asyncio import time from throttler import throttle # Limit to two calls per second @throttle(rate_limit=2, period=1.0) async def task(): return await asyncio.sleep(0.1) async def many_tasks(count: int): coros = [task() for _ in range(count)] for coro in asyncio.as_completed(coros): _ = await coro print(f'Timestamp: {time.time()}') asyncio.run(many_tasks(10)) ``` Result output: ```text Timestamp: 1585183394.8141203 Timestamp: 1585183394.8141203 Timestamp: 1585183395.830335 Timestamp: 1585183395.830335 Timestamp: 1585183396.8460555 Timestamp: 1585183396.8460555 ... ``` #### API Example ```python import asyncio import time import aiohttp from throttler import Throttler, ThrottlerSimultaneous class SomeAPI: api_url = 'https://example.com' def __init__(self, throttler): self.throttler = throttler async def request(self, session: aiohttp.ClientSession): async with self.throttler: async with session.get(self.api_url) as resp: return resp async def many_requests(self, count: int): async with aiohttp.ClientSession() as session: coros = [self.request(session) for _ in range(count)] for coro in asyncio.as_completed(coros): response = await coro print(f'{int(time.time())} | Result: {response.status}') async def run(): # Throttler can be of any type t = ThrottlerSimultaneous(count=5) # Five simultaneous requests t = Throttler(rate_limit=10, period=3.0) # Ten requests in three seconds api = SomeAPI(t) await api.many_requests(100) asyncio.run(run()) ``` Result output: ```text 1585182908 | Result: 200 1585182908 | Result: 200 1585182908 | Result: 200 1585182909 | Result: 200 1585182909 | Result: 200 1585182909 | Result: 200 1585182910 | Result: 200 1585182910 | Result: 200 1585182910 | Result: 200 ... ``` ### ExecutionTimer > Context manager for time limiting of accessing to context block. Simply sleep `period` secs before next accessing, not analog of `Throttler`. Also it can align to start of minutes. ```python import time from throttler import ExecutionTimer et = ExecutionTimer(60, align_sleep=True) while True: with et: print(time.asctime(), '|', time.time()) ``` Or ```python import time from throttler import execution_timer @execution_timer(60, align_sleep=True) def f(): print(time.asctime(), '|', time.time()) while True: f() ``` Result output: ```text Thu Mar 26 00:56:17 2020 | 1585173377.1203406 Thu Mar 26 00:57:00 2020 | 1585173420.0006166 Thu Mar 26 00:58:00 2020 | 1585173480.002517 Thu Mar 26 00:59:00 2020 | 1585173540.001494 ``` ### Timer > Context manager for pretty printing start, end, elapsed and average times. ```python import random import time from throttler import Timer timer = Timer('My Timer', verbose=True) for _ in range(3): with timer: time.sleep(random.random()) ``` Or ```python import random import time from throttler import timer @timer('My Timer', verbose=True) def f(): time.sleep(random.random()) for _ in range(3): f() ``` Result output: ```text #1 | My Timer | begin: 2020-03-26 01:46:07.648661 #1 | My Timer | end: 2020-03-26 01:46:08.382135, elapsed: 0.73 sec, average: 0.73 sec #2 | My Timer | begin: 2020-03-26 01:46:08.382135 #2 | My Timer | end: 2020-03-26 01:46:08.599919, elapsed: 0.22 sec, average: 0.48 sec #3 | My Timer | begin: 2020-03-26 01:46:08.599919 #3 | My Timer | end: 2020-03-26 01:46:09.083370, elapsed: 0.48 sec, average: 0.48 sec ``` ## 👨🏻‍💻 Author **Ramzan Bekbulatov**: - Telegram: [@rm_bk](https://t.me/rm_bk) - Github: [@uburuntu](https://github.com/uburuntu) ## 💬 Contributing Contributions, issues and feature requests are welcome! ## 📝 License This project is [MIT](https://github.com/uburuntu/throttler/blob/master/LICENSE) licensed. throttler-1.2.2/requirements-dev.txt000066400000000000000000000001271433721637400176150ustar00rootroot00000000000000aiohttp>=3.8 codecov>=2.1 flake8>=4.0 pytest>=7.0 pytest-asyncio>=0.16 pytest-cov>=3.0 throttler-1.2.2/setup.py000066400000000000000000000037771433721637400153050ustar00rootroot00000000000000import importlib.util import os import sys import setuptools from pkg_resources import parse_requirements def read(filename: str) -> str: with open(filename, encoding='utf-8') as file: return file.read() def get_module(name: str): spec = importlib.util.spec_from_file_location(name, os.path.join(name, '__init__.py')) module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) return module def load_requirements(filename: str) -> list: requirements = [] for req in parse_requirements(read(filename)): extras = '[{}]'.format(','.join(req.extras)) if req.extras else '' requirements.append( '{}{}{}'.format(req.name, extras, req.specifier) ) return requirements module_name = 'throttler' module = get_module(module_name) setuptools.setup( name=module_name, version=module.__version__, author=module.__author__, author_email=module.__email__, license=module.__license__, description=module.__doc__, platforms='all', long_description=read('readme.md'), long_description_content_type='text/markdown', url='https://github.com/uburuntu/{}'.format(module_name), download_url='https://github.com/uburuntu/{}/archive/master.zip'.format(module_name), packages=setuptools.find_packages(exclude=['examples', 'tests']), include_package_data=True, install_requires=[], extras_require={'dev': load_requirements('requirements-dev.txt')}, keywords=['asyncio', 'aio-throttle', 'aiothrottle', 'aiothrottler', 'aiothrottling', 'asyncio-throttle', 'rate-limit', 'rate-limiter', 'throttling', 'throttle', 'throttler'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: AsyncIO', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', 'Typing :: Typed', ], ) throttler-1.2.2/tests/000077500000000000000000000000001433721637400147175ustar00rootroot00000000000000throttler-1.2.2/tests/__init__.py000066400000000000000000000000001433721637400170160ustar00rootroot00000000000000throttler-1.2.2/tests/service.py000066400000000000000000000021561433721637400167350ustar00rootroot00000000000000import asyncio import random import time from collections import deque class BanException(Exception): pass async def sleeper(value: float): s = random.uniform(0, 0.1) await asyncio.sleep(s) return value class Service: def __init__(self, rate_limit: int, period: float): self.rate_limit = rate_limit self.period = period - 0.01 self.times = deque((0.,) * rate_limit) async def get(self, value: float): curr_ts = time.monotonic() diff = curr_ts - self.times[0] if diff < self.period: raise BanException('Limit exceeded') self.times.popleft() self.times.append(curr_ts) return await sleeper(value) class ServiceSimultaneous: def __init__(self, max_simultaneous: int): self.max_simultaneous = max_simultaneous self.counter = 0 async def get(self, value: float): self.counter += 1 try: if self.counter > self.max_simultaneous: raise BanException('Limit exceeded') return await sleeper(value) finally: self.counter -= 1 throttler-1.2.2/tests/test_execution_timer.py000066400000000000000000000042451433721637400215400ustar00rootroot00000000000000import os import time from math import isclose import pytest from throttler import execution_timer, execution_timer_async # Weak machines may be used for CI, causing delays ABS_TOL = 0.2 if os.getenv('CI') else 0.1 class TestExecutionTimer: @pytest.mark.parametrize( ('period',), ((1.,), (3.,)) ) def test_without_align(self, period: float): @execution_timer(period) def t(): curr_ts = time.time() if i > 0: assert isclose(curr_ts - prev_ts, period, abs_tol=ABS_TOL) return curr_ts prev_ts = None for i in range(3): prev_ts = t() @pytest.mark.parametrize( ('period',), ((3.,), (5.,)) ) def test_with_align(self, period: float): @execution_timer(period, align_sleep=True) def t(): curr_ts = time.time() if i > 0: assert isclose(curr_ts % period, 0., abs_tol=ABS_TOL) if i > 1: assert isclose(curr_ts - prev_ts, period, abs_tol=ABS_TOL) return curr_ts prev_ts = None for i in range(3): prev_ts = t() @pytest.mark.asyncio @pytest.mark.parametrize( ('period',), ((1.,), (3.,)) ) async def test_without_align_async(self, period: float): @execution_timer_async(period) async def t(): curr_ts = time.time() if i > 0: assert isclose(curr_ts - prev_ts, period, abs_tol=ABS_TOL) return curr_ts prev_ts = None for i in range(3): prev_ts = await t() @pytest.mark.asyncio @pytest.mark.parametrize( ('period',), ((3.,), (5.,)) ) async def test_with_align_async(self, period: float): @execution_timer_async(period, align_sleep=True) async def t(): curr_ts = time.time() if i > 0: assert isclose(curr_ts % period, 0., abs_tol=ABS_TOL) if i > 1: assert isclose(curr_ts - prev_ts, period, abs_tol=ABS_TOL) return curr_ts prev_ts = None for i in range(3): prev_ts = await t() throttler-1.2.2/tests/test_service.py000066400000000000000000000020711433721637400177700ustar00rootroot00000000000000import asyncio import pytest from tests.service import BanException, Service, ServiceSimultaneous class TestService: @pytest.mark.parametrize( ('rate_limit', 'period'), ((1, 0.5), (3, 1.0), (100, 1.5)) ) def test_service(self, rate_limit: int, period: float): s = Service(rate_limit, period) async def request(value: float): return await s.get(value) with pytest.raises(BanException): main = asyncio.gather(*[request(v) for v in range(int(rate_limit / period) + 100)]) asyncio.get_event_loop().run_until_complete(main) @pytest.mark.parametrize( ('max_simultaneous',), ((1,), (3,), (100,)) ) def test_service_simultaneous(self, max_simultaneous: int): s = ServiceSimultaneous(max_simultaneous) async def request(value: float): return await s.get(value) with pytest.raises(BanException): main = asyncio.gather(*[request(v) for v in range(max_simultaneous + 100)]) asyncio.get_event_loop().run_until_complete(main) throttler-1.2.2/tests/test_throttler.py000066400000000000000000000017551433721637400203670ustar00rootroot00000000000000import asyncio from itertools import product import pytest from tests.service import Service from throttler import throttle, Throttler class TestThrottler: @pytest.mark.parametrize( ('rate_limit', 'period'), tuple(product((-1, 0, '', 1), (-1, -1., 0, 0., ''))) ) def test_exceptions(self, rate_limit: int, period: float): with pytest.raises(ValueError): Throttler(rate_limit, period) @pytest.mark.parametrize( ('rate_limit', 'period', 'count'), tuple(product((1, 3, 5), (0.5, 1.0, 1.5), (3, 5, 7))) + tuple(product((100, 1000), (0.5, 1.0, 1.5), (10, 1000))) ) def test_via_service(self, rate_limit: int, period: float, count: int): s = Service(rate_limit, period) @throttle(rate_limit, period) async def request(value: float): return await s.get(value) main = asyncio.gather(*[request(v) for v in range(count)]) asyncio.get_event_loop().run_until_complete(main) throttler-1.2.2/tests/test_throttler_simultaneous.py000066400000000000000000000012071433721637400231670ustar00rootroot00000000000000import asyncio import pytest from tests.service import ServiceSimultaneous from throttler import throttle_simultaneous class TestThrottlerSimultaneous: @pytest.mark.parametrize( ('max_simultaneous', 'count'), ((1, 10), (3, 10), (100, 500)) ) def test_via_service_simultaneous(self, max_simultaneous: int, count: int): s = ServiceSimultaneous(max_simultaneous) @throttle_simultaneous(max_simultaneous) async def request(value: float): return await s.get(value) main = asyncio.gather(*[request(v) for v in range(count)]) asyncio.get_event_loop().run_until_complete(main) throttler-1.2.2/tests/test_timer.py000066400000000000000000000012341433721637400174500ustar00rootroot00000000000000import time import pytest from throttler import timer, timer_async class TestTimer: @pytest.mark.parametrize( ('verbose',), ((True,), (False,)) ) def test_simple(self, verbose: bool): @timer(name='TestTimer', verbose=verbose) def t(): print(time.time()) for _ in range(3): t() @pytest.mark.asyncio @pytest.mark.parametrize( ('verbose',), ((True,), (False,)) ) async def test_simple_async(self, verbose: bool): @timer_async(name='TestTimer', verbose=verbose) async def t(): print(time.time()) for _ in range(3): await t() throttler-1.2.2/throttler/000077500000000000000000000000001433721637400156045ustar00rootroot00000000000000throttler-1.2.2/throttler/__init__.py000066400000000000000000000007041433721637400177160ustar00rootroot00000000000000"""Zero-dependency Python package for easy throttling with asyncio support""" from .execution_timer import ExecutionTimer from .throttler import Throttler from .throttler_simultaneous import ThrottlerSimultaneous from .timer import Timer from .decorators import execution_timer, execution_timer_async, throttle, throttle_simultaneous, timer, timer_async __author__ = 'uburuntu' __email__ = 'github@rmbk.me' __license__ = 'MIT' __version__ = '1.2.2' throttler-1.2.2/throttler/decorators.py000066400000000000000000000040611433721637400203240ustar00rootroot00000000000000from functools import wraps from typing import Callable from throttler import ExecutionTimer, Throttler, ThrottlerSimultaneous, Timer def throttle(rate_limit: int, period: float = 1.0): def decorator(func): throttler = Throttler(rate_limit, period) @wraps(func) async def wrapper(*args, **kwargs): async with throttler: return await func(*args, **kwargs) return wrapper return decorator def throttle_simultaneous(count: int): def decorator(func): throttler = ThrottlerSimultaneous(count) @wraps(func) async def wrapper(*args, **kwargs): async with throttler: return await func(*args, **kwargs) return wrapper return decorator def execution_timer(period: float = 60., align_sleep: bool = False): def decorator(func): et = ExecutionTimer(period, align_sleep) @wraps(func) def wrapper(*args, **kwargs): with et: return func(*args, **kwargs) return wrapper return decorator def execution_timer_async(period: float = 60., align_sleep: bool = False): def decorator(func): et = ExecutionTimer(period, align_sleep) @wraps(func) async def wrapper(*args, **kwargs): async with et: return await func(*args, **kwargs) return wrapper return decorator def timer(name: str = None, verbose: bool = False, print_func: Callable = None): def decorator(func): t = Timer(name, verbose, print_func) @wraps(func) def wrapper(*args, **kwargs): with t: return func(*args, **kwargs) return wrapper return decorator def timer_async(name: str = None, verbose: bool = False, print_func: Callable = None): def decorator(func): t = Timer(name, verbose, print_func) @wraps(func) async def wrapper(*args, **kwargs): with t: return await func(*args, **kwargs) return wrapper return decorator throttler-1.2.2/throttler/execution_timer.py000066400000000000000000000026211433721637400213620ustar00rootroot00000000000000import asyncio import time class ExecutionTimer: """ Context manager for time limiting of accessing to context block. Simply sleep `period` secs before next accessing, not analog of Throttler. Also it can align to start of minutes. Example usage: - https://github.com/uburuntu/throttler/blob/master/examples/example_execution_timer.py """ __slots__ = ('_period', '_align_sleep', '_start_time', '_next_time',) def __init__(self, period: float = 60., align_sleep: bool = False): self._period = period self._align_sleep = align_sleep self._start_time = 0. self._next_time = 0. def _start(self): curr_time = time.time() diff = self._next_time - curr_time return diff def _exit(self): next_time = self._start_time + self._period if self._align_sleep: next_time -= self._start_time % self._period self._next_time = next_time def __enter__(self): diff = self._start() if diff > 0.: time.sleep(diff) self._start_time = time.time() def __exit__(self, exc_type, exc_val, exc_tb): self._exit() async def __aenter__(self): diff = self._start() if diff > 0.: await asyncio.sleep(diff) self._start_time = time.time() async def __aexit__(self, exc_type, exc_val, exc_tb): self._exit() throttler-1.2.2/throttler/throttler.py000066400000000000000000000024571433721637400202150ustar00rootroot00000000000000import asyncio import time from collections import deque from typing import Union class Throttler: """ Context manager for limiting rate of accessing to context block. Example usages: - https://github.com/uburuntu/throttler/blob/master/examples/example_throttlers.py - https://github.com/uburuntu/throttler/blob/master/examples/example_throttlers_aiohttp.py """ __slots__ = ('_rate_limit', '_period', '_times',) def __init__(self, rate_limit: int, period: Union[int, float] = 1.0): if not (isinstance(rate_limit, int) and rate_limit > 0): raise ValueError('`rate_limit` should be positive integer') if not (isinstance(period, (int, float)) and period > 0.): raise ValueError('`period` should be positive float') self._rate_limit = float(rate_limit) self._period = float(period) self._times = deque(0. for _ in range(rate_limit)) async def __aenter__(self): while True: curr_ts = time.monotonic() diff = curr_ts - (self._times[0] + self._period) if diff > 0.: self._times.popleft() break await asyncio.sleep(-diff) self._times.append(curr_ts) async def __aexit__(self, exc_type, exc_val, exc_tb): pass throttler-1.2.2/throttler/throttler_simultaneous.py000066400000000000000000000012601433721637400230140ustar00rootroot00000000000000import asyncio class ThrottlerSimultaneous: """ Context manager for limiting simultaneous count of accessing to context block. Should be created inside of async loop. Example usages: - https://github.com/uburuntu/throttler/blob/master/examples/example_throttlers.py - https://github.com/uburuntu/throttler/blob/master/examples/example_throttlers_aiohttp.py """ __slots__ = ('_semaphore',) def __init__(self, count: int): self._semaphore = asyncio.Semaphore(count) async def __aenter__(self): await self._semaphore.acquire() async def __aexit__(self, exc_type, exc_val, exc_tb): self._semaphore.release() throttler-1.2.2/throttler/timer.py000066400000000000000000000024361433721637400173030ustar00rootroot00000000000000from datetime import datetime from typing import Callable class Timer: """ Context manager for pretty printing start, end, elapsed and average times. Example usage: - https://github.com/uburuntu/throttler/blob/master/examples/example_timer.py """ def __init__(self, name: str = None, verbose: bool = False, print_func: Callable = None): self.iteration = 1 self.start_dt = None self.elapsed_all = 0. self.name = name self.verbose = verbose self.print = print_func or print def __enter__(self): self.start_dt = datetime.now() if self.verbose: self.print(f'{f"#{self.iteration}":>5} | {self.name or "Timer"} | begin: {self.start_dt}') def __exit__(self, exc_type, exc_val, exc_tb): curr_dt = datetime.now() elapsed = (curr_dt - self.start_dt).total_seconds() self.elapsed_all += elapsed average = self.elapsed_all / self.iteration if self.verbose: self.print(f'{f"#{self.iteration}":>5} | {self.name or "Timer"} | end: {curr_dt}, elapsed: {elapsed:.2f} sec, ' f'average: {average:.2f} sec\n') else: self.print(f'{self.name or "Timer"} | elapsed: {elapsed:.2f} sec') self.iteration += 1